Skip to main content

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)

  1. Iterate over submitted answers only, which means a required field that was never sent is not checked at all — parity with the source.
  2. A key matching no question produces the error Question with ID {id} not found in form definition.
  3. Non-required questions with an empty value ("" or null) are skipped.
  4. Everything else goes into validateSingleAnswer, and error messages are prefixed with the question's name, resolved from title, then placeholder, and finally Question {id}.
  5. The result is {IsValid, Errors}, which the caller converts into a 400 object body.

validateSingleAnswer(q, value)

  1. 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.
  2. An empty value on a non-required question passes; an empty value on a required question yields This field is required.
  3. commonRule.pattern takes precedence over any field-type rule. When present, that pattern is used along with the commonRule's minLength and maxLength, and the error message falls back through errorMessageTherrorMessage → the default text.
  4. Without a commonRule pattern, the field-type rules apply:
    • email defaults to ^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$, overridable via q.rule.
    • phone defaults to ^[0-9]{10,15}$; if q.rule is present, " characters are stripped from it first.
    • number uses ^[0-9]+$, url uses ^(https?|ftp)://[^\s/$.?#].[^\s]*$, and citizen_id uses ^[0-9]{13}$.
    • custom uses q.regex verbatim; custom_input strips leading and trailing / first and supports min/maxLength plus a Thai message from commonRule, defaulting to กรุณากรอกข้อมูลให้ถูกต้อง.
    • date uses ^\d{4}-\d{2}-\d{2}$.
    • date_of_birth checks the date format, then computes age as the current year minus the birth year, minus one more if the birthday has not yet occurred. Below minAge yields Age must be at least N years.
    • short_text allows 1–255 characters, paragraph allows 1–5000, and custom_text uses the question's own minLength/maxLength, defaulting to 1–1000.
    • terms must be true or "true", otherwise You must agree to the terms and conditions.
    • file_upload always passes.
    • The choice types — single_choice, multiple_choice, dropdown, and multi_select_dropdown — remain unchecked, matching the source where that code is commented out.
    • Unknown types pass.
  5. 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{..}) by util.TranslateJSRegex.
  6. 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.

FileFunctions
internal/formsubmission/validation.goNewFormValidationService(), (*FormValidationService).ValidateAnswers, validateSingleAnswer, validateTextLength, computeAge, utf16Len, stripSlashes, firstNonEmpty, formatJSNumber, decodeQuestionMap, isEmptyValue
internal/util/jsregex.goTranslateJSRegex
internal/formsubmission/errors.gonewValidationError(errors), which builds the 400 object body
internal/formsubmission/entity.goAll question-type constants, such as QFirstName, QEmail, QPhone, QCustomInput, QDateOfBirth, and QTerms

Connections to Other Services

  • Reads question definitions from form_builder.questions after they have passed through transformPattern, so the commonRule key 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.