Zod Validation
Generated schemas accept and reject what the Go server does: validator’s rules applied to the value encoding/json decodes. This page lists the cases where that behavior is not obvious. For everyday use, see Zod Schemas.
Format checks are tested against Go 1.26, Go 1.27, and validator v10.30.4. Email, URL, IP, and timestamp checks use parsers derived from Go’s, and Unicode character classes use the generator’s Go Unicode tables.
- String
min,max,len,gt,gte,lt, andltecount Unicode code points, as validator counts runes. 0x2Cand0x7Cin a parameter stand for a literal comma and pipe.- A format combined with another rule must satisfy both:
email,oneof=admin@example.comaccepts only that address. numericon a numeric Go field keeps the numeric schema instead of applying a string pattern.- Unsupported tags and invalid parameters become comments in the generated schema. Set
Strictin custom validation rules to fail generation instead. - A rule that panics in validator on the field’s Go kind, such as
oneofon a float orlowercaseon a number, fails generation. - Malformed
diveorkeysscopes fail generation.
Required And Omission
Section titled “Required And Omission”validate:"omitempty"does not make a key optional; only an optional TypeScript property does. See Struct Tags.- Rules before an omission tag always run:
required,omitempty,min=3rejects""throughrequiredand skips onlymin. omitnilskips nil pointers, maps, and slices, and still validates scalar zero values.omitemptyon a map or slice skips only nil. A present empty collection still runs the later rules.omitzerolooks through pointers: a pointer to0or""skips the later rules, and so does an empty map or slice.encoding/jsondecodes every JSON string, even"", into a non-nil[]byte. On a[]bytefield,omitemptyandomitnilnever skip a supplied value andrequiredaccepts any value.omitzeroskips a value whose decoded length is zero, such as""or a string of line breaks.structonlyandnostructlevelend a field’s rule list; rules after them never run in Go and are not generated.- On a struct field,
structonlyandnostructlevelskip the nested struct’s field rules, so only its JSON shape is checked.structonlystill applies the struct’sStructRules;nostructlevelskips them too.
Containers
Section titled “Containers”- A slice or map of structs validates its elements only after
dive. Without it, elements are checked for JSON shape only. Struct fields and pointers to structs are always validated. - The exported schema of a named slice, array, or map type validates every struct its elements reach, as
StructValidatordoes for a procedure input. A struct field of that type still validates its elements only afterdive. - Integer map keys must fit the Go key type.
"01","+1", and"1"name the same entry: keys are normalized to decimal before length,unique, key rules, anddiverun. Parsed output uses the normalized keys and leaves the input unchanged. - Map keys of a named scalar type accept any value of that type. Declared constants neither require entries nor restrict keys.
[]bytefields are Base64 in JSON. Size rules count decoded bytes, and element rules validate those bytes.uniqueworks on comparable values, andunique=Fieldon struct elements.uniquefails generation on noncomparable values, which panic in validator, and on nested pointer identity.unique=Fieldfails when the field is missing, unexported, or not in the JSON.uniqueoverjson.Numberfails generation unless the field hasjson:",string", which keeps the text Go compares.
Fixed Arrays
Section titled “Fixed Arrays”- Go fixed arrays keep their declared length. Schemas pad a short array with Go zero values and ignore extra entries before validating.
- Padding can produce
nullfor pointers, maps, and slices, including inside nested structs. Generated types allownullin those positions, so parsed data stays assignable toRouterInputsandRouterOutputs. Named struct declarations andtstypeoverrides are unchanged. requiredrejects an all-zero array.omitemptyskips the later rules, includingdive, for an all-zero array.- A non-nil pointer or an empty map or slice inside an array counts as nonzero.
- A missing optional array is validated as the zero array Go creates.
- Element rules run only after
dive. JSON type and range checks always apply. - A
time.Timeelement is zero only when it is Go’s zero time in UTC, asreflect.Value.IsZerosees it. Fractional digits after the ninth are dropped, and any explicit offset, even+00:00, makes the value nonzero. - Zero-value rules that depend on fields JSON cannot set, or on the zero values of
json.Numberandjson.RawMessage, fail generation.
Cross-Field Rules
Section titled “Cross-Field Rules”- Numbers compare decoded values. Integers with
json:",string"compare without losing precision. - String
eqfieldandnefieldcompare contents.gtfield,gtefield,ltfield, andltefieldon strings compare UTF-8 byte lengths. - Array, slice, and map
eqfieldandnefieldcompare lengths, not elements. Ordered rules on collections follow validator’s reflection behavior; use size tags such asmin=2orgt=1for size limits. time.Timecompares instants across timezone offsets, to the nanosecond.- Fields of different Go kinds compare as validator compares them, even when their TypeScript types match:
eqfieldbetween anintand anint64fails. - A target that JSON never sets, such as an unexported field or one tagged
json:"-", compares as its Go zero value. - A target field that does not exist fails the comparison, except for
nefield, which passes. - Refinements respect pointer presence, omission tags, and whether a field promoted from an embedded pointer exists.
- An OR group produces one check with one error path.
- Nested paths such as
eqfield=Address.Codefail generation.*csfieldrules are not supported.
Numbers
Section titled “Numbers”- Integer schemas require whole numbers within the Go type’s bounds where JavaScript can represent them. Go
intuses Zod’s safe-integer range, and largerint64anduint64values lose precision as JavaScript numbers. - Generic schemas keep the Go kinds of their type arguments, so
Box[int8]andBox[int16]have different bounds even though both are TypeScript numbers. float32values and numeric rule parameters are rounded to 32 bits before comparison:1.00000001compares as1. Parsed output keeps the original number.- Declared constants do not restrict a named type: its schema accepts any value of the underlying type. Add
oneofto restrict it. - A
json.Numberfield is anumberin TypeScript and in its schema, so a quoted numeric string that Go accepts is rejected. Its rules compare the decimal text JavaScript prints, where validator compares the text Go stored.
JSON String Fields
Section titled “JSON String Fields”Use json:",string" when an integer must stay exact across its full range:
type LedgerInput struct { Amount int64 `json:"amount,string" validate:"gte=0"`}The TypeScript property is a string, and the schema validates the integer with BigInt. Parsing returns the string unchanged.
- The option also works on float, bool, string, and
json.Numberfields, and keeps their string encoding. - Quoted floats follow Go’s grammar for the option, including hexadecimal floats and digit separators, and round to the field’s float size. Cross-field comparisons use the same rounding.
- A quoted
json.Numberaccepts what Go’s decoder stores, and its rules see that text. See Go Versions. - The string
"null"validates as a JSONnulldoes in Go: pointers are nil and other fields keep their zero value. Parsing keeps"null".
Strings And Time
Section titled “Strings And Time”- String rules check the text Go decodes. Unpaired surrogate escapes become U+FFFD, as in Go; valid pairs are kept.
time.Timeaccepts Go’s JSON timestamp grammar, including offsets, comma fractions, and the other spellings Go accepts. Parsing keeps the original string, and comparisons use its offset and full nanosecond precision.- Go’s
time.Time.UnmarshalJSONcan tell escaped timestamp characters from literal ones.JSON.parsehas already decoded them, so schemas cannot. min,max,gt,gte,lt, andlteontime.Timecompare with the server’s current time in Go and become comments in the schema.
decodeGoJSON
Section titled “decodeGoJSON”decodeGoJSON decodes JSON text as Go does before validating it:
- Field names match case-insensitively, with Go’s case folding.
- Repeated struct fields merge according to their Go types. Repeated map fields combine entries, and each map entry decodes into a fresh value.
- A value Go cannot decode fails at its path in the text, even if a later key overwrote it.
- Validator rules apply to the final value of each field or entry.
- Unknown keys are checked at every occurrence in the text.
- JSON nested more than 10,000 levels deep fails, as in Go.
safeParse on an object from JSON.parse cannot see overwritten keys, but it rejects keys that name the same Go entry, such as "01" and "1" in a map[int] field.
decodeGoJSON is not a complete Go decoder. It does not run custom UnmarshalJSON or UnmarshalText methods, and map key types it cannot decode fail generation.
Go Versions
Section titled “Go Versions”Schemas follow the Go toolchain that generates them: its Unicode tables and its encoding/json decoder. Go 1.27 moved to Unicode 17 and to encoding/json/v2. Building with GOEXPERIMENT=nojsonv2 keeps the original decoder.
alphaunicode,alphanumunicode,lowercase,uppercase, and field-name matching use the generating Go’s Unicode tables.urlfollows the generating Go’snet/url, including itsurlstrictcolonsGODEBUG setting. Go 1.27 rejects a bracket after the first character of a host and accepts extra host colons for every scheme buthttpandhttps.- A tag naming a key Go rejects keeps the Go field name. Go 1.27’s default decoder accepts symbols such as emoji, and ignores a field whose tag name holds a backslash or quote.
- Go 1.27’s default decoder accepts a leading
+in quoted integers, and anystrconv.ParseFloatsyntax in quoted floats, includingInfandNaN. - A quoted
NaNis nonzero forrequiredand unequal to every value, itself included. - Go 1.27’s default decoder stores only a JSON number in a quoted
json.Number. The original decoder also stores text that starts like a number, or a nested string literal holding one. - Go 1.27’s default decoder rejects an unpaired surrogate escape inside a quoted string’s nested literal.
Custom Rule Predicates
Section titled “Custom Rule Predicates”- Scalar predicates receive the value Go decodes:
float32values are rounded, unpaired surrogates are replaced, and integerjson:",string"fields and integer map keys arrive asbigint. - Collection predicates and
StructRulesreceive the parsed JSON value. - The parameter is a string, with
0x2Cand0x7Cdecoded. - Aliases expand before field names resolve and before
diveandkeysscopes compile. - Alias cycles, malformed scopes, aliases used in OR groups or given parameters, and rules used outside their
GoKindsfail generation. - Built-in tags cannot be redefined. Set a different
TagNameto change their behavior. - A
StructRuleskey that matches no type, several types, or a non-struct fails generation. StructRulesdo not translate Go struct-level validation functions. Attach checks for fields promoted from embedded structs to the containing type.Strictaccepts rules markedServerOnly.
Zod-Only Omit
Section titled “Zod-Only Omit”- A
zod_omitfield becomes an optional property with no runtime check, typed as the field’s TypeScript type, such asz.custom<string>().optional(). Strict schemas accept the key, and its value passes through unchanged, including throughdecodeGoJSON. - Named types used only by omitted fields get type-only declarations and no schema, so their rules never run.
Limits
Section titled “Limits”- JSON
nullon a non-pointer field and a missing required key are rejected, although Go leaves the zero value for both. Clients of the generated types cannot send either, anddecodeGoJSONrejects them too. - Go validator functions and options are not read from
WithValidator. Declare client counterparts with custom validation rules. - Conditional tags such as
required_ifandexcluded_with, and nested field namespaces, are not supported. - Values of a recursive type nested a few thousand levels deep can exceed the JavaScript stack, although Go decodes up to 10,000 levels.
- Objects reject unknown keys by default, matching strict input.
WithStrictInput(false)or--zod-allow-unknown-fieldsgenerates objects that keep them. - Nullability, coercion, defaults, and transformations are not inferred.