// Copyright 2025 The Go MCP SDK Authors. All rights reserved. // Use of this source code is governed by an MIT-style // license that can be found in the LICENSE file. package jsonschema import ( "encoding/json" "fmt" "net/url" "os" "path/filepath" "reflect" "strings" "testing" ) // The test for validation uses the official test suite, expressed as a set of JSON files. // Each file is an array of group objects. // A testGroup consists of a schema and some tests on it. type testGroup struct { Description string Schema *Schema Tests []test } // A test consists of a JSON instance to be validated and the expected result. type test struct { Description string Data any Valid bool } func TestValidate(t *testing.T) { files, err := filepath.Glob(filepath.FromSlash("testdata/draft2020-12/*.json")) if err != nil { t.Fatal(err) } if len(files) == 0 { t.Fatal("no files") } for _, file := range files { base := filepath.Base(file) t.Run(base, func(t *testing.T) { data, err := os.ReadFile(file) if err != nil { t.Fatal(err) } var groups []testGroup if err := json.Unmarshal(data, &groups); err != nil { t.Fatal(err) } for _, g := range groups { t.Run(g.Description, func(t *testing.T) { rs, err := g.Schema.Resolve(&ResolveOptions{Loader: loadRemote}) if err != nil { t.Fatal(err) } for _, test := range g.Tests { t.Run(test.Description, func(t *testing.T) { err = rs.Validate(test.Data) if err != nil && test.Valid { t.Errorf("wanted success, but failed with: %v", err) } if err == nil && !test.Valid { t.Error("succeeded but wanted failure") } if t.Failed() { t.Errorf("schema: %s", g.Schema.json()) t.Fatalf("instance: %v (%[1]T)", test.Data) } }) } }) } }) } } func TestValidateErrors(t *testing.T) { schema := &Schema{ PrefixItems: []*Schema{{Contains: &Schema{Type: "integer"}}}, } rs, err := schema.Resolve(nil) if err != nil { t.Fatal(err) } err = rs.Validate([]any{[]any{"1"}}) want := "prefixItems/0" if err == nil || !strings.Contains(err.Error(), want) { t.Errorf("error:\n%s\ndoes not contain %q", err, want) } } func TestValidateDefaults(t *testing.T) { s := &Schema{ Properties: map[string]*Schema{ "a": {Type: "integer", Default: mustMarshal(1)}, "b": {Type: "string", Default: mustMarshal("s")}, }, Default: mustMarshal(map[string]any{"a": 1, "b": "two"}), } if _, err := s.Resolve(&ResolveOptions{ValidateDefaults: true}); err != nil { t.Fatal(err) } s = &Schema{ Properties: map[string]*Schema{ "a": {Type: "integer", Default: mustMarshal(3)}, "b": {Type: "string", Default: mustMarshal("s")}, }, Default: mustMarshal(map[string]any{"a": 1, "b": 2}), } _, err := s.Resolve(&ResolveOptions{ValidateDefaults: true}) want := `has type "integer", want "string"` if err == nil || !strings.Contains(err.Error(), want) { t.Errorf("Resolve returned error %q, want %q", err, want) } } func TestApplyDefaults(t *testing.T) { schema := &Schema{ Properties: map[string]*Schema{ "A": {Default: mustMarshal(1)}, "B": {Default: mustMarshal(2)}, "C": {Default: mustMarshal(3)}, }, Required: []string{"C"}, } rs, err := schema.Resolve(&ResolveOptions{ValidateDefaults: true}) if err != nil { t.Fatal(err) } type S struct{ A, B, C int } for _, tt := range []struct { instancep any // pointer to instance value want any // desired value (not a pointer) }{ { &map[string]any{"B": 0}, map[string]any{ "A": float64(1), // filled from default "B": 0, // untouched: it was already there // "C" not added: it is required (Validate will catch that) }, }, { &S{B: 1}, S{ A: 1, // filled from default B: 1, // untouched: non-zero C: 0, // untouched: required }, }, } { if err := rs.ApplyDefaults(tt.instancep); err != nil { t.Fatal(err) } got := reflect.ValueOf(tt.instancep).Elem().Interface() // dereference the pointer if !reflect.DeepEqual(got, tt.want) { t.Errorf("\ngot %#v\nwant %#v", got, tt.want) } } } func TestStructInstance(t *testing.T) { instance := struct { I int B bool `json:"b"` P *int // either missing or nil u int // unexported: not a property }{1, true, nil, 0} for _, tt := range []struct { s Schema want bool }{ { Schema{MinProperties: Ptr(4)}, false, }, { Schema{MinProperties: Ptr(3)}, true, // P interpreted as present }, { Schema{MaxProperties: Ptr(1)}, false, }, { Schema{MaxProperties: Ptr(2)}, true, // P interpreted as absent }, { Schema{Required: []string{"i"}}, // the name is "I" false, }, { Schema{Required: []string{"B"}}, // the name is "b" false, }, { Schema{PropertyNames: &Schema{MinLength: Ptr(2)}}, false, }, { Schema{Properties: map[string]*Schema{"b": {Type: "boolean"}}}, true, }, { Schema{Properties: map[string]*Schema{"b": {Type: "number"}}}, false, }, { Schema{Required: []string{"I"}}, true, }, { Schema{Required: []string{"I", "P"}}, true, // P interpreted as present }, { Schema{Required: []string{"I", "P"}, Properties: map[string]*Schema{"P": {Type: "number"}}}, false, // P interpreted as present, but not a number }, { Schema{Required: []string{"I"}, Properties: map[string]*Schema{"P": {Type: "number"}}}, true, // P not required, so interpreted as absent }, { Schema{Required: []string{"I"}, AdditionalProperties: falseSchema()}, false, }, { Schema{DependentRequired: map[string][]string{"b": {"u"}}}, false, }, { Schema{DependentSchemas: map[string]*Schema{"b": falseSchema()}}, false, }, { Schema{UnevaluatedProperties: falseSchema()}, false, }, } { res, err := tt.s.Resolve(nil) if err != nil { t.Fatal(err) } err = res.Validate(instance) if err == nil && !tt.want { t.Errorf("succeeded unexpectedly\nschema = %s", tt.s.json()) } else if err != nil && tt.want { t.Errorf("Validate: %v\nschema = %s", err, tt.s.json()) } } } func TestJSONName(t *testing.T) { type S struct { A int B int `json:","` C int `json:"-"` D int `json:"-,"` E int `json:"echo"` F int `json:"foxtrot,omitempty"` g int `json:"golf"` } want := []string{"A", "B", "", "-", "echo", "foxtrot", ""} tt := reflect.TypeFor[S]() for i := range tt.NumField() { got, _ := jsonName(tt.Field(i)) if got != want[i] { t.Errorf("got %q, want %q", got, want[i]) } } } func mustMarshal(x any) json.RawMessage { data, err := json.Marshal(x) if err != nil { panic(err) } return json.RawMessage(data) } // loadRemote loads a remote reference used in the test suite. func loadRemote(uri *url.URL) (*Schema, error) { // Anything with localhost:1234 refers to the remotes directory in the test suite repo. if uri.Host == "localhost:1234" { return loadSchemaFromFile(filepath.FromSlash(filepath.Join("testdata/remotes", uri.Path))) } // One test needs the meta-schema files. const metaPrefix = "https://json-schema.org/draft/2020-12/" if after, ok := strings.CutPrefix(uri.String(), metaPrefix); ok { return loadSchemaFromFile(filepath.FromSlash("meta-schemas/draft2020-12/" + after + ".json")) } return nil, fmt.Errorf("don't know how to load %s", uri) } func loadSchemaFromFile(filename string) (*Schema, error) { data, err := os.ReadFile(filename) if err != nil { return nil, err } var s Schema if err := json.Unmarshal(data, &s); err != nil { return nil, fmt.Errorf("unmarshaling JSON at %s: %w", filename, err) } return &s, nil }