diff --git a/jsonschema/resolve.go b/jsonschema/resolve.go index 9fb8826..8f88121 100644 --- a/jsonschema/resolve.go +++ b/jsonschema/resolve.go @@ -153,6 +153,14 @@ func (s *Schema) checkLocal(report func(error)) { // TODO: validate the schema's properties, // 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. if s.Pattern != "" { re, err := regexp.Compile(s.Pattern) diff --git a/jsonschema/schema.go b/jsonschema/schema.go index e87dd33..f3e6a08 100644 --- a/jsonschema/schema.go +++ b/jsonschema/schema.go @@ -58,6 +58,11 @@ type Schema struct { // metadata Title string `json:"title,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 // 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"` 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 // This schema's base schema. @@ -237,6 +251,7 @@ func (s *Schema) UnmarshalJSON(data []byte) error { ms := struct { Type json.RawMessage `json:"type,omitempty"` Const json.RawMessage `json:"const,omitempty"` + Default json.RawMessage `json:"default,omitempty"` MinLength *integer `json:"minLength,omitempty"` MaxLength *integer `json:"maxLength,omitempty"` MinItems *integer `json:"minItems,omitempty"` @@ -269,14 +284,24 @@ func (s *Schema) UnmarshalJSON(data []byte) error { return err } - // Setting Const to a pointer to null will marshal properly, but won't unmarshal: - // the *any is set to nil, not a pointer to nil. - if len(ms.Const) > 0 { - if bytes.Equal(ms.Const, []byte("null")) { - s.Const = new(any) - } else if err := json.Unmarshal(ms.Const, &s.Const); err != nil { - return err + unmarshalAnyPtr := func(p **any, raw json.RawMessage) error { + if len(raw) == 0 { + return nil } + 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) { diff --git a/jsonschema/schema_test.go b/jsonschema/schema_test.go index 117db7b..b44df3c 100644 --- a/jsonschema/schema_test.go +++ b/jsonschema/schema_test.go @@ -24,6 +24,7 @@ func TestGoRoundTrip(t *testing.T) { {Const: Ptr(any(nil))}, {Const: Ptr(any([]int{}))}, {Const: Ptr(any(map[string]any{}))}, + {Default: Ptr(any(nil))}, } { data, err := json.Marshal(s) if err != nil { @@ -31,9 +32,7 @@ func TestGoRoundTrip(t *testing.T) { } t.Logf("marshal: %s", data) var got *Schema - if err := json.Unmarshal(data, &got); err != nil { - t.Fatal(err) - } + mustUnmarshal(t, data, &got) if !Equal(got, s) { t.Errorf("got %+v, want %+v", got, s) if got.Const != nil && s.Const != nil { @@ -68,9 +67,7 @@ func TestJSONRoundTrip(t *testing.T) { {`{"unk":0}`, `{}`}, // unknown fields are dropped, unfortunately } { var s Schema - if err := json.Unmarshal([]byte(tt.in), &s); err != nil { - t.Fatal(err) - } + mustUnmarshal(t, []byte(tt.in), &s) data, err := json.Marshal(s) if err != nil { t.Fatal(err) @@ -126,3 +123,10 @@ func TestEvery(t *testing.T) { 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) + } +} diff --git a/jsonschema/testdata/draft2020-12/default.json b/jsonschema/testdata/draft2020-12/default.json new file mode 100644 index 0000000..ceb3ae2 --- /dev/null +++ b/jsonschema/testdata/draft2020-12/default.json @@ -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 + } + ] + } +] diff --git a/jsonschema/validate_test.go b/jsonschema/validate_test.go index b5cbbc6..66208a2 100644 --- a/jsonschema/validate_test.go +++ b/jsonschema/validate_test.go @@ -42,14 +42,12 @@ func TestValidate(t *testing.T) { for _, file := range files { base := filepath.Base(file) t.Run(base, func(t *testing.T) { - f, err := os.Open(file) + data, err := os.ReadFile(file) if err != nil { t.Fatal(err) } - defer f.Close() - dec := json.NewDecoder(f) var groups []testGroup - if err := dec.Decode(&groups); err != nil { + if err := json.Unmarshal(data, &groups); err != nil { t.Fatal(err) } for _, g := range groups {