Validation
Generated types check the constraints of the spec with a plain Go Validate() error method. There
is no reflection and no validation library: each check is a call into the runtime package.
A schema and the method generated for it:
Pet:
type: object
required: [name, tags]
properties:
name: {type: string, minLength: 1, maxLength: 20}
age: {type: integer, minimum: 0}
tags: {type: array, minItems: 1, items: {type: string, pattern: '^[a-z]+$'}}
owner: {$ref: '#/components/schemas/Owner'}
var patternPetTagsItem = regexp.MustCompile(`^[a-z]+$`)
func (p Pet) Validate() error {
var errs validation.Errors
errs.Append("name", validation.MinLength(p.Name, 1))
errs.Append("name", validation.MaxLength(p.Name, 20))
if p.Age != nil {
errs.Append("age", validation.Minimum(*p.Age, 0, false))
}
if p.Tags == nil {
errs.Required("tags")
}
if p.Tags != nil {
errs.Append("tags", validation.MinItems(p.Tags, 1))
for idx, item := range p.Tags {
errs.Append(validation.Index("tags", idx), validation.Pattern(item, patternPetTagsItem, `^[a-z]+$`))
}
}
if p.Owner != nil {
errs.Append("owner", p.Owner.Validate())
}
return errs.Err()
}
Which types get Validate
A struct, union, enum, or named slice or map type gets Validate when it checks something, itself
or through a type it holds. A type that checks nothing has no Validate.
Aliases never get one: the checks of an alias apply where it is used.
models.validation.skip: true turns validation off.
Checks
A value of a declared type is checked by calling its Validate. This holds for fields, array
items, map values and union variants alike. An optional value that is nil is absent and not
checked.
Strings
| Keyword | Check |
|---|---|
minLength, maxLength |
length in characters, not bytes. For format: byte, the length of the base64 text |
pattern |
ECMA-262, see patterns. For format: byte, matched against the base64 text |
format |
one of the formats below |
Numbers
| Keyword | Check |
|---|---|
minimum, maximum, exclusive forms |
the boolean and the numeric form, in every version. See exclusive bounds |
multipleOf |
exact, on the decimal the Go type writes: a float32 0.07 is a multiple of 0.01 |
Arrays
| Keyword | Check |
|---|---|
minItems, maxItems |
the number of items |
uniqueItems |
no item repeats |
Maps
| Keyword | Check |
|---|---|
minProperties, maxProperties |
the number of properties |
propertyNames |
each key: length, pattern, format, const and enum |
Any value
| Keyword | Applies to | Check |
|---|---|---|
const |
strings, numbers, booleans | the value is the constant |
enum |
enum types | the value is one of the constants |
enum |
objects, arrays, unions, any |
the value written as JSON is one of the values, each read into the Go type and written back. See enums compared as JSON |
required |
pointers, slices, maps | not nil. See required |
oneOf, anyOf |
unions | how many variants are set, the discriminator value, then each set variant |
Formats
format checks a string against one of these: uuid, uri, uri-reference, ipv4, ipv6,
hostname, date, date-time and email.
A format that becomes its own Go type is checked by decoding:
| Format | Go type | Check |
|---|---|---|
date-time |
time.Time |
decoding |
date |
runtime.Date |
decoding |
email |
runtime.Email |
decoding, then its Validate checks the address |
A format that models.format-types maps to a type of your own works the same way.
An email address is checked as RFC 5321 writes it: a@example.com, "a b"@example.com,
a@[10.0.0.1]. Only ASCII is valid: josé@example.com is not an email.
Enums compared as JSON
An enum of objects, arrays, unions or any matches a value as Go holds it. The value
{"x": 0, "y": null} matches Corner{X: new(0)}, since a nil Y is how Go holds null.
Key order and number form do not count.
A value that does not fit the schema is left out, with a warning (enum-value).
allOf
Under allOf, every member's limits hold:
- The strictest of each limit is checked.
- Every
patternandmultipleOfis checked. - An
enumorconstkeeps the values all members allow.
Exclusive bounds
exclusiveMinimum and exclusiveMaximum are read by their value:
| Value | Meaning |
|---|---|
| a number | the bound |
true or false |
makes minimum or maximum exclusive |
The form of the other version still counts, with a warning (keyword-version). That is a number
in a 3.0 spec, or a boolean in a 3.1 one.
Keywords that cannot be checked
A keyword that does not fit the Go type is left out: minLength on a number, a const of 2.5 on
an integer.
A keyword with a value of the wrong kind is left out, with a warning (keyword-invalid). Examples
are minLength: abc, required: true on a property, and items holding a list. The rest of the
schema still counts.
A schema that is not an object, such as name: string, reads as any, with a warning
(schema-invalid).
These keywords are not checked, with a warning (keyword-unsupported):
patternPropertiesprefixItemsnotcontains,minContains,maxContainsdependentRequired,dependentSchemasunevaluatedProperties,unevaluatedItemspropertyNameson a struct without additional properties, which keeps no other keys to check
required
required on a pointer, slice or map field means it is not nil.
A required field that is a plain value, such as Name string, gets no presence check. After JSON
decoding, a missing name and an empty one look the same. A server with validation.request checks
that the key is in the request body before it decodes it, see
request bodies.
Patterns
A pattern is ECMA-262, the dialect of JSON Schema. It is compiled once, into a package-level variable. Generation writes it in Go's syntax with the meaning ECMA-262 gives it:
| Spec | Go | Why |
|---|---|---|
\s, \S |
[\t-\r \xA0\x{1680}...], [^...] |
Go's \s knows ASCII spaces only |
. |
[^\n\r\x{2028}\x{2029}] |
Go's . matches \r and U+2028 |
[^], [] |
[\s\S], [^\s\S] |
any character, none |
\cJ |
\n |
a control character |
\uXXXX |
\xHH, \x{XXXX} |
Go has no \u |
[\b] |
\x08 |
a backspace |
[[:alpha:]] |
[\[:alpha:]] |
a [ in a class is a character, not a POSIX class |
An escape ECMA-262 does not have, such as \A or \z, keeps Go's meaning.
A pattern Go cannot compile, such as one with a lookahead or a backreference, is not checked, with a warning that names it.
An error quotes the pattern as the spec writes it: tags[1]: must match ^[a-z]+$.
Error paths
validation.Errors, of the package pkg/runtime/validation, collects every failed check with the
path of its value:
name: must be at least 1 characters long; tags[1]: must match ^[a-z]+$; labels["team"]: must be at most 5 characters long
errors.As finds the *validation.Errors and each validation.Error in it.
Map values come out in key order, so the same value gives the same error text on every run. A key
that fails propertyNames is reported under the path of its value:
labels["Bad Key"]: must match ^[a-z]+$.
Rule and Limit
Each error also names the keyword that failed, Rule, and its value in the spec, Limit. A
service can use them to write messages of its own:
var errs *validation.Errors
if errors.As(err, &errs) {
for _, e := range *errs {
// e.Field "name", e.Rule validation.RuleMinLength, e.Limit 2
}
}
| Rule | Limit |
|---|---|
minLength, maxLength, minItems, maxItems, minProperties, maxProperties |
the number, an int |
minimum, maximum, exclusiveMinimum, exclusiveMaximum, multipleOf |
the number, a float64 |
pattern |
the pattern as the spec writes it |
format |
the name of the format |
const |
the value |
enum |
the values, a slice of the enum type, or of the Go type for an enum compared as JSON |
uniqueItems |
true |
additionalProperties |
false |
required, type, oneOf, anyOf, discriminator |
nil |
Two rules come from the server's check of a request body:
type: anullthe schema does not take.additionalProperties: a key a closed object does not take.
A key that fails propertyNames has the rule of the check it failed. An error added with Add
has no rule.
readOnly and writeOnly
A readOnly value only comes in responses, a writeOnly one only in requests.
| Method | Checks a value as | Leaves out |
|---|---|---|
Validate |
a request carries it | readOnly fields |
ValidateResponse |
a response carries it | writeOnly fields |
ValidateResponse also calls ValidateResponse on the types it holds. It is generated with
models.validation.response: true, for the types whose response checks differ.
user.Validate() // name and password
user.ValidateResponse() // id, name and roles