Skip to content
Talk to our solutions team

Validations

A validation is a declarative rule attached to a field. data.svc evaluates every rule on create and update before any SQL is emitted; a failure aborts the mutation and the row is never written.

Rules live in a field’s validations: list. Each entry is a mapping with a type: and, depending on the type, a literal value: and/or named parameters written as sibling keys.

entities:
- name: contacts
fields:
- name: email
type: string
validations:
- type: required
- type: email
message: enter a work email address
code: CONTACT_EMAIL
- name: handle
type: string
validations:
- type: pattern
name: slug
- type: length
min: 3
max: 32
- name: website
type: string
validations:
- type: url
protocols: [https]
KeyMeaning
typeRule selector. Unknown values are rejected at write time, not at load time.
valueThe rule’s single literal argument (minlength, maxlength, min, max, divisibleby, contains, startswith, …).
messageOverrides the default message verbatim.
codeOverrides the default machine code.
any other keyCollected into the rule’s named parameters (min, max, name, values, protocols, version, types, unit, expression).

Two field-level shorthands expand into the same rules: maxlength: N appends a maxlength rule and minlength: N appends a minlength rule. Both are ignored when the value is 0.

required and unique are handled specially wherever they appear — in validations: or in modifiers: — and are hoisted onto the field’s structural flags, so modifiers: [required, unique] works as written.

Field validation and cross-field validation both run in the BeforeValidate phase, after default filling and the access gate. Both run before anything touches the database.

PhasePriorityHookDoes
BeforeValidate100defaultsFills defaultvalue: on create
BeforeValidate100accessEvaluates access rules
BeforeValidate200validateField validations: and modifiers
BeforeValidate250entityvalidateEntity entityvalidations:
AfterValidate260filefieldStores uploaded file parts
AfterValidate350businessconstraintMulti-entity business rules
AfterValidate350stateflowState transitions
BeforeExec290materializedEvaluates write-time computed fields

Consequences worth holding on to:

  • Defaults are applied first, so a rule sees the defaulted value, not the absent one.
  • Computed fields are materialized after validation. A rule can never see a value computed by the same write.
  • File uploads are stored after validation, so mimetype and filesize inspect the inbound multipart part, not the stored reference.

Every rule below skips silently when the value is of the wrong Go kind — string rules on a number, numeric rules on a string. Every rule also skips when the field is absent or null; the create-time required check is the only thing that fires on a missing value. Modifiers are checked only when the payload carries a value for the field.

"x" in a message stands for the field name, quoted.

TypeParametersRejectsCodeDefault message
requiredValue missing on createREQUIREDfield "x" is required
requiredBlank-or-whitespace string on createREQUIREDfield "x" must not be empty
requiredPresent but empty string, list, or mapREQUIREDfield "x" is required
readonlyField present in an update payloadREADONLYfield "x" is read-only and cannot be modified after creation
finalField present in an update payloadFINALfield "x" is final and cannot be modified
writeonceField present in an update payloadWRITE_ONCEfield "x" can only be written once
uniqueNothing. See below.

readonly, final, and writeonce are modifiers: they are lifted out of validations: into the field’s modifier list and enforced only when the update payload actually carries a value for the field. Accepted spellings are readonly/read-only, final/immutable, writeonce/write-once/writeonceonly, and writeonly/write-only.

TypeParametersRejectsCodeDefault message
emailNot [email protected] shapeEMAILfield "x" must be a valid email address
regexexpression: (or value:)String does not match the expressionPATTERNfield "x" does not match required pattern
patternname:String does not match the named patternPATTERNfield "x" must match named pattern <name>
patternname:Name is not one of the six belowPATTERN_UNKNOWNfield "x": unknown named pattern "<name>"
patternvalue:String does not match the raw expressionPATTERNfield "x" does not match pattern
lengthmin:Fewer than min runesMIN_LENGTHfield "x" must be at least N characters
lengthmax:More than max runesMAX_LENGTHfield "x" must be at most N characters
lengthvalue: aloneRune count is not exactly valueLENGTHfield "x" must be exactly N characters
minlengthvalue:Fewer than value runesMIN_LENGTHfield "x" must be at least N characters
maxlengthvalue:More than value runesMAX_LENGTHfield "x" must be at most N characters
jsonString is not parseable JSONJSONfield "x" must be valid JSON: <parse error>

Length is counted in runes, so a multibyte character costs one. The exact-length form of length applies only when the rule carries no named parameters at all.

json accepts any already-structured value without inspecting it; only strings are parsed.

pattern with name: selects one of six built-in expressions.

NameAccepts
alphaA–Z, a–z, one or more
alphanumericA–Z, a–z, 0–9, one or more
numeric0–9, one or more
slugLower-case alphanumeric groups joined by single hyphens
hexHexadecimal digits, one or more
base64Base64 alphabet with optional = padding

The name is matched case-insensitively. Anything else fails the write with PATTERN_UNKNOWN — an unknown pattern is never treated as “no rule”.

TypeParametersRejectsCodeDefault message
urlUnparseable, or missing scheme or hostURLfield "x" must be a valid URL
urlprotocols: [..]Scheme not in the list (case-insensitive)URL_PROTOCOLfield "x" must use one of [https]
phoneShorter than 8 or longer than 20 characters after an optional leading +, does not start and end with a digit, or contains anything but digits, whitespace, hyphens, dots and parenthesesPHONEfield "x" must be a valid phone number
creditcardFails the Luhn checksum, or contains a non-digit other than space and hyphenCREDITCARDfield "x" must be a valid card number
uuidNot canonical 8-4-4-4-12 hexUUIDfield "x" must be a valid UUID
ulidNot 26 upper-case Crockford base32 charactersULIDfield "x" must be a valid ULID
cuidversion: v1Not c + 24 lower-case alphanumericsCUID_V1field "x" must be a valid cuid v1
cuidversion: v2Not a lower-case letter + 23–31 lower-case alphanumericsCUID_V2field "x" must be a valid cuid v2
cuidomittedMatches neither v1 nor v2CUIDfield "x" must be a valid cuid (v1 or v2)
nanoidNot 8–64 characters of A–Za–z0–9_-NANOIDfield "x" must be a valid nanoid
semverNot SemVer 2.0.0 major.minor.patch with optional pre-release and buildSEMVERfield "x" must be a valid semver
datauriNot a data: URIDATAURIfield "x" must be a valid data: URI

uuid accepts any version digit and both hex cases. Pin a specific version by layering a regex rule on top.

cuid also accepts version: 1 and version: 2 as spellings.

TypeParametersRejectsCodeDefault message
ipNot a parseable IP addressIPfield "x" must be a valid IP address
ipversion: v4Parses but is not IPv4IP_V4field "x" must be a valid IPv4 address
ipversion: v6Parses but is IPv4 or IPv4-mappedIP_V6field "x" must be a valid IPv6 address
ipv4Not a parseable IPv4 addressIPV4field "x" must be a valid IPv4 address
ipv6Not a parseable IPv6 address, or IPv4-mappedIPV6field "x" must be a valid IPv6 address
hostnameEmpty, longer than 253 characters, or not RFC 1123 label shapeHOSTNAMEfield "x" must be a valid hostname
macaddressNot six colon- or hyphen-separated hex octetsMACADDRESSfield "x" must be a valid MAC address
macaddrAlias of macaddressMACADDRESSfield "x" must be a valid MAC address
portInteger outside 1–65535PORTfield "x" must be a port number between 1 and 65535

ip without a version: accepts both families.

TypeParametersRejectsCodeDefault message
minvalue:Number below the boundMINfield "x" must be ≥ V
maxvalue:Number above the boundMAXfield "x" must be ≤ V
minmaxmin:Number below minMINfield "x" must be ≥ V
minmaxmax:Number above maxMAXfield "x" must be ≤ V
divisiblebyvalue:Division leaves a remainderDIVISIBLEBYfield "x" must be divisible by V
latitudeOutside −90…90LATITUDEfield "x" must be a latitude in [-90, 90]
longitudeOutside −180…180LONGITUDEfield "x" must be a longitude in [-180, 180]

Numeric rules accept int, int32, int64, float32, and float64. A numeric string — "42" sent as JSON text — is not coerced and skips the rule entirely. A divisibleby with a divisor of 0 is skipped rather than failing.

TypeParametersRejectsCodeDefault message
enumvalues: [..]Value not in the listENUMfield "x" must be one of [a b]
invalues: [..]Value not in the listINfield "x" must be one of [a b]
notinvalues: [..]Value in the listNOTINfield "x" must not be one of [a b]
containsvalue:Substring absentCONTAINSfield "x" must contain "s"
notcontainsvalue:Substring presentNOTCONTAINSfield "x" must not contain "s"
startswithvalue:Prefix absentSTARTSWITHfield "x" must start with "s"
endswithvalue:Suffix absentENDSWITHfield "x" must end with "s"

Membership compares by equality first, then by string rendering — so values: [1, 2] matches a payload value of "1". values: must be a list; a comma-separated string is not accepted here.

The four substring rules take their argument from value:. A rule with no argument is skipped.

TypeParametersRejectsCodeDefault message
futureInstant is not strictly after nowFUTUREfield "x" must be in the future
pastInstant is not strictly before nowPASTfield "x" must be in the past

Accepted inputs are a native timestamp or a string in RFC 3339 (with or without fractional seconds), 2006-01-02 15:04:05, or 2006-01-02. Anything else skips the rule. Comparison is against the server clock at the instant the rule runs, and is strict — an instant equal to now fails both rules.

mimetype and filesize read the multipart part a client uploaded — its declared content type and byte length — before the part is written to storage.

TypeParametersRejectsCodeDefault message
mimetypetypes: [..]Content type matches no entryMIMETYPEfield "x" file type image/png not allowed
filesizemin:, unit:Smaller than the boundFILESIZE_MINfield "x" file too small (< N MB)
filesizemax:, unit:Larger than the boundFILESIZE_MAXfield "x" file too large (> N MB)

types: entries match exactly or with a trailing wildcard: image/* accepts image/png. unit: is one of B, KB, MB, GB, binary multiples, defaulting to B; an unrecognised unit is treated as bytes. Both rules skip when the value carries no content type or size — a plain string reference, for instance.

- name: avatar
type: file
validations:
- type: mimetype
types: [image/png, image/jpeg]
message: upload a PNG or JPEG
- type: filesize
max: 2
unit: MB

A rule that compares two fields belongs at the entity level, in entityvalidations:. Each entry is a script expression that must return a truthy value.

entities:
- name: bookings
fields:
- name: starts_at
type: datetime
- name: ends_at
type: datetime
entityvalidations:
- name: window_is_ordered
language: expr
rule: "row.ends_at > row.starts_at"
message: the end of a booking must follow its start
code: BOOKING_WINDOW
KeyMeaningDefault
nameReported as the failing “field”
languageExpression languageexpr
ruleInline expression body; an empty rule is skipped
messageOverrides the defaultentity validation "<name>" failed
codeOverrides the defaultENTITY_VALIDATE

Bindings are the same vocabulary access rules use, plus the row under validation:

BindingValue
rowThe mutation payload as a map
<field>Every payload key, promoted to top level when it does not collide
user.id, user.roles, user.tenant_id, user.service, user.session_idCaller identity
tenantTenant key
entityEntity name
action / operationCreate or Update
user_id, rolesFlat aliases of the nested form

null, false, 0, "", and the string "false" are falsy; everything else passes. A rule that throws is reported as a failure with code ENTITY_VALIDATE_ERR and the message entity validation "<name>" failed to evaluate: <error>.

Within one field, rules run in declaration order: modifiers first, then validations: top to bottom. Across fields, the order is unspecified — fields are held in a map and iterated in whatever order the runtime chooses.

The validator collects every failure it finds, but only the first one reaches the caller: the error value renders as its first element. The structured field, rule, and code are dropped for every failure, including that one — the response carries a message string and nothing else.

Field and entity validation failures are typed, so they map to 422 Unprocessable Entity regardless of how the message is worded. The body is the standard error envelope described in the HTTP reference.

POST /rest/contacts
Content-Type: application/json
Authorization: Bearer <token>
{"email": "not-an-address"}
{
"error": "hook validate (BeforeValidate): field \"email\" must be a valid email address",
"code": "v2.create_failed"
}

The same failure on PATCH or PUT carries v2.update_failed. The hook <name> (<phase>): prefix is added by the execution chain and is present on every hook error.

OutcomeStatuscode
Field or entity validation failed on create422v2.create_failed
Field or entity validation failed on update422v2.update_failed
Unknown validation type in the schema422v2.create_failed / v2.update_failed
Access rule denied the operation403v2.create_failed / v2.update_failed

An unrecognised type: is not ignored. The rule is evaluated, finds no handler, and fails the mutation:

field "status": unknown validation type "exsts"

with code VALIDATION_UNKNOWN. This is a runtime failure, not a load-time one — a schema with a typo’d rule loads and serves reads normally, and only breaks on the first write that carries that field. Exercise every write path after editing rules.

These are accepted by the schema loader and reach the compiled schema, but the shipping service never acts on them. Nothing warns you.

DeclarationStatus
type: before / type: afterThe rule reads its reference instant from a named value parameter that the loader never populates — a value: key lands on the literal-argument slot instead. A declared rule never rejects anything. Express date ordering as an entity validation.
type: exists with entity: / field:No handler. Fails every write on that field with VALIDATION_UNKNOWN. Use a reference for referential integrity.
type: customReturns without evaluating. Field-level rules have no script runtime attached; entity validations do.
type: uniqueShape check only; no database lookup.
type: writeonlyLifted to a modifier and then never read. It is not a write-time check, and the read-side hook that strips write-only fields from a result is not registered in data.svc — the value comes back on read.
when: on a ruleParsed into the rule’s parameters and never read. There is no conditional validation.
scope: on uniqueParsed for compound uniqueness and never read.
region: on phoneParsed into parameters and never read; the pattern is not locale-aware.
length / minlength / maxlength on a list fieldString-only. A list of any size passes.
Numeric rules on a string-typed valueNot coerced. "42" skips min, max, minmax, divisibleby, and port.
  • Entities — the field definitions rules attach to
  • Types — what each field type accepts before a rule sees it
  • Enums — named value sets, and why they need an explicit rule
  • Stateflows — the transition check that runs after validation
  • Access rules — the gate that runs before it