jsonschema: add more schema fields
Add additional fields to Schema which are mentioned in the spec but do not affect validation. Change-Id: I89488261a00c207d01cd7fc59d782f43692fe528 Reviewed-on: https://go-review.googlesource.com/c/tools/+/674976 Reviewed-by: Alan Donovan <adonovan@google.com> LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com> Auto-Submit: Jonathan Amsterdam <jba@google.com>
This commit is contained in:
parent
715e9f1935
commit
fdbd35a415
5 changed files with 134 additions and 17 deletions
|
|
@ -153,6 +153,14 @@ func (s *Schema) checkLocal(report func(error)) {
|
||||||
// TODO: validate the schema's properties,
|
// TODO: validate the schema's properties,
|
||||||
// ideally by jsonschema-validating it against the meta-schema.
|
// ideally by jsonschema-validating it against the meta-schema.
|
||||||
|
|
||||||
|
// Some properties are present so that Schemas can round-trip, but we do not
|
||||||
|
// validate them.
|
||||||
|
// Currently, it's just the $vocabulary property.
|
||||||
|
// As a special case, we can validate the 2020-12 meta-schema.
|
||||||
|
if s.Vocabulary != nil && s.Schema != draft202012 {
|
||||||
|
addf("cannot validate a schema with $vocabulary")
|
||||||
|
}
|
||||||
|
|
||||||
// Check and compile regexps.
|
// Check and compile regexps.
|
||||||
if s.Pattern != "" {
|
if s.Pattern != "" {
|
||||||
re, err := regexp.Compile(s.Pattern)
|
re, err := regexp.Compile(s.Pattern)
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,11 @@ type Schema struct {
|
||||||
// metadata
|
// metadata
|
||||||
Title string `json:"title,omitempty"`
|
Title string `json:"title,omitempty"`
|
||||||
Description string `json:"description,omitempty"`
|
Description string `json:"description,omitempty"`
|
||||||
|
Default *any `json:"default,omitempty"`
|
||||||
|
Deprecated bool `json:"deprecated,omitempty"`
|
||||||
|
ReadOnly bool `json:"readOnly,omitempty"`
|
||||||
|
WriteOnly bool `json:"writeOnly,omitempty"`
|
||||||
|
Examples []any `json:"examples,omitempty"`
|
||||||
|
|
||||||
// validation
|
// validation
|
||||||
// Use Type for a single type, or Types for multiple types; never both.
|
// Use Type for a single type, or Types for multiple types; never both.
|
||||||
|
|
@ -110,6 +115,15 @@ type Schema struct {
|
||||||
Else *Schema `json:"else,omitempty"`
|
Else *Schema `json:"else,omitempty"`
|
||||||
DependentSchemas map[string]*Schema `json:"dependentSchemas,omitempty"`
|
DependentSchemas map[string]*Schema `json:"dependentSchemas,omitempty"`
|
||||||
|
|
||||||
|
// other
|
||||||
|
// https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-00#rfc.section.8
|
||||||
|
ContentEncoding string `json:"contentEncoding,omitempty"`
|
||||||
|
ContentMediaType string `json:"contentMediaType,omitempty"`
|
||||||
|
ContentSchema *Schema `json:"contentSchema,omitempty"`
|
||||||
|
|
||||||
|
// https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-00#rfc.section.7
|
||||||
|
Format string `json:"format,omitempty"`
|
||||||
|
|
||||||
// computed fields
|
// computed fields
|
||||||
|
|
||||||
// This schema's base schema.
|
// This schema's base schema.
|
||||||
|
|
@ -237,6 +251,7 @@ func (s *Schema) UnmarshalJSON(data []byte) error {
|
||||||
ms := struct {
|
ms := struct {
|
||||||
Type json.RawMessage `json:"type,omitempty"`
|
Type json.RawMessage `json:"type,omitempty"`
|
||||||
Const json.RawMessage `json:"const,omitempty"`
|
Const json.RawMessage `json:"const,omitempty"`
|
||||||
|
Default json.RawMessage `json:"default,omitempty"`
|
||||||
MinLength *integer `json:"minLength,omitempty"`
|
MinLength *integer `json:"minLength,omitempty"`
|
||||||
MaxLength *integer `json:"maxLength,omitempty"`
|
MaxLength *integer `json:"maxLength,omitempty"`
|
||||||
MinItems *integer `json:"minItems,omitempty"`
|
MinItems *integer `json:"minItems,omitempty"`
|
||||||
|
|
@ -269,14 +284,24 @@ func (s *Schema) UnmarshalJSON(data []byte) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Setting Const to a pointer to null will marshal properly, but won't unmarshal:
|
unmarshalAnyPtr := func(p **any, raw json.RawMessage) error {
|
||||||
// the *any is set to nil, not a pointer to nil.
|
if len(raw) == 0 {
|
||||||
if len(ms.Const) > 0 {
|
return nil
|
||||||
if bytes.Equal(ms.Const, []byte("null")) {
|
|
||||||
s.Const = new(any)
|
|
||||||
} else if err := json.Unmarshal(ms.Const, &s.Const); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
if bytes.Equal(raw, []byte("null")) {
|
||||||
|
*p = new(any)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return json.Unmarshal(raw, p)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setting Const or Default to a pointer to null will marshal properly, but won't
|
||||||
|
// unmarshal: the *any is set to nil, not a pointer to nil.
|
||||||
|
if err := unmarshalAnyPtr(&s.Const, ms.Const); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := unmarshalAnyPtr(&s.Default, ms.Default); err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
set := func(dst **int, src *integer) {
|
set := func(dst **int, src *integer) {
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ func TestGoRoundTrip(t *testing.T) {
|
||||||
{Const: Ptr(any(nil))},
|
{Const: Ptr(any(nil))},
|
||||||
{Const: Ptr(any([]int{}))},
|
{Const: Ptr(any([]int{}))},
|
||||||
{Const: Ptr(any(map[string]any{}))},
|
{Const: Ptr(any(map[string]any{}))},
|
||||||
|
{Default: Ptr(any(nil))},
|
||||||
} {
|
} {
|
||||||
data, err := json.Marshal(s)
|
data, err := json.Marshal(s)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -31,9 +32,7 @@ func TestGoRoundTrip(t *testing.T) {
|
||||||
}
|
}
|
||||||
t.Logf("marshal: %s", data)
|
t.Logf("marshal: %s", data)
|
||||||
var got *Schema
|
var got *Schema
|
||||||
if err := json.Unmarshal(data, &got); err != nil {
|
mustUnmarshal(t, data, &got)
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if !Equal(got, s) {
|
if !Equal(got, s) {
|
||||||
t.Errorf("got %+v, want %+v", got, s)
|
t.Errorf("got %+v, want %+v", got, s)
|
||||||
if got.Const != nil && s.Const != nil {
|
if got.Const != nil && s.Const != nil {
|
||||||
|
|
@ -68,9 +67,7 @@ func TestJSONRoundTrip(t *testing.T) {
|
||||||
{`{"unk":0}`, `{}`}, // unknown fields are dropped, unfortunately
|
{`{"unk":0}`, `{}`}, // unknown fields are dropped, unfortunately
|
||||||
} {
|
} {
|
||||||
var s Schema
|
var s Schema
|
||||||
if err := json.Unmarshal([]byte(tt.in), &s); err != nil {
|
mustUnmarshal(t, []byte(tt.in), &s)
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
data, err := json.Marshal(s)
|
data, err := json.Marshal(s)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
|
|
@ -126,3 +123,10 @@ func TestEvery(t *testing.T) {
|
||||||
t.Errorf("got %d, want %d", got, want)
|
t.Errorf("got %d, want %d", got, want)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func mustUnmarshal(t *testing.T, data []byte, ptr any) {
|
||||||
|
t.Helper()
|
||||||
|
if err := json.Unmarshal(data, ptr); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
82
jsonschema/testdata/draft2020-12/default.json
vendored
Normal file
82
jsonschema/testdata/draft2020-12/default.json
vendored
Normal file
|
|
@ -0,0 +1,82 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"description": "invalid type for default",
|
||||||
|
"schema": {
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"properties": {
|
||||||
|
"foo": {
|
||||||
|
"type": "integer",
|
||||||
|
"default": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tests": [
|
||||||
|
{
|
||||||
|
"description": "valid when property is specified",
|
||||||
|
"data": {"foo": 13},
|
||||||
|
"valid": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "still valid when the invalid default is used",
|
||||||
|
"data": {},
|
||||||
|
"valid": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "invalid string value for default",
|
||||||
|
"schema": {
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"properties": {
|
||||||
|
"bar": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 4,
|
||||||
|
"default": "bad"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tests": [
|
||||||
|
{
|
||||||
|
"description": "valid when property is specified",
|
||||||
|
"data": {"bar": "good"},
|
||||||
|
"valid": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "still valid when the invalid default is used",
|
||||||
|
"data": {},
|
||||||
|
"valid": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "the default keyword does not do anything if the property is missing",
|
||||||
|
"schema": {
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"alpha": {
|
||||||
|
"type": "number",
|
||||||
|
"maximum": 3,
|
||||||
|
"default": 5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tests": [
|
||||||
|
{
|
||||||
|
"description": "an explicit property value is checked against maximum (passing)",
|
||||||
|
"data": { "alpha": 1 },
|
||||||
|
"valid": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "an explicit property value is checked against maximum (failing)",
|
||||||
|
"data": { "alpha": 5 },
|
||||||
|
"valid": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "missing properties are not filled in with the default",
|
||||||
|
"data": {},
|
||||||
|
"valid": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
@ -42,14 +42,12 @@ func TestValidate(t *testing.T) {
|
||||||
for _, file := range files {
|
for _, file := range files {
|
||||||
base := filepath.Base(file)
|
base := filepath.Base(file)
|
||||||
t.Run(base, func(t *testing.T) {
|
t.Run(base, func(t *testing.T) {
|
||||||
f, err := os.Open(file)
|
data, err := os.ReadFile(file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
defer f.Close()
|
|
||||||
dec := json.NewDecoder(f)
|
|
||||||
var groups []testGroup
|
var groups []testGroup
|
||||||
if err := dec.Decode(&groups); err != nil {
|
if err := json.Unmarshal(data, &groups); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
for _, g := range groups {
|
for _, g := range groups {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue