Form Answer Validation
Overview
The server-side validation engine that runs before answers are saved. It is the real gate, not a UI hint: client-web validates first, but a hand-crafted request can bypass that entirely. The engine is a purely stateless service and never touches the database.
The key thing to know is that this engine was ported from JS and therefore has to reproduce JS's quirks exactly. Length, for example, must be counted in UTF-16 code units just as String.length does, not in bytes, so that Thai text passes or fails exactly as it did before.
Business Flow
ValidateAnswers(questions, answers)
- Iterate over submitted answers only, which means a required field that was never sent is not checked at all — parity with the source.
- A key matching no question produces the error
Question with ID {id} not found in form definition. - Non-required questions with an empty value (
""ornull) are skipped. - Everything else goes into
validateSingleAnswer, and error messages are prefixed with the question's name, resolved fromtitle, thenplaceholder, and finallyQuestion {id}. - The result is
{IsValid, Errors}, which the caller converts into a 400 object body.
validateSingleAnswer(q, value)
- Coerce the value to a string: strings are trimmed, numbers use JS formatting, booleans become
"true"or"false", and anything else is rendered as JSON. - An empty value on a non-required question passes; an empty value on a required question yields
This field is required. commonRule.patterntakes precedence over any field-type rule. When present, that pattern is used along with the commonRule'sminLengthandmaxLength, and the error message falls back througherrorMessageTh→errorMessage→ the default text.- Without a commonRule pattern, the field-type rules apply:
emaildefaults to^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$, overridable viaq.rule.phonedefaults to^[0-9]{10,15}$; ifq.ruleis present,"characters are stripped from it first.numberuses^[0-9]+$,urluses^(https?|ftp)://[^\s/$.?#].[^\s]*$, andcitizen_iduses^[0-9]{13}$.customusesq.regexverbatim;custom_inputstrips leading and trailing/first and supports min/maxLength plus a Thai message from commonRule, defaulting toกรุณากรอกข้อมูลให้ถูกต้อง.dateuses^\d{4}-\d{2}-\d{2}$.date_of_birthchecks the date format, then computes age as the current year minus the birth year, minus one more if the birthday has not yet occurred. BelowminAgeyieldsAge must be at least N years.short_textallows 1–255 characters,paragraphallows 1–5000, andcustom_textuses the question's ownminLength/maxLength, defaulting to 1–1000.termsmust betrueor"true", otherwiseYou must agree to the terms and conditions.file_uploadalways passes.- The choice types —
single_choice,multiple_choice,dropdown, andmulti_select_dropdown— remain unchecked, matching the source where that code is commented out. - Unknown types pass.
- When a regex is present and the value is non-empty, the pattern is compiled and matched. It is first translated from JS escapes (
\uXXXX,\u{..}) into RE2 form (\x{..}) byutil.TranslateJSRegex. - A pattern RE2 cannot compile — lookaround or backreferences, which Go does not support — is not treated as a user error. It returns
Invalid validation pattern configured for this field, signaling a configuration problem.
Key Files & Functions
This feature has no routes of its own; it is called from formsubmission.Service.Submit.
| File | Functions |
|---|---|
internal/formsubmission/validation.go | NewFormValidationService(), (*FormValidationService).ValidateAnswers, validateSingleAnswer, validateTextLength, computeAge, utf16Len, stripSlashes, firstNonEmpty, formatJSNumber, decodeQuestionMap, isEmptyValue |
internal/util/jsregex.go | TranslateJSRegex |
internal/formsubmission/errors.go | newValidationError(errors), which builds the 400 object body |
internal/formsubmission/entity.go | All question-type constants, such as QFirstName, QEmail, QPhone, QCustomInput, QDateOfBirth, and QTerms |
Connections to Other Services
- Reads question definitions from
form_builder.questionsafter they have passed throughtransformPattern, so thecommonRulekey is already attached. See Loading a Form's Structure and Central Validation Rules. - Invoked at step 5 of Form Answer Submission — before profile mapping and before the OTP gate.
- Touches no tables and no external services.
- The related client-web feature is
form-fill, since the error messages shown on the form originate from this engine.