jsonschema: validate logic keywords
Validate keywords that combine schemas with various logical operators. Also add iterators over schemas. Now we only need them to skip some tests, but later they will come in handy for non-test code. Change-Id: Idaf41efea4d693b6be0ee5c6c3a527664b8c472f Reviewed-on: https://go-review.googlesource.com/c/tools/+/668115 Reviewed-by: Alan Donovan <adonovan@google.com> LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
This commit is contained in:
parent
061f842459
commit
9396e7f6a6
8 changed files with 1517 additions and 5 deletions
|
|
@ -11,11 +11,31 @@ import (
|
|||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"iter"
|
||||
)
|
||||
|
||||
// A Schema is a JSON schema object.
|
||||
// It corresponds to the 2020-12 draft, as described in
|
||||
// https://json-schema.org/draft/2020-12.
|
||||
// It corresponds to the 2020-12 draft, as described in https://json-schema.org/draft/2020-12,
|
||||
// specifically:
|
||||
// - https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-01
|
||||
// - https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-01
|
||||
//
|
||||
// A Schema value may have non-zero values for more than one field:
|
||||
// all relevant non-zero fields are used for validation.
|
||||
// There is one exception to provide more Go type-safety: the Type and Types fields
|
||||
// are mutually exclusive.
|
||||
//
|
||||
// Since this struct is a Go representation of a JSON value, it inherits JSON's
|
||||
// distinction between nil and empty. Nil slices and maps are considered absent,
|
||||
// but empty ones are present and affect validation. For example,
|
||||
//
|
||||
// Schema{Enum: nil}
|
||||
//
|
||||
// is equivalent to an empty schema, so it validates every instance. But
|
||||
//
|
||||
// Schema{Enum: []any{}}
|
||||
//
|
||||
// requires equality to some slice element, so it vacuously rejects every instance.
|
||||
type Schema struct {
|
||||
// core
|
||||
ID string `json:"$id,omitempty"`
|
||||
|
|
@ -23,7 +43,7 @@ type Schema struct {
|
|||
Ref string `json:"$ref,omitempty"`
|
||||
Comment string `json:"$comment,omitempty"`
|
||||
Defs map[string]*Schema `json:"$defs,omitempty"`
|
||||
// definitions is deprecated but still allowed. It is a synonym for defs.
|
||||
// definitions is deprecated but still allowed. It is a synonym for $defs.
|
||||
Definitions map[string]*Schema `json:"definitions,omitempty"`
|
||||
|
||||
Anchor string `json:"$anchor,omitempty"`
|
||||
|
|
@ -192,3 +212,52 @@ func (s *Schema) UnmarshalJSON(data []byte) error {
|
|||
|
||||
// Ptr returns a pointer to a new variable whose value is x.
|
||||
func Ptr[T any](x T) *T { return &x }
|
||||
|
||||
// every applies f preorder to every schema under s including s.
|
||||
// It stops when f returns false.
|
||||
func (s *Schema) every(f func(*Schema) bool) bool {
|
||||
return s == nil ||
|
||||
f(s) && s.everyChild(f)
|
||||
}
|
||||
|
||||
// everyChild returns an iterator over the immediate child schemas of s.
|
||||
//
|
||||
// It does not yield nils from fields holding individual schemas, like Contains,
|
||||
// because a nil value indicates that the field is absent.
|
||||
// It does yield nils when they occur in slices and maps, so those invalid values
|
||||
// can be detected when the schema is validated.
|
||||
func (s *Schema) everyChild(f func(*Schema) bool) bool {
|
||||
// Fields that contain individual schemas. A nil is valid: it just means the field isn't present.
|
||||
for _, c := range []*Schema{
|
||||
s.Items, s.AdditionalItems, s.Contains, s.PropertyNames, s.AdditionalProperties,
|
||||
s.If, s.Then, s.Else, s.Not, s.UnevaluatedItems, s.UnevaluatedProperties,
|
||||
} {
|
||||
if c != nil && !f(c) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
// Fields that contain slices of schemas. Yield nils so we can check for their presence.
|
||||
for _, sl := range [][]*Schema{s.PrefixItems, s.AllOf, s.AnyOf, s.OneOf} {
|
||||
for _, c := range sl {
|
||||
if !f(c) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
// Fields that are maps of schemas. Ditto about nils.
|
||||
for _, m := range []map[string]*Schema{
|
||||
s.Defs, s.Definitions, s.Properties, s.PatternProperties, s.DependentSchemas,
|
||||
} {
|
||||
for _, c := range m {
|
||||
if !f(c) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// all wraps every in an iterator.
|
||||
func (s *Schema) all() iter.Seq[*Schema] {
|
||||
return func(yield func(*Schema) bool) { s.every(yield) }
|
||||
}
|
||||
|
|
|
|||
312
internal/jsonschema/testdata/draft2020-12/allOf.json
vendored
Normal file
312
internal/jsonschema/testdata/draft2020-12/allOf.json
vendored
Normal file
|
|
@ -0,0 +1,312 @@
|
|||
[
|
||||
{
|
||||
"description": "allOf",
|
||||
"schema": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"allOf": [
|
||||
{
|
||||
"properties": {
|
||||
"bar": {"type": "integer"}
|
||||
},
|
||||
"required": ["bar"]
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"foo": {"type": "string"}
|
||||
},
|
||||
"required": ["foo"]
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "allOf",
|
||||
"data": {"foo": "baz", "bar": 2},
|
||||
"valid": true
|
||||
},
|
||||
{
|
||||
"description": "mismatch second",
|
||||
"data": {"foo": "baz"},
|
||||
"valid": false
|
||||
},
|
||||
{
|
||||
"description": "mismatch first",
|
||||
"data": {"bar": 2},
|
||||
"valid": false
|
||||
},
|
||||
{
|
||||
"description": "wrong type",
|
||||
"data": {"foo": "baz", "bar": "quux"},
|
||||
"valid": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "allOf with base schema",
|
||||
"schema": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"properties": {"bar": {"type": "integer"}},
|
||||
"required": ["bar"],
|
||||
"allOf" : [
|
||||
{
|
||||
"properties": {
|
||||
"foo": {"type": "string"}
|
||||
},
|
||||
"required": ["foo"]
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"baz": {"type": "null"}
|
||||
},
|
||||
"required": ["baz"]
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "valid",
|
||||
"data": {"foo": "quux", "bar": 2, "baz": null},
|
||||
"valid": true
|
||||
},
|
||||
{
|
||||
"description": "mismatch base schema",
|
||||
"data": {"foo": "quux", "baz": null},
|
||||
"valid": false
|
||||
},
|
||||
{
|
||||
"description": "mismatch first allOf",
|
||||
"data": {"bar": 2, "baz": null},
|
||||
"valid": false
|
||||
},
|
||||
{
|
||||
"description": "mismatch second allOf",
|
||||
"data": {"foo": "quux", "bar": 2},
|
||||
"valid": false
|
||||
},
|
||||
{
|
||||
"description": "mismatch both",
|
||||
"data": {"bar": 2},
|
||||
"valid": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "allOf simple types",
|
||||
"schema": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"allOf": [
|
||||
{"maximum": 30},
|
||||
{"minimum": 20}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "valid",
|
||||
"data": 25,
|
||||
"valid": true
|
||||
},
|
||||
{
|
||||
"description": "mismatch one",
|
||||
"data": 35,
|
||||
"valid": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "allOf with boolean schemas, all true",
|
||||
"schema": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"allOf": [true, true]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "any value is valid",
|
||||
"data": "foo",
|
||||
"valid": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "allOf with boolean schemas, some false",
|
||||
"schema": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"allOf": [true, false]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "any value is invalid",
|
||||
"data": "foo",
|
||||
"valid": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "allOf with boolean schemas, all false",
|
||||
"schema": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"allOf": [false, false]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "any value is invalid",
|
||||
"data": "foo",
|
||||
"valid": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "allOf with one empty schema",
|
||||
"schema": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"allOf": [
|
||||
{}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "any data is valid",
|
||||
"data": 1,
|
||||
"valid": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "allOf with two empty schemas",
|
||||
"schema": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"allOf": [
|
||||
{},
|
||||
{}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "any data is valid",
|
||||
"data": 1,
|
||||
"valid": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "allOf with the first empty schema",
|
||||
"schema": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"allOf": [
|
||||
{},
|
||||
{ "type": "number" }
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "number is valid",
|
||||
"data": 1,
|
||||
"valid": true
|
||||
},
|
||||
{
|
||||
"description": "string is invalid",
|
||||
"data": "foo",
|
||||
"valid": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "allOf with the last empty schema",
|
||||
"schema": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"allOf": [
|
||||
{ "type": "number" },
|
||||
{}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "number is valid",
|
||||
"data": 1,
|
||||
"valid": true
|
||||
},
|
||||
{
|
||||
"description": "string is invalid",
|
||||
"data": "foo",
|
||||
"valid": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "nested allOf, to check validation semantics",
|
||||
"schema": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"allOf": [
|
||||
{
|
||||
"allOf": [
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "null is valid",
|
||||
"data": null,
|
||||
"valid": true
|
||||
},
|
||||
{
|
||||
"description": "anything non-null is invalid",
|
||||
"data": 123,
|
||||
"valid": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "allOf combined with anyOf, oneOf",
|
||||
"schema": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"allOf": [ { "multipleOf": 2 } ],
|
||||
"anyOf": [ { "multipleOf": 3 } ],
|
||||
"oneOf": [ { "multipleOf": 5 } ]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "allOf: false, anyOf: false, oneOf: false",
|
||||
"data": 1,
|
||||
"valid": false
|
||||
},
|
||||
{
|
||||
"description": "allOf: false, anyOf: false, oneOf: true",
|
||||
"data": 5,
|
||||
"valid": false
|
||||
},
|
||||
{
|
||||
"description": "allOf: false, anyOf: true, oneOf: false",
|
||||
"data": 3,
|
||||
"valid": false
|
||||
},
|
||||
{
|
||||
"description": "allOf: false, anyOf: true, oneOf: true",
|
||||
"data": 15,
|
||||
"valid": false
|
||||
},
|
||||
{
|
||||
"description": "allOf: true, anyOf: false, oneOf: false",
|
||||
"data": 2,
|
||||
"valid": false
|
||||
},
|
||||
{
|
||||
"description": "allOf: true, anyOf: false, oneOf: true",
|
||||
"data": 10,
|
||||
"valid": false
|
||||
},
|
||||
{
|
||||
"description": "allOf: true, anyOf: true, oneOf: false",
|
||||
"data": 6,
|
||||
"valid": false
|
||||
},
|
||||
{
|
||||
"description": "allOf: true, anyOf: true, oneOf: true",
|
||||
"data": 30,
|
||||
"valid": true
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
203
internal/jsonschema/testdata/draft2020-12/anyOf.json
vendored
Normal file
203
internal/jsonschema/testdata/draft2020-12/anyOf.json
vendored
Normal file
|
|
@ -0,0 +1,203 @@
|
|||
[
|
||||
{
|
||||
"description": "anyOf",
|
||||
"schema": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "integer"
|
||||
},
|
||||
{
|
||||
"minimum": 2
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "first anyOf valid",
|
||||
"data": 1,
|
||||
"valid": true
|
||||
},
|
||||
{
|
||||
"description": "second anyOf valid",
|
||||
"data": 2.5,
|
||||
"valid": true
|
||||
},
|
||||
{
|
||||
"description": "both anyOf valid",
|
||||
"data": 3,
|
||||
"valid": true
|
||||
},
|
||||
{
|
||||
"description": "neither anyOf valid",
|
||||
"data": 1.5,
|
||||
"valid": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "anyOf with base schema",
|
||||
"schema": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"type": "string",
|
||||
"anyOf" : [
|
||||
{
|
||||
"maxLength": 2
|
||||
},
|
||||
{
|
||||
"minLength": 4
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "mismatch base schema",
|
||||
"data": 3,
|
||||
"valid": false
|
||||
},
|
||||
{
|
||||
"description": "one anyOf valid",
|
||||
"data": "foobar",
|
||||
"valid": true
|
||||
},
|
||||
{
|
||||
"description": "both anyOf invalid",
|
||||
"data": "foo",
|
||||
"valid": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "anyOf with boolean schemas, all true",
|
||||
"schema": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"anyOf": [true, true]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "any value is valid",
|
||||
"data": "foo",
|
||||
"valid": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "anyOf with boolean schemas, some true",
|
||||
"schema": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"anyOf": [true, false]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "any value is valid",
|
||||
"data": "foo",
|
||||
"valid": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "anyOf with boolean schemas, all false",
|
||||
"schema": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"anyOf": [false, false]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "any value is invalid",
|
||||
"data": "foo",
|
||||
"valid": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "anyOf complex types",
|
||||
"schema": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"anyOf": [
|
||||
{
|
||||
"properties": {
|
||||
"bar": {"type": "integer"}
|
||||
},
|
||||
"required": ["bar"]
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"foo": {"type": "string"}
|
||||
},
|
||||
"required": ["foo"]
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "first anyOf valid (complex)",
|
||||
"data": {"bar": 2},
|
||||
"valid": true
|
||||
},
|
||||
{
|
||||
"description": "second anyOf valid (complex)",
|
||||
"data": {"foo": "baz"},
|
||||
"valid": true
|
||||
},
|
||||
{
|
||||
"description": "both anyOf valid (complex)",
|
||||
"data": {"foo": "baz", "bar": 2},
|
||||
"valid": true
|
||||
},
|
||||
{
|
||||
"description": "neither anyOf valid (complex)",
|
||||
"data": {"foo": 2, "bar": "quux"},
|
||||
"valid": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "anyOf with one empty schema",
|
||||
"schema": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"anyOf": [
|
||||
{ "type": "number" },
|
||||
{}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "string is valid",
|
||||
"data": "foo",
|
||||
"valid": true
|
||||
},
|
||||
{
|
||||
"description": "number is valid",
|
||||
"data": 123,
|
||||
"valid": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "nested anyOf, to check validation semantics",
|
||||
"schema": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"anyOf": [
|
||||
{
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "null is valid",
|
||||
"data": null,
|
||||
"valid": true
|
||||
},
|
||||
{
|
||||
"description": "anything non-null is invalid",
|
||||
"data": 123,
|
||||
"valid": false
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
268
internal/jsonschema/testdata/draft2020-12/if-then-else.json
vendored
Normal file
268
internal/jsonschema/testdata/draft2020-12/if-then-else.json
vendored
Normal file
|
|
@ -0,0 +1,268 @@
|
|||
[
|
||||
{
|
||||
"description": "ignore if without then or else",
|
||||
"schema": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"if": {
|
||||
"const": 0
|
||||
}
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "valid when valid against lone if",
|
||||
"data": 0,
|
||||
"valid": true
|
||||
},
|
||||
{
|
||||
"description": "valid when invalid against lone if",
|
||||
"data": "hello",
|
||||
"valid": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "ignore then without if",
|
||||
"schema": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"then": {
|
||||
"const": 0
|
||||
}
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "valid when valid against lone then",
|
||||
"data": 0,
|
||||
"valid": true
|
||||
},
|
||||
{
|
||||
"description": "valid when invalid against lone then",
|
||||
"data": "hello",
|
||||
"valid": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "ignore else without if",
|
||||
"schema": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"else": {
|
||||
"const": 0
|
||||
}
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "valid when valid against lone else",
|
||||
"data": 0,
|
||||
"valid": true
|
||||
},
|
||||
{
|
||||
"description": "valid when invalid against lone else",
|
||||
"data": "hello",
|
||||
"valid": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "if and then without else",
|
||||
"schema": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"if": {
|
||||
"exclusiveMaximum": 0
|
||||
},
|
||||
"then": {
|
||||
"minimum": -10
|
||||
}
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "valid through then",
|
||||
"data": -1,
|
||||
"valid": true
|
||||
},
|
||||
{
|
||||
"description": "invalid through then",
|
||||
"data": -100,
|
||||
"valid": false
|
||||
},
|
||||
{
|
||||
"description": "valid when if test fails",
|
||||
"data": 3,
|
||||
"valid": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "if and else without then",
|
||||
"schema": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"if": {
|
||||
"exclusiveMaximum": 0
|
||||
},
|
||||
"else": {
|
||||
"multipleOf": 2
|
||||
}
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "valid when if test passes",
|
||||
"data": -1,
|
||||
"valid": true
|
||||
},
|
||||
{
|
||||
"description": "valid through else",
|
||||
"data": 4,
|
||||
"valid": true
|
||||
},
|
||||
{
|
||||
"description": "invalid through else",
|
||||
"data": 3,
|
||||
"valid": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "validate against correct branch, then vs else",
|
||||
"schema": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"if": {
|
||||
"exclusiveMaximum": 0
|
||||
},
|
||||
"then": {
|
||||
"minimum": -10
|
||||
},
|
||||
"else": {
|
||||
"multipleOf": 2
|
||||
}
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "valid through then",
|
||||
"data": -1,
|
||||
"valid": true
|
||||
},
|
||||
{
|
||||
"description": "invalid through then",
|
||||
"data": -100,
|
||||
"valid": false
|
||||
},
|
||||
{
|
||||
"description": "valid through else",
|
||||
"data": 4,
|
||||
"valid": true
|
||||
},
|
||||
{
|
||||
"description": "invalid through else",
|
||||
"data": 3,
|
||||
"valid": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "non-interference across combined schemas",
|
||||
"schema": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"allOf": [
|
||||
{
|
||||
"if": {
|
||||
"exclusiveMaximum": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"then": {
|
||||
"minimum": -10
|
||||
}
|
||||
},
|
||||
{
|
||||
"else": {
|
||||
"multipleOf": 2
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "valid, but would have been invalid through then",
|
||||
"data": -100,
|
||||
"valid": true
|
||||
},
|
||||
{
|
||||
"description": "valid, but would have been invalid through else",
|
||||
"data": 3,
|
||||
"valid": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "if with boolean schema true",
|
||||
"schema": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"if": true,
|
||||
"then": { "const": "then" },
|
||||
"else": { "const": "else" }
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "boolean schema true in if always chooses the then path (valid)",
|
||||
"data": "then",
|
||||
"valid": true
|
||||
},
|
||||
{
|
||||
"description": "boolean schema true in if always chooses the then path (invalid)",
|
||||
"data": "else",
|
||||
"valid": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "if with boolean schema false",
|
||||
"schema": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"if": false,
|
||||
"then": { "const": "then" },
|
||||
"else": { "const": "else" }
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "boolean schema false in if always chooses the else path (invalid)",
|
||||
"data": "then",
|
||||
"valid": false
|
||||
},
|
||||
{
|
||||
"description": "boolean schema false in if always chooses the else path (valid)",
|
||||
"data": "else",
|
||||
"valid": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "if appears at the end when serialized (keyword processing sequence)",
|
||||
"schema": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"then": { "const": "yes" },
|
||||
"else": { "const": "other" },
|
||||
"if": { "maxLength": 4 }
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "yes redirects to then and passes",
|
||||
"data": "yes",
|
||||
"valid": true
|
||||
},
|
||||
{
|
||||
"description": "other redirects to else and passes",
|
||||
"data": "other",
|
||||
"valid": true
|
||||
},
|
||||
{
|
||||
"description": "no redirects to then and fails",
|
||||
"data": "no",
|
||||
"valid": false
|
||||
},
|
||||
{
|
||||
"description": "invalid redirects to else and fails",
|
||||
"data": "invalid",
|
||||
"valid": false
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
301
internal/jsonschema/testdata/draft2020-12/not.json
vendored
Normal file
301
internal/jsonschema/testdata/draft2020-12/not.json
vendored
Normal file
|
|
@ -0,0 +1,301 @@
|
|||
[
|
||||
{
|
||||
"description": "not",
|
||||
"schema": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"not": {"type": "integer"}
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "allowed",
|
||||
"data": "foo",
|
||||
"valid": true
|
||||
},
|
||||
{
|
||||
"description": "disallowed",
|
||||
"data": 1,
|
||||
"valid": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "not multiple types",
|
||||
"schema": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"not": {"type": ["integer", "boolean"]}
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "valid",
|
||||
"data": "foo",
|
||||
"valid": true
|
||||
},
|
||||
{
|
||||
"description": "mismatch",
|
||||
"data": 1,
|
||||
"valid": false
|
||||
},
|
||||
{
|
||||
"description": "other mismatch",
|
||||
"data": true,
|
||||
"valid": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "not more complex schema",
|
||||
"schema": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"not": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"foo": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "match",
|
||||
"data": 1,
|
||||
"valid": true
|
||||
},
|
||||
{
|
||||
"description": "other match",
|
||||
"data": {"foo": 1},
|
||||
"valid": true
|
||||
},
|
||||
{
|
||||
"description": "mismatch",
|
||||
"data": {"foo": "bar"},
|
||||
"valid": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "forbidden property",
|
||||
"schema": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"properties": {
|
||||
"foo": {
|
||||
"not": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "property present",
|
||||
"data": {"foo": 1, "bar": 2},
|
||||
"valid": false
|
||||
},
|
||||
{
|
||||
"description": "property absent",
|
||||
"data": {"bar": 1, "baz": 2},
|
||||
"valid": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "forbid everything with empty schema",
|
||||
"schema": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"not": {}
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "number is invalid",
|
||||
"data": 1,
|
||||
"valid": false
|
||||
},
|
||||
{
|
||||
"description": "string is invalid",
|
||||
"data": "foo",
|
||||
"valid": false
|
||||
},
|
||||
{
|
||||
"description": "boolean true is invalid",
|
||||
"data": true,
|
||||
"valid": false
|
||||
},
|
||||
{
|
||||
"description": "boolean false is invalid",
|
||||
"data": false,
|
||||
"valid": false
|
||||
},
|
||||
{
|
||||
"description": "null is invalid",
|
||||
"data": null,
|
||||
"valid": false
|
||||
},
|
||||
{
|
||||
"description": "object is invalid",
|
||||
"data": {"foo": "bar"},
|
||||
"valid": false
|
||||
},
|
||||
{
|
||||
"description": "empty object is invalid",
|
||||
"data": {},
|
||||
"valid": false
|
||||
},
|
||||
{
|
||||
"description": "array is invalid",
|
||||
"data": ["foo"],
|
||||
"valid": false
|
||||
},
|
||||
{
|
||||
"description": "empty array is invalid",
|
||||
"data": [],
|
||||
"valid": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "forbid everything with boolean schema true",
|
||||
"schema": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"not": true
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "number is invalid",
|
||||
"data": 1,
|
||||
"valid": false
|
||||
},
|
||||
{
|
||||
"description": "string is invalid",
|
||||
"data": "foo",
|
||||
"valid": false
|
||||
},
|
||||
{
|
||||
"description": "boolean true is invalid",
|
||||
"data": true,
|
||||
"valid": false
|
||||
},
|
||||
{
|
||||
"description": "boolean false is invalid",
|
||||
"data": false,
|
||||
"valid": false
|
||||
},
|
||||
{
|
||||
"description": "null is invalid",
|
||||
"data": null,
|
||||
"valid": false
|
||||
},
|
||||
{
|
||||
"description": "object is invalid",
|
||||
"data": {"foo": "bar"},
|
||||
"valid": false
|
||||
},
|
||||
{
|
||||
"description": "empty object is invalid",
|
||||
"data": {},
|
||||
"valid": false
|
||||
},
|
||||
{
|
||||
"description": "array is invalid",
|
||||
"data": ["foo"],
|
||||
"valid": false
|
||||
},
|
||||
{
|
||||
"description": "empty array is invalid",
|
||||
"data": [],
|
||||
"valid": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "allow everything with boolean schema false",
|
||||
"schema": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"not": false
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "number is valid",
|
||||
"data": 1,
|
||||
"valid": true
|
||||
},
|
||||
{
|
||||
"description": "string is valid",
|
||||
"data": "foo",
|
||||
"valid": true
|
||||
},
|
||||
{
|
||||
"description": "boolean true is valid",
|
||||
"data": true,
|
||||
"valid": true
|
||||
},
|
||||
{
|
||||
"description": "boolean false is valid",
|
||||
"data": false,
|
||||
"valid": true
|
||||
},
|
||||
{
|
||||
"description": "null is valid",
|
||||
"data": null,
|
||||
"valid": true
|
||||
},
|
||||
{
|
||||
"description": "object is valid",
|
||||
"data": {"foo": "bar"},
|
||||
"valid": true
|
||||
},
|
||||
{
|
||||
"description": "empty object is valid",
|
||||
"data": {},
|
||||
"valid": true
|
||||
},
|
||||
{
|
||||
"description": "array is valid",
|
||||
"data": ["foo"],
|
||||
"valid": true
|
||||
},
|
||||
{
|
||||
"description": "empty array is valid",
|
||||
"data": [],
|
||||
"valid": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "double negation",
|
||||
"schema": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"not": { "not": {} }
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "any value is valid",
|
||||
"data": "foo",
|
||||
"valid": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "collect annotations inside a 'not', even if collection is disabled",
|
||||
"schema": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"not": {
|
||||
"$comment": "this subschema must still produce annotations internally, even though the 'not' will ultimately discard them",
|
||||
"anyOf": [
|
||||
true,
|
||||
{ "properties": { "foo": true } }
|
||||
],
|
||||
"unevaluatedProperties": false
|
||||
}
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "unevaluated property",
|
||||
"data": { "bar": 1 },
|
||||
"valid": true
|
||||
},
|
||||
{
|
||||
"description": "annotations are still collected inside a 'not'",
|
||||
"data": { "foo": 1 },
|
||||
"valid": false
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
293
internal/jsonschema/testdata/draft2020-12/oneOf.json
vendored
Normal file
293
internal/jsonschema/testdata/draft2020-12/oneOf.json
vendored
Normal file
|
|
@ -0,0 +1,293 @@
|
|||
[
|
||||
{
|
||||
"description": "oneOf",
|
||||
"schema": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "integer"
|
||||
},
|
||||
{
|
||||
"minimum": 2
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "first oneOf valid",
|
||||
"data": 1,
|
||||
"valid": true
|
||||
},
|
||||
{
|
||||
"description": "second oneOf valid",
|
||||
"data": 2.5,
|
||||
"valid": true
|
||||
},
|
||||
{
|
||||
"description": "both oneOf valid",
|
||||
"data": 3,
|
||||
"valid": false
|
||||
},
|
||||
{
|
||||
"description": "neither oneOf valid",
|
||||
"data": 1.5,
|
||||
"valid": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "oneOf with base schema",
|
||||
"schema": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"type": "string",
|
||||
"oneOf" : [
|
||||
{
|
||||
"minLength": 2
|
||||
},
|
||||
{
|
||||
"maxLength": 4
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "mismatch base schema",
|
||||
"data": 3,
|
||||
"valid": false
|
||||
},
|
||||
{
|
||||
"description": "one oneOf valid",
|
||||
"data": "foobar",
|
||||
"valid": true
|
||||
},
|
||||
{
|
||||
"description": "both oneOf valid",
|
||||
"data": "foo",
|
||||
"valid": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "oneOf with boolean schemas, all true",
|
||||
"schema": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"oneOf": [true, true, true]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "any value is invalid",
|
||||
"data": "foo",
|
||||
"valid": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "oneOf with boolean schemas, one true",
|
||||
"schema": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"oneOf": [true, false, false]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "any value is valid",
|
||||
"data": "foo",
|
||||
"valid": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "oneOf with boolean schemas, more than one true",
|
||||
"schema": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"oneOf": [true, true, false]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "any value is invalid",
|
||||
"data": "foo",
|
||||
"valid": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "oneOf with boolean schemas, all false",
|
||||
"schema": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"oneOf": [false, false, false]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "any value is invalid",
|
||||
"data": "foo",
|
||||
"valid": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "oneOf complex types",
|
||||
"schema": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": {
|
||||
"bar": {"type": "integer"}
|
||||
},
|
||||
"required": ["bar"]
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"foo": {"type": "string"}
|
||||
},
|
||||
"required": ["foo"]
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "first oneOf valid (complex)",
|
||||
"data": {"bar": 2},
|
||||
"valid": true
|
||||
},
|
||||
{
|
||||
"description": "second oneOf valid (complex)",
|
||||
"data": {"foo": "baz"},
|
||||
"valid": true
|
||||
},
|
||||
{
|
||||
"description": "both oneOf valid (complex)",
|
||||
"data": {"foo": "baz", "bar": 2},
|
||||
"valid": false
|
||||
},
|
||||
{
|
||||
"description": "neither oneOf valid (complex)",
|
||||
"data": {"foo": 2, "bar": "quux"},
|
||||
"valid": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "oneOf with empty schema",
|
||||
"schema": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"oneOf": [
|
||||
{ "type": "number" },
|
||||
{}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "one valid - valid",
|
||||
"data": "foo",
|
||||
"valid": true
|
||||
},
|
||||
{
|
||||
"description": "both valid - invalid",
|
||||
"data": 123,
|
||||
"valid": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "oneOf with required",
|
||||
"schema": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"type": "object",
|
||||
"oneOf": [
|
||||
{ "required": ["foo", "bar"] },
|
||||
{ "required": ["foo", "baz"] }
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "both invalid - invalid",
|
||||
"data": {"bar": 2},
|
||||
"valid": false
|
||||
},
|
||||
{
|
||||
"description": "first valid - valid",
|
||||
"data": {"foo": 1, "bar": 2},
|
||||
"valid": true
|
||||
},
|
||||
{
|
||||
"description": "second valid - valid",
|
||||
"data": {"foo": 1, "baz": 3},
|
||||
"valid": true
|
||||
},
|
||||
{
|
||||
"description": "both valid - invalid",
|
||||
"data": {"foo": 1, "bar": 2, "baz" : 3},
|
||||
"valid": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "oneOf with missing optional property",
|
||||
"schema": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": {
|
||||
"bar": true,
|
||||
"baz": true
|
||||
},
|
||||
"required": ["bar"]
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"foo": true
|
||||
},
|
||||
"required": ["foo"]
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "first oneOf valid",
|
||||
"data": {"bar": 8},
|
||||
"valid": true
|
||||
},
|
||||
{
|
||||
"description": "second oneOf valid",
|
||||
"data": {"foo": "foo"},
|
||||
"valid": true
|
||||
},
|
||||
{
|
||||
"description": "both oneOf valid",
|
||||
"data": {"foo": "foo", "bar": 8},
|
||||
"valid": false
|
||||
},
|
||||
{
|
||||
"description": "neither oneOf valid",
|
||||
"data": {"baz": "quux"},
|
||||
"valid": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "nested oneOf, to check validation semantics",
|
||||
"schema": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"oneOf": [
|
||||
{
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "null is valid",
|
||||
"data": null,
|
||||
"valid": true
|
||||
},
|
||||
{
|
||||
"description": "anything non-null is invalid",
|
||||
"data": 123,
|
||||
"valid": false
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
|
@ -166,6 +166,70 @@ func (st *state) validate(instance reflect.Value, schema *Schema, path []any) (e
|
|||
}
|
||||
}
|
||||
|
||||
// logic
|
||||
// https://json-schema.org/draft/2020-12/json-schema-core#section-10.2
|
||||
// These must happen before arrays and objects because if they evaluate an item or property,
|
||||
// then the unevaluatedItems/Properties schemas don't apply to it.
|
||||
// See https://json-schema.org/draft/2020-12/json-schema-core#section-11.2, paragraph 4.
|
||||
//
|
||||
// If any of these fail, then validation fails, even if there is an unevaluatedXXX
|
||||
// keyword in the schema. The spec is unclear about this, but that is the intention.
|
||||
|
||||
valid := func(s *Schema) bool { return st.validate(instance, s, path) == nil }
|
||||
|
||||
if schema.AllOf != nil {
|
||||
for _, ss := range schema.AllOf {
|
||||
if err := st.validate(instance, ss, path); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
if schema.AnyOf != nil {
|
||||
ok := false
|
||||
for _, ss := range schema.AnyOf {
|
||||
if valid(ss) {
|
||||
ok = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !ok {
|
||||
return fmt.Errorf("anyOf: did not validate against any of %v", schema.AnyOf)
|
||||
}
|
||||
}
|
||||
if schema.OneOf != nil {
|
||||
// Exactly one.
|
||||
var okSchema *Schema
|
||||
for _, ss := range schema.OneOf {
|
||||
if valid(ss) {
|
||||
if okSchema != nil {
|
||||
return fmt.Errorf("oneOf: validated against both %v and %v", okSchema, ss)
|
||||
}
|
||||
okSchema = ss
|
||||
}
|
||||
}
|
||||
if okSchema == nil {
|
||||
return fmt.Errorf("oneOf: did not validate against any of %v", schema.OneOf)
|
||||
}
|
||||
}
|
||||
if schema.Not != nil {
|
||||
if valid(schema.Not) {
|
||||
return fmt.Errorf("not: validated against %v", schema.Not)
|
||||
}
|
||||
}
|
||||
if schema.If != nil {
|
||||
var ss *Schema
|
||||
if valid(schema.If) {
|
||||
ss = schema.Then
|
||||
} else {
|
||||
ss = schema.Else
|
||||
}
|
||||
if ss != nil {
|
||||
if err := st.validate(instance, ss, path); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -50,8 +50,10 @@ func TestValidate(t *testing.T) {
|
|||
t.Fatal(err)
|
||||
}
|
||||
for _, g := range groups {
|
||||
if g.Schema.Properties != nil {
|
||||
t.Skip("schema has properties")
|
||||
for s := range g.Schema.all() {
|
||||
if s.Properties != nil {
|
||||
t.Skip("schema or subschema has properties")
|
||||
}
|
||||
}
|
||||
rs := &ResolvedSchema{root: g.Schema}
|
||||
t.Run(g.Description, func(t *testing.T) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue