jsonschema: validate object keywords

Add validation for JSON objects (Go maps).

The map keys are called "properties."

As with arrays, annotations are needed to support the
unevaluatedProperties keyword.

Change-Id: Ia01f74300f4bcdbce94065572d83c46dfaa53ae7
Reviewed-on: https://go-review.googlesource.com/c/tools/+/668256
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Alan Donovan <adonovan@google.com>
This commit is contained in:
Jonathan Amsterdam 2025-04-25 11:13:31 -04:00
commit 0def4e0327
15 changed files with 3310 additions and 3 deletions

View file

@ -14,6 +14,7 @@ type annotations struct {
allItems bool // all items were evaluated
endIndex int // 1+largest index evaluated by prefixItems
evaluatedIndexes map[int]bool // set of indexes evaluated by contains
allProperties bool // all properties were evaluated
evaluatedProperties map[string]bool // set of properties evaluated by various keywords
}
@ -58,6 +59,9 @@ func (a *annotations) merge(b *annotations) {
a.endIndex = b.endIndex
}
a.evaluatedIndexes = merge(a.evaluatedIndexes, b.evaluatedIndexes)
if b.allProperties {
a.allProperties = true
}
a.evaluatedProperties = merge(a.evaluatedProperties, b.evaluatedProperties)
}

View file

@ -0,0 +1,219 @@
[
{
"description":
"additionalProperties being false does not allow other properties",
"specification": [ { "core":"10.3.2.3", "quote": "The value of \"additionalProperties\" MUST be a valid JSON Schema. Boolean \"false\" forbids everything." } ],
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"properties": {"foo": {}, "bar": {}},
"patternProperties": { "^v": {} },
"additionalProperties": false
},
"tests": [
{
"description": "no additional properties is valid",
"data": {"foo": 1},
"valid": true
},
{
"description": "an additional property is invalid",
"data": {"foo" : 1, "bar" : 2, "quux" : "boom"},
"valid": false
},
{
"description": "ignores arrays",
"data": [1, 2, 3],
"valid": true
},
{
"description": "ignores strings",
"data": "foobarbaz",
"valid": true
},
{
"description": "ignores other non-objects",
"data": 12,
"valid": true
},
{
"description": "patternProperties are not additional properties",
"data": {"foo":1, "vroom": 2},
"valid": true
}
]
},
{
"description": "non-ASCII pattern with additionalProperties",
"specification": [ { "core":"10.3.2.3"} ],
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"patternProperties": {"^á": {}},
"additionalProperties": false
},
"tests": [
{
"description": "matching the pattern is valid",
"data": {"ármányos": 2},
"valid": true
},
{
"description": "not matching the pattern is invalid",
"data": {"élmény": 2},
"valid": false
}
]
},
{
"description": "additionalProperties with schema",
"specification": [ { "core":"10.3.2.3", "quote": "The value of \"additionalProperties\" MUST be a valid JSON Schema." } ],
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"properties": {"foo": {}, "bar": {}},
"additionalProperties": {"type": "boolean"}
},
"tests": [
{
"description": "no additional properties is valid",
"data": {"foo": 1},
"valid": true
},
{
"description": "an additional valid property is valid",
"data": {"foo" : 1, "bar" : 2, "quux" : true},
"valid": true
},
{
"description": "an additional invalid property is invalid",
"data": {"foo" : 1, "bar" : 2, "quux" : 12},
"valid": false
}
]
},
{
"description": "additionalProperties can exist by itself",
"specification": [ { "core":"10.3.2.3", "quote": "With no other applicator applying to object instances. This validates all the instance values irrespective of their property names" } ],
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"additionalProperties": {"type": "boolean"}
},
"tests": [
{
"description": "an additional valid property is valid",
"data": {"foo" : true},
"valid": true
},
{
"description": "an additional invalid property is invalid",
"data": {"foo" : 1},
"valid": false
}
]
},
{
"description": "additionalProperties are allowed by default",
"specification": [ { "core":"10.3.2.3", "quote": "Omitting this keyword has the same assertion behavior as an empty schema." } ],
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"properties": {"foo": {}, "bar": {}}
},
"tests": [
{
"description": "additional properties are allowed",
"data": {"foo": 1, "bar": 2, "quux": true},
"valid": true
}
]
},
{
"description": "additionalProperties does not look in applicators",
"specification":[ { "core": "10.2", "quote": "Subschemas of applicator keywords evaluate the instance completely independently such that the results of one such subschema MUST NOT impact the results of sibling subschemas." } ],
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"allOf": [
{"properties": {"foo": {}}}
],
"additionalProperties": {"type": "boolean"}
},
"tests": [
{
"description": "properties defined in allOf are not examined",
"data": {"foo": 1, "bar": true},
"valid": false
}
]
},
{
"description": "additionalProperties with null valued instance properties",
"specification": [ { "core":"10.3.2.3" } ],
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"additionalProperties": {
"type": "null"
}
},
"tests": [
{
"description": "allows null values",
"data": {"foo": null},
"valid": true
}
]
},
{
"description": "additionalProperties with propertyNames",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"propertyNames": {
"maxLength": 5
},
"additionalProperties": {
"type": "number"
}
},
"tests": [
{
"description": "Valid against both keywords",
"data": { "apple": 4 },
"valid": true
},
{
"description": "Valid against propertyNames, but not additionalProperties",
"data": { "fig": 2, "pear": "available" },
"valid": false
}
]
},
{
"description": "dependentSchemas with additionalProperties",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"properties": {"foo2": {}},
"dependentSchemas": {
"foo" : {},
"foo2": {
"properties": {
"bar": {}
}
}
},
"additionalProperties": false
},
"tests": [
{
"description": "additionalProperties doesn't consider dependentSchemas",
"data": {"foo": ""},
"valid": false
},
{
"description": "additionalProperties can't see bar",
"data": {"bar": ""},
"valid": false
},
{
"description": "additionalProperties can't see bar even when foo2 is present",
"data": {"foo2": "", "bar": ""},
"valid": false
}
]
}
]

View file

@ -0,0 +1,104 @@
[
{
"description": "boolean schema 'true'",
"schema": true,
"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": "boolean schema 'false'",
"schema": false,
"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
}
]
}
]

View file

@ -0,0 +1,152 @@
[
{
"description": "single dependency",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"dependentRequired": {"bar": ["foo"]}
},
"tests": [
{
"description": "neither",
"data": {},
"valid": true
},
{
"description": "nondependant",
"data": {"foo": 1},
"valid": true
},
{
"description": "with dependency",
"data": {"foo": 1, "bar": 2},
"valid": true
},
{
"description": "missing dependency",
"data": {"bar": 2},
"valid": false
},
{
"description": "ignores arrays",
"data": ["bar"],
"valid": true
},
{
"description": "ignores strings",
"data": "foobar",
"valid": true
},
{
"description": "ignores other non-objects",
"data": 12,
"valid": true
}
]
},
{
"description": "empty dependents",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"dependentRequired": {"bar": []}
},
"tests": [
{
"description": "empty object",
"data": {},
"valid": true
},
{
"description": "object with one property",
"data": {"bar": 2},
"valid": true
},
{
"description": "non-object is valid",
"data": 1,
"valid": true
}
]
},
{
"description": "multiple dependents required",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"dependentRequired": {"quux": ["foo", "bar"]}
},
"tests": [
{
"description": "neither",
"data": {},
"valid": true
},
{
"description": "nondependants",
"data": {"foo": 1, "bar": 2},
"valid": true
},
{
"description": "with dependencies",
"data": {"foo": 1, "bar": 2, "quux": 3},
"valid": true
},
{
"description": "missing dependency",
"data": {"foo": 1, "quux": 2},
"valid": false
},
{
"description": "missing other dependency",
"data": {"bar": 1, "quux": 2},
"valid": false
},
{
"description": "missing both dependencies",
"data": {"quux": 1},
"valid": false
}
]
},
{
"description": "dependencies with escaped characters",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"dependentRequired": {
"foo\nbar": ["foo\rbar"],
"foo\"bar": ["foo'bar"]
}
},
"tests": [
{
"description": "CRLF",
"data": {
"foo\nbar": 1,
"foo\rbar": 2
},
"valid": true
},
{
"description": "quoted quotes",
"data": {
"foo'bar": 1,
"foo\"bar": 2
},
"valid": true
},
{
"description": "CRLF missing dependent",
"data": {
"foo\nbar": 1,
"foo": 2
},
"valid": false
},
{
"description": "quoted quotes missing dependent",
"data": {
"foo\"bar": 2
},
"valid": false
}
]
}
]

View file

@ -0,0 +1,171 @@
[
{
"description": "single dependency",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"dependentSchemas": {
"bar": {
"properties": {
"foo": {"type": "integer"},
"bar": {"type": "integer"}
}
}
}
},
"tests": [
{
"description": "valid",
"data": {"foo": 1, "bar": 2},
"valid": true
},
{
"description": "no dependency",
"data": {"foo": "quux"},
"valid": true
},
{
"description": "wrong type",
"data": {"foo": "quux", "bar": 2},
"valid": false
},
{
"description": "wrong type other",
"data": {"foo": 2, "bar": "quux"},
"valid": false
},
{
"description": "wrong type both",
"data": {"foo": "quux", "bar": "quux"},
"valid": false
},
{
"description": "ignores arrays",
"data": ["bar"],
"valid": true
},
{
"description": "ignores strings",
"data": "foobar",
"valid": true
},
{
"description": "ignores other non-objects",
"data": 12,
"valid": true
}
]
},
{
"description": "boolean subschemas",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"dependentSchemas": {
"foo": true,
"bar": false
}
},
"tests": [
{
"description": "object with property having schema true is valid",
"data": {"foo": 1},
"valid": true
},
{
"description": "object with property having schema false is invalid",
"data": {"bar": 2},
"valid": false
},
{
"description": "object with both properties is invalid",
"data": {"foo": 1, "bar": 2},
"valid": false
},
{
"description": "empty object is valid",
"data": {},
"valid": true
}
]
},
{
"description": "dependencies with escaped characters",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"dependentSchemas": {
"foo\tbar": {"minProperties": 4},
"foo'bar": {"required": ["foo\"bar"]}
}
},
"tests": [
{
"description": "quoted tab",
"data": {
"foo\tbar": 1,
"a": 2,
"b": 3,
"c": 4
},
"valid": true
},
{
"description": "quoted quote",
"data": {
"foo'bar": {"foo\"bar": 1}
},
"valid": false
},
{
"description": "quoted tab invalid under dependent schema",
"data": {
"foo\tbar": 1,
"a": 2
},
"valid": false
},
{
"description": "quoted quote invalid under dependent schema",
"data": {"foo'bar": 1},
"valid": false
}
]
},
{
"description": "dependent subschema incompatible with root",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"properties": {
"foo": {}
},
"dependentSchemas": {
"foo": {
"properties": {
"bar": {}
},
"additionalProperties": false
}
}
},
"tests": [
{
"description": "matches root",
"data": {"foo": 1},
"valid": false
},
{
"description": "matches dependency",
"data": {"bar": 1},
"valid": true
},
{
"description": "matches both",
"data": {"foo": 1, "bar": 2},
"valid": false
},
{
"description": "no dependency",
"data": {"baz": 1},
"valid": true
}
]
}
]

View file

@ -0,0 +1,37 @@
[
{
"description": "evaluating the same schema location against the same data location twice is not a sign of an infinite loop",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$defs": {
"int": { "type": "integer" }
},
"allOf": [
{
"properties": {
"foo": {
"$ref": "#/$defs/int"
}
}
},
{
"additionalProperties": {
"$ref": "#/$defs/int"
}
}
]
},
"tests": [
{
"description": "passing case",
"data": { "foo": 1 },
"valid": true
},
{
"description": "failing case",
"data": { "foo": "a string" },
"valid": false
}
]
}
]

View file

@ -0,0 +1,79 @@
[
{
"description": "maxProperties validation",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"maxProperties": 2
},
"tests": [
{
"description": "shorter is valid",
"data": {"foo": 1},
"valid": true
},
{
"description": "exact length is valid",
"data": {"foo": 1, "bar": 2},
"valid": true
},
{
"description": "too long is invalid",
"data": {"foo": 1, "bar": 2, "baz": 3},
"valid": false
},
{
"description": "ignores arrays",
"data": [1, 2, 3],
"valid": true
},
{
"description": "ignores strings",
"data": "foobar",
"valid": true
},
{
"description": "ignores other non-objects",
"data": 12,
"valid": true
}
]
},
{
"description": "maxProperties validation with a decimal",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"maxProperties": 2.0
},
"tests": [
{
"description": "shorter is valid",
"data": {"foo": 1},
"valid": true
},
{
"description": "too long is invalid",
"data": {"foo": 1, "bar": 2, "baz": 3},
"valid": false
}
]
},
{
"description": "maxProperties = 0 means the object is empty",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"maxProperties": 0
},
"tests": [
{
"description": "no properties is valid",
"data": {},
"valid": true
},
{
"description": "one property is invalid",
"data": { "foo": 1 },
"valid": false
}
]
}
]

View file

@ -0,0 +1,60 @@
[
{
"description": "minProperties validation",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"minProperties": 1
},
"tests": [
{
"description": "longer is valid",
"data": {"foo": 1, "bar": 2},
"valid": true
},
{
"description": "exact length is valid",
"data": {"foo": 1},
"valid": true
},
{
"description": "too short is invalid",
"data": {},
"valid": false
},
{
"description": "ignores arrays",
"data": [],
"valid": true
},
{
"description": "ignores strings",
"data": "",
"valid": true
},
{
"description": "ignores other non-objects",
"data": 12,
"valid": true
}
]
},
{
"description": "minProperties validation with a decimal",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"minProperties": 1.0
},
"tests": [
{
"description": "longer is valid",
"data": {"foo": 1, "bar": 2},
"valid": true
},
{
"description": "too short is invalid",
"data": {},
"valid": false
}
]
}
]

View file

@ -0,0 +1,176 @@
[
{
"description":
"patternProperties validates properties matching a regex",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"patternProperties": {
"f.*o": {"type": "integer"}
}
},
"tests": [
{
"description": "a single valid match is valid",
"data": {"foo": 1},
"valid": true
},
{
"description": "multiple valid matches is valid",
"data": {"foo": 1, "foooooo" : 2},
"valid": true
},
{
"description": "a single invalid match is invalid",
"data": {"foo": "bar", "fooooo": 2},
"valid": false
},
{
"description": "multiple invalid matches is invalid",
"data": {"foo": "bar", "foooooo" : "baz"},
"valid": false
},
{
"description": "ignores arrays",
"data": ["foo"],
"valid": true
},
{
"description": "ignores strings",
"data": "foo",
"valid": true
},
{
"description": "ignores other non-objects",
"data": 12,
"valid": true
}
]
},
{
"description": "multiple simultaneous patternProperties are validated",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"patternProperties": {
"a*": {"type": "integer"},
"aaa*": {"maximum": 20}
}
},
"tests": [
{
"description": "a single valid match is valid",
"data": {"a": 21},
"valid": true
},
{
"description": "a simultaneous match is valid",
"data": {"aaaa": 18},
"valid": true
},
{
"description": "multiple matches is valid",
"data": {"a": 21, "aaaa": 18},
"valid": true
},
{
"description": "an invalid due to one is invalid",
"data": {"a": "bar"},
"valid": false
},
{
"description": "an invalid due to the other is invalid",
"data": {"aaaa": 31},
"valid": false
},
{
"description": "an invalid due to both is invalid",
"data": {"aaa": "foo", "aaaa": 31},
"valid": false
}
]
},
{
"description": "regexes are not anchored by default and are case sensitive",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"patternProperties": {
"[0-9]{2,}": { "type": "boolean" },
"X_": { "type": "string" }
}
},
"tests": [
{
"description": "non recognized members are ignored",
"data": { "answer 1": "42" },
"valid": true
},
{
"description": "recognized members are accounted for",
"data": { "a31b": null },
"valid": false
},
{
"description": "regexes are case sensitive",
"data": { "a_x_3": 3 },
"valid": true
},
{
"description": "regexes are case sensitive, 2",
"data": { "a_X_3": 3 },
"valid": false
}
]
},
{
"description": "patternProperties with boolean schemas",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"patternProperties": {
"f.*": true,
"b.*": false
}
},
"tests": [
{
"description": "object with property matching schema true is valid",
"data": {"foo": 1},
"valid": true
},
{
"description": "object with property matching schema false is invalid",
"data": {"bar": 2},
"valid": false
},
{
"description": "object with both properties is invalid",
"data": {"foo": 1, "bar": 2},
"valid": false
},
{
"description": "object with a property matching both true and false is invalid",
"data": {"foobar":1},
"valid": false
},
{
"description": "empty object is valid",
"data": {},
"valid": true
}
]
},
{
"description": "patternProperties with null valued instance properties",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"patternProperties": {
"^.*bar$": {"type": "null"}
}
},
"tests": [
{
"description": "allows null values",
"data": {"foobar": null},
"valid": true
}
]
}
]

View file

@ -0,0 +1,242 @@
[
{
"description": "object properties validation",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"properties": {
"foo": {"type": "integer"},
"bar": {"type": "string"}
}
},
"tests": [
{
"description": "both properties present and valid is valid",
"data": {"foo": 1, "bar": "baz"},
"valid": true
},
{
"description": "one property invalid is invalid",
"data": {"foo": 1, "bar": {}},
"valid": false
},
{
"description": "both properties invalid is invalid",
"data": {"foo": [], "bar": {}},
"valid": false
},
{
"description": "doesn't invalidate other properties",
"data": {"quux": []},
"valid": true
},
{
"description": "ignores arrays",
"data": [],
"valid": true
},
{
"description": "ignores other non-objects",
"data": 12,
"valid": true
}
]
},
{
"description":
"properties, patternProperties, additionalProperties interaction",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"properties": {
"foo": {"type": "array", "maxItems": 3},
"bar": {"type": "array"}
},
"patternProperties": {"f.o": {"minItems": 2}},
"additionalProperties": {"type": "integer"}
},
"tests": [
{
"description": "property validates property",
"data": {"foo": [1, 2]},
"valid": true
},
{
"description": "property invalidates property",
"data": {"foo": [1, 2, 3, 4]},
"valid": false
},
{
"description": "patternProperty invalidates property",
"data": {"foo": []},
"valid": false
},
{
"description": "patternProperty validates nonproperty",
"data": {"fxo": [1, 2]},
"valid": true
},
{
"description": "patternProperty invalidates nonproperty",
"data": {"fxo": []},
"valid": false
},
{
"description": "additionalProperty ignores property",
"data": {"bar": []},
"valid": true
},
{
"description": "additionalProperty validates others",
"data": {"quux": 3},
"valid": true
},
{
"description": "additionalProperty invalidates others",
"data": {"quux": "foo"},
"valid": false
}
]
},
{
"description": "properties with boolean schema",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"properties": {
"foo": true,
"bar": false
}
},
"tests": [
{
"description": "no property present is valid",
"data": {},
"valid": true
},
{
"description": "only 'true' property present is valid",
"data": {"foo": 1},
"valid": true
},
{
"description": "only 'false' property present is invalid",
"data": {"bar": 2},
"valid": false
},
{
"description": "both properties present is invalid",
"data": {"foo": 1, "bar": 2},
"valid": false
}
]
},
{
"description": "properties with escaped characters",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"properties": {
"foo\nbar": {"type": "number"},
"foo\"bar": {"type": "number"},
"foo\\bar": {"type": "number"},
"foo\rbar": {"type": "number"},
"foo\tbar": {"type": "number"},
"foo\fbar": {"type": "number"}
}
},
"tests": [
{
"description": "object with all numbers is valid",
"data": {
"foo\nbar": 1,
"foo\"bar": 1,
"foo\\bar": 1,
"foo\rbar": 1,
"foo\tbar": 1,
"foo\fbar": 1
},
"valid": true
},
{
"description": "object with strings is invalid",
"data": {
"foo\nbar": "1",
"foo\"bar": "1",
"foo\\bar": "1",
"foo\rbar": "1",
"foo\tbar": "1",
"foo\fbar": "1"
},
"valid": false
}
]
},
{
"description": "properties with null valued instance properties",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"properties": {
"foo": {"type": "null"}
}
},
"tests": [
{
"description": "allows null values",
"data": {"foo": null},
"valid": true
}
]
},
{
"description": "properties whose names are Javascript object property names",
"comment": "Ensure JS implementations don't universally consider e.g. __proto__ to always be present in an object.",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"properties": {
"__proto__": {"type": "number"},
"toString": {
"properties": { "length": { "type": "string" } }
},
"constructor": {"type": "number"}
}
},
"tests": [
{
"description": "ignores arrays",
"data": [],
"valid": true
},
{
"description": "ignores other non-objects",
"data": 12,
"valid": true
},
{
"description": "none of the properties mentioned",
"data": {},
"valid": true
},
{
"description": "__proto__ not valid",
"data": { "__proto__": "foo" },
"valid": false
},
{
"description": "toString not valid",
"data": { "toString": { "length": 37 } },
"valid": false
},
{
"description": "constructor not valid",
"data": { "constructor": { "length": 37 } },
"valid": false
},
{
"description": "all present and valid",
"data": {
"__proto__": 12,
"toString": { "length": "foo" },
"constructor": 37
},
"valid": true
}
]
}
]

View file

@ -0,0 +1,168 @@
[
{
"description": "propertyNames validation",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"propertyNames": {"maxLength": 3}
},
"tests": [
{
"description": "all property names valid",
"data": {
"f": {},
"foo": {}
},
"valid": true
},
{
"description": "some property names invalid",
"data": {
"foo": {},
"foobar": {}
},
"valid": false
},
{
"description": "object without properties is valid",
"data": {},
"valid": true
},
{
"description": "ignores arrays",
"data": [1, 2, 3, 4],
"valid": true
},
{
"description": "ignores strings",
"data": "foobar",
"valid": true
},
{
"description": "ignores other non-objects",
"data": 12,
"valid": true
}
]
},
{
"description": "propertyNames validation with pattern",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"propertyNames": { "pattern": "^a+$" }
},
"tests": [
{
"description": "matching property names valid",
"data": {
"a": {},
"aa": {},
"aaa": {}
},
"valid": true
},
{
"description": "non-matching property name is invalid",
"data": {
"aaA": {}
},
"valid": false
},
{
"description": "object without properties is valid",
"data": {},
"valid": true
}
]
},
{
"description": "propertyNames with boolean schema true",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"propertyNames": true
},
"tests": [
{
"description": "object with any properties is valid",
"data": {"foo": 1},
"valid": true
},
{
"description": "empty object is valid",
"data": {},
"valid": true
}
]
},
{
"description": "propertyNames with boolean schema false",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"propertyNames": false
},
"tests": [
{
"description": "object with any properties is invalid",
"data": {"foo": 1},
"valid": false
},
{
"description": "empty object is valid",
"data": {},
"valid": true
}
]
},
{
"description": "propertyNames with const",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"propertyNames": {"const": "foo"}
},
"tests": [
{
"description": "object with property foo is valid",
"data": {"foo": 1},
"valid": true
},
{
"description": "object with any other property is invalid",
"data": {"bar": 1},
"valid": false
},
{
"description": "empty object is valid",
"data": {},
"valid": true
}
]
},
{
"description": "propertyNames with enum",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"propertyNames": {"enum": ["foo", "bar"]}
},
"tests": [
{
"description": "object with property foo is valid",
"data": {"foo": 1},
"valid": true
},
{
"description": "object with property foo and bar is valid",
"data": {"foo": 1, "bar": 1},
"valid": true
},
{
"description": "object with any other property is invalid",
"data": {"baz": 1},
"valid": false
},
{
"description": "empty object is valid",
"data": {},
"valid": true
}
]
}
]

View file

@ -0,0 +1,158 @@
[
{
"description": "required validation",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"properties": {
"foo": {},
"bar": {}
},
"required": ["foo"]
},
"tests": [
{
"description": "present required property is valid",
"data": {"foo": 1},
"valid": true
},
{
"description": "non-present required property is invalid",
"data": {"bar": 1},
"valid": false
},
{
"description": "ignores arrays",
"data": [],
"valid": true
},
{
"description": "ignores strings",
"data": "",
"valid": true
},
{
"description": "ignores other non-objects",
"data": 12,
"valid": true
}
]
},
{
"description": "required default validation",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"properties": {
"foo": {}
}
},
"tests": [
{
"description": "not required by default",
"data": {},
"valid": true
}
]
},
{
"description": "required with empty array",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"properties": {
"foo": {}
},
"required": []
},
"tests": [
{
"description": "property not required",
"data": {},
"valid": true
}
]
},
{
"description": "required with escaped characters",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"required": [
"foo\nbar",
"foo\"bar",
"foo\\bar",
"foo\rbar",
"foo\tbar",
"foo\fbar"
]
},
"tests": [
{
"description": "object with all properties present is valid",
"data": {
"foo\nbar": 1,
"foo\"bar": 1,
"foo\\bar": 1,
"foo\rbar": 1,
"foo\tbar": 1,
"foo\fbar": 1
},
"valid": true
},
{
"description": "object with some properties missing is invalid",
"data": {
"foo\nbar": "1",
"foo\"bar": "1"
},
"valid": false
}
]
},
{
"description": "required properties whose names are Javascript object property names",
"comment": "Ensure JS implementations don't universally consider e.g. __proto__ to always be present in an object.",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"required": ["__proto__", "toString", "constructor"]
},
"tests": [
{
"description": "ignores arrays",
"data": [],
"valid": true
},
{
"description": "ignores other non-objects",
"data": 12,
"valid": true
},
{
"description": "none of the properties mentioned",
"data": {},
"valid": false
},
{
"description": "__proto__ present",
"data": { "__proto__": "foo" },
"valid": false
},
{
"description": "toString present",
"data": { "toString": { "length": 37 } },
"valid": false
},
{
"description": "constructor present",
"data": { "constructor": { "length": 37 } },
"valid": false
},
{
"description": "all present",
"data": {
"__proto__": 12,
"toString": { "length": "foo" },
"constructor": 37
},
"valid": true
}
]
}
]

File diff suppressed because it is too large Load diff

View file

@ -79,11 +79,11 @@ func (st *state) validate(instance reflect.Value, schema *Schema, callerAnns *an
// "number" subsumes integers
if !(gotType == schema.Type ||
gotType == "integer" && schema.Type == "number") {
return fmt.Errorf("type: %s has type %q, want %q", instance, gotType, schema.Type)
return fmt.Errorf("type: %v has type %q, want %q", instance, gotType, schema.Type)
}
} else {
if !(slices.Contains(schema.Types, gotType) || (gotType == "integer" && slices.Contains(schema.Types, "number"))) {
return fmt.Errorf("type: %s has type %q, want one of %q",
return fmt.Errorf("type: %v has type %q, want one of %q",
instance, gotType, strings.Join(schema.Types, ", "))
}
}
@ -322,6 +322,142 @@ func (st *state) validate(instance reflect.Value, schema *Schema, callerAnns *an
}
}
// objects
// https://json-schema.org/draft/2020-12/json-schema-core#section-10.3.2
if instance.Kind() == reflect.Map {
if kt := instance.Type().Key(); kt.Kind() != reflect.String {
return fmt.Errorf("map key type %s is not a string", kt)
}
// Track the evaluated properties for just this schema, to support additionalProperties.
// If we used anns here, then we'd be including properties evaluated in subschemas
// from allOf, etc., which additionalProperties shouldn't observe.
evalProps := map[string]bool{}
for prop, schema := range schema.Properties {
val := instance.MapIndex(reflect.ValueOf(prop))
if !val.IsValid() {
// It's OK if the instance doesn't have the property.
continue
}
if err := st.validate(val, schema, nil, append(path, prop)); err != nil {
return err
}
evalProps[prop] = true
}
if len(schema.PatternProperties) > 0 {
for vprop, val := range instance.Seq2() {
prop := vprop.String()
// Check every matching pattern.
for pattern, schema := range schema.PatternProperties {
// TODO(jba): pre-compile regexps
m, err := regexp.MatchString(pattern, prop)
if err != nil {
return err
}
if m {
if err := st.validate(val, schema, nil, append(path, prop)); err != nil {
return err
}
evalProps[prop] = true
}
}
}
}
if schema.AdditionalProperties != nil {
// Apply to all properties not handled above.
for vprop, val := range instance.Seq2() {
prop := vprop.String()
if !evalProps[prop] {
if err := st.validate(val, schema.AdditionalProperties, nil, append(path, prop)); err != nil {
return err
}
evalProps[prop] = true
}
}
}
anns.noteProperties(evalProps)
if schema.PropertyNames != nil {
for prop := range instance.Seq() {
if err := st.validate(prop, schema.PropertyNames, nil, append(path, prop.String())); err != nil {
return err
}
}
}
// https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-01#section-6.5
if schema.MinProperties != nil {
if n, m := instance.Len(), int(*schema.MinProperties); n < m {
return fmt.Errorf("minProperties: object has %d properties, less than %d", n, m)
}
}
if schema.MaxProperties != nil {
if n, m := instance.Len(), int(*schema.MaxProperties); n > m {
return fmt.Errorf("maxProperties: object has %d properties, greater than %d", n, m)
}
}
hasProperty := func(prop string) bool {
return instance.MapIndex(reflect.ValueOf(prop)).IsValid()
}
missingProperties := func(props []string) []string {
var missing []string
for _, p := range props {
if !hasProperty(p) {
missing = append(missing, p)
}
}
return missing
}
if schema.Required != nil {
if m := missingProperties(schema.Required); len(m) > 0 {
return fmt.Errorf("required: missing properties: %q", m)
}
}
if schema.DependentRequired != nil {
// "Validation succeeds if, for each name that appears in both the instance
// and as a name within this keyword's value, every item in the corresponding
// array is also the name of a property in the instance." §6.5.4
for dprop, reqs := range schema.DependentRequired {
if hasProperty(dprop) {
if m := missingProperties(reqs); len(m) > 0 {
return fmt.Errorf("dependentRequired[%q]: missing properties %q", dprop, m)
}
}
}
}
// https://json-schema.org/draft/2020-12/json-schema-core#section-10.2.2.4
if schema.DependentSchemas != nil {
// This does not collect annotations, although it seems like it should.
for dprop, ss := range schema.DependentSchemas {
if hasProperty(dprop) {
// TODO: include dependentSchemas[dprop] in the errors.
err := st.validate(instance, ss, &anns, path)
if err != nil {
return err
}
}
}
}
if schema.UnevaluatedProperties != nil && !anns.allProperties {
// This looks a lot like AdditionalProperties, but depends on in-place keywords like allOf
// in addition to sibling keywords.
for vprop, val := range instance.Seq2() {
prop := vprop.String()
if !anns.evaluatedProperties[prop] {
if err := st.validate(val, schema.UnevaluatedProperties, nil, append(path, prop)); err != nil {
return err
}
}
}
// The spec says the annotation should be the set of evaluated properties, but we can optimize
// by setting a single boolean, since after this succeeds all properties will be validated.
// See https://json-schema.slack.com/archives/CT7FF623C/p1745592564381459.
anns.allProperties = true
}
}
if callerAnns != nil {
// Our caller wants to know what we've validated.
callerAnns.merge(&anns)

View file

@ -53,7 +53,7 @@ func TestValidate(t *testing.T) {
t.Run(g.Description, func(t *testing.T) {
rs := &ResolvedSchema{root: g.Schema}
for s := range g.Schema.all() {
if s.Properties != nil || s.Required != nil || s.Defs != nil || s.UnevaluatedProperties != nil {
if s.Defs != nil || s.Ref != "" {
t.Skip("schema or subschema has unimplemented keywords")
}
}