jsonschema: remove jsonschema code and depend on google/jsonschema-go (#262)

This CL prunes the jsonschema directory and updates the go.mod.

For #28
This commit is contained in:
Sam Thanawalla 2025-08-07 15:30:06 -04:00 • committed by GitHub
commit e9e0da8dac
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
103 changed files with 13 additions and 15643 deletions

1
go.mod
View file

@ -4,6 +4,7 @@ go 1.23.0
require (
github.com/google/go-cmp v0.7.0
github.com/google/jsonschema-go v0.2.0
github.com/yosida95/uritemplate/v3 v3.0.2
golang.org/x/tools v0.34.0
)

2
go.sum
View file

@ -1,5 +1,7 @@
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/jsonschema-go v0.2.0 h1:Uh19091iHC56//WOsAd1oRg6yy1P9BpSvpjOL6RcjLQ=
github.com/google/jsonschema-go v0.2.0/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE=
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=

View file

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2025 Go MCP SDK Authors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -1,39 +0,0 @@
TODO: this file should live at the root of the jsonschema-go module,
above the jsonschema package.
# JSON Schema for GO
This module implements the [JSON Schema](https://json-schema.org/) specification.
The `jsonschema` package supports creating schemas, validating JSON values
against a schema, and inferring a schema from a Go struct. See the package
documentation for usage.
## Contributing
This module welcomes external contributions.
It has no dependencies outside of the standard library, and can be built with
the standard Go toolchain. Run `go test ./...` at the module root to run all
the tests.
## Issues
This project uses the [GitHub issue
tracker](https://github.com/TODO/jsonschema-go/issues) for bug reports, feature requests, and other issues.
Please [report
bugs](https://github.com/TODO/jsonschema-go/issues/new). If the SDK is
not working as you expected, it is likely due to a bug or inadequate
documentation, and reporting an issue will help us address this shortcoming.
When reporting a bug, make sure to answer these five questions:
1. What did you do?
2. What did you see?
3. What did you expect to see?
4. What version of the Go MCP SDK are you using?
5. What version of Go are you using (`go version`)?
## License
This project is licensed under the MIT license. See the LICENSE file for details.

View file

@ -1,76 +0,0 @@
// 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 "maps"
// An annotations tracks certain properties computed by keywords that are used by validation.
// ("Annotation" is the spec's term.)
// In particular, the unevaluatedItems and unevaluatedProperties keywords need to know which
// items and properties were evaluated (validated successfully).
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
}
// noteIndex marks i as evaluated.
func (a *annotations) noteIndex(i int) {
if a.evaluatedIndexes == nil {
a.evaluatedIndexes = map[int]bool{}
}
a.evaluatedIndexes[i] = true
}
// noteEndIndex marks items with index less than end as evaluated.
func (a *annotations) noteEndIndex(end int) {
if end > a.endIndex {
a.endIndex = end
}
}
// noteProperty marks prop as evaluated.
func (a *annotations) noteProperty(prop string) {
if a.evaluatedProperties == nil {
a.evaluatedProperties = map[string]bool{}
}
a.evaluatedProperties[prop] = true
}
// noteProperties marks all the properties in props as evaluated.
func (a *annotations) noteProperties(props map[string]bool) {
a.evaluatedProperties = merge(a.evaluatedProperties, props)
}
// merge adds b's annotations to a.
// a must not be nil.
func (a *annotations) merge(b *annotations) {
if b == nil {
return
}
if b.allItems {
a.allItems = true
}
if b.endIndex > a.endIndex {
a.endIndex = b.endIndex
}
a.evaluatedIndexes = merge(a.evaluatedIndexes, b.evaluatedIndexes)
if b.allProperties {
a.allProperties = true
}
a.evaluatedProperties = merge(a.evaluatedProperties, b.evaluatedProperties)
}
// merge adds t's keys to s and returns s.
// If s is nil, it returns a copy of t.
func merge[K comparable](s, t map[K]bool) map[K]bool {
if s == nil {
return maps.Clone(t)
}
maps.Copy(s, t)
return s
}

View file

@ -1,101 +0,0 @@
// 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 is an implementation of the [JSON Schema specification],
a JSON-based format for describing the structure of JSON data.
The package can be used to read schemas for code generation, and to validate
data using the draft 2020-12 specification. Validation with other drafts
or custom meta-schemas is not supported.
Construct a [Schema] as you would any Go struct (for example, by writing
a struct literal), or unmarshal a JSON schema into a [Schema] in the usual
way (with [encoding/json], for instance). It can then be used for code
generation or other purposes without further processing.
You can also infer a schema from a Go struct.
# Resolution
A Schema can refer to other schemas, both inside and outside itself. These
references must be resolved before a schema can be used for validation.
Call [Schema.Resolve] to obtain a resolved schema (called a [Resolved]).
If the schema has external references, pass a [ResolveOptions] with a [Loader]
to load them. To validate default values in a schema, set
[ResolveOptions.ValidateDefaults] to true.
# Validation
Call [Resolved.Validate] to validate a JSON value. The value must be a
Go value that looks like the result of unmarshaling a JSON value into an
[any] or a struct. For example, the JSON value
{"name": "Al", "scores": [90, 80, 100]}
could be represented as the Go value
map[string]any{
"name": "Al",
"scores": []any{90, 80, 100},
}
or as a value of this type:
type Player struct {
Name string `json:"name"`
Scores []int `json:"scores"`
}
# Inference
The [For] function returns a [Schema] describing the given Go type.
Each field in the struct becomes a property of the schema.
The values of "json" tags are respected: the field's property name is taken
from the tag, and fields omitted from the JSON are omitted from the schema as
well.
For example, `jsonschema.For[Player]()` returns this schema:
{
"properties": {
"name": {
"type": "string"
},
"scores": {
"type": "array",
"items": {"type": "integer"}
}
"required": ["name", "scores"],
"additionalProperties": {"not": {}}
}
}
Use the "jsonschema" struct tag to provide a description for the property:
type Player struct {
Name string `json:"name" jsonschema:"player name"`
Scores []int `json:"scores" jsonschema:"scores of player's games"`
}
# Deviations from the specification
Regular expressions are processed with Go's regexp package, which differs
from ECMA 262, most significantly in not supporting back-references.
See [this table of differences] for more.
The "format" keyword described in [section 7 of the validation spec] is recorded
in the Schema, but is ignored during validation.
It does not even produce [annotations].
Use the "pattern" keyword instead: it will work more reliably across JSON Schema
implementations. See [learnjsonschema.com] for more recommendations about "format".
The content keywords described in [section 8 of the validation spec]
are recorded in the schema, but ignored during validation.
[JSON Schema specification]: https://json-schema.org
[section 7 of the validation spec]: https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-00#rfc.section.7
[section 8 of the validation spec]: https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-00#rfc.section.8
[learnjsonschema.com]: https://www.learnjsonschema.com/2020-12/format-annotation/format/
[this table of differences]: https://github.com/dlclark/regexp2?tab=readme-ov-file#compare-regexp-and-regexp2
[annotations]: https://json-schema.org/draft/2020-12/json-schema-core#name-annotations
*/
package jsonschema

View file

@ -1,251 +0,0 @@
// 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.
// This file contains functions that infer a schema from a Go type.
package jsonschema
import (
"fmt"
"log/slog"
"maps"
"math/big"
"reflect"
"regexp"
"time"
)
// ForOptions are options for the [For] function.
type ForOptions struct {
// If IgnoreInvalidTypes is true, fields that can't be represented as a JSON
// Schema are ignored instead of causing an error.
// This allows callers to adjust the resulting schema using custom knowledge.
// For example, an interface type where all the possible implementations are
// known can be described with "oneof".
IgnoreInvalidTypes bool
// TypeSchemas maps types to their schemas.
// If [For] encounters a type equal to a type of a key in this map, the
// corresponding value is used as the resulting schema (after cloning to
// ensure uniqueness).
// Types in this map override the default translations, as described
// in [For]'s documentation.
TypeSchemas map[any]*Schema
}
// For constructs a JSON schema object for the given type argument.
// If non-nil, the provided options configure certain aspects of this contruction,
// described below.
// It translates Go types into compatible JSON schema types, as follows.
// These defaults can be overridden by [ForOptions.TypeSchemas].
//
// - Strings have schema type "string".
// - Bools have schema type "boolean".
// - Signed and unsigned integer types have schema type "integer".
// - Floating point types have schema type "number".
// - Slices and arrays have schema type "array", and a corresponding schema
// for items.
// - Maps with string key have schema type "object", and corresponding
// schema for additionalProperties.
// - Structs have schema type "object", and disallow additionalProperties.
// Their properties are derived from exported struct fields, using the
// struct field JSON name. Fields that are marked "omitempty" are
// considered optional; all other fields become required properties.
// - Some types in the standard library that implement json.Marshaler
// translate to schemas that match the values to which they marshal.
// For example, [time.Time] translates to the schema for strings.
//
// For will return an error if there is a cycle in the types.
//
// By default, For returns an error if t contains (possibly recursively) any of the
// following Go types, as they are incompatible with the JSON schema spec.
// If [ForOptions.IgnoreInvalidTypes] is true, then these types are ignored instead.
// - maps with key other than 'string'
// - function types
// - channel types
// - complex numbers
// - unsafe pointers
//
// This function recognizes struct field tags named "jsonschema".
// A jsonschema tag on a field is used as the description for the corresponding property.
// For future compatibility, descriptions must not start with "WORD=", where WORD is a
// sequence of non-whitespace characters.
func For[T any](opts *ForOptions) (*Schema, error) {
if opts == nil {
opts = &ForOptions{}
}
schemas := maps.Clone(initialSchemaMap)
// Add types from the options. They override the default ones.
for v, s := range opts.TypeSchemas {
schemas[reflect.TypeOf(v)] = s
}
s, err := forType(reflect.TypeFor[T](), map[reflect.Type]bool{}, opts.IgnoreInvalidTypes, schemas)
if err != nil {
var z T
return nil, fmt.Errorf("For[%T](): %w", z, err)
}
return s, nil
}
// ForType is like [For], but takes a [reflect.Type]
func ForType(t reflect.Type, opts *ForOptions) (*Schema, error) {
schemas := maps.Clone(initialSchemaMap)
// Add types from the options. They override the default ones.
for v, s := range opts.TypeSchemas {
schemas[reflect.TypeOf(v)] = s
}
s, err := forType(t, map[reflect.Type]bool{}, opts.IgnoreInvalidTypes, schemas)
if err != nil {
return nil, fmt.Errorf("ForType(%s): %w", t, err)
}
return s, nil
}
func forType(t reflect.Type, seen map[reflect.Type]bool, ignore bool, schemas map[reflect.Type]*Schema) (*Schema, error) {
// Follow pointers: the schema for *T is almost the same as for T, except that
// an explicit JSON "null" is allowed for the pointer.
allowNull := false
for t.Kind() == reflect.Pointer {
allowNull = true
t = t.Elem()
}
// Check for cycles
// User defined types have a name, so we can skip those that are natively defined
if t.Name() != "" {
if seen[t] {
return nil, fmt.Errorf("cycle detected for type %v", t)
}
seen[t] = true
defer delete(seen, t)
}
if s := schemas[t]; s != nil {
return s.CloneSchemas(), nil
}
var (
s = new(Schema)
err error
)
switch t.Kind() {
case reflect.Bool:
s.Type = "boolean"
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64,
reflect.Uintptr:
s.Type = "integer"
case reflect.Float32, reflect.Float64:
s.Type = "number"
case reflect.Interface:
// Unrestricted
case reflect.Map:
if t.Key().Kind() != reflect.String {
if ignore {
return nil, nil // ignore
}
return nil, fmt.Errorf("unsupported map key type %v", t.Key().Kind())
}
if t.Key().Kind() != reflect.String {
}
s.Type = "object"
s.AdditionalProperties, err = forType(t.Elem(), seen, ignore, schemas)
if err != nil {
return nil, fmt.Errorf("computing map value schema: %v", err)
}
if ignore && s.AdditionalProperties == nil {
// Ignore if the element type is invalid.
return nil, nil
}
case reflect.Slice, reflect.Array:
s.Type = "array"
s.Items, err = forType(t.Elem(), seen, ignore, schemas)
if err != nil {
return nil, fmt.Errorf("computing element schema: %v", err)
}
if ignore && s.Items == nil {
// Ignore if the element type is invalid.
return nil, nil
}
if t.Kind() == reflect.Array {
s.MinItems = Ptr(t.Len())
s.MaxItems = Ptr(t.Len())
}
case reflect.String:
s.Type = "string"
case reflect.Struct:
s.Type = "object"
// no additional properties are allowed
s.AdditionalProperties = falseSchema()
for i := range t.NumField() {
field := t.Field(i)
info := fieldJSONInfo(field)
if info.omit {
continue
}
if s.Properties == nil {
s.Properties = make(map[string]*Schema)
}
fs, err := forType(field.Type, seen, ignore, schemas)
if err != nil {
return nil, err
}
if ignore && fs == nil {
// Skip fields of invalid type.
continue
}
if tag, ok := field.Tag.Lookup("jsonschema"); ok {
if tag == "" {
return nil, fmt.Errorf("empty jsonschema tag on struct field %s.%s", t, field.Name)
}
if disallowedPrefixRegexp.MatchString(tag) {
return nil, fmt.Errorf("tag must not begin with 'WORD=': %q", tag)
}
fs.Description = tag
}
s.Properties[info.name] = fs
if !info.settings["omitempty"] && !info.settings["omitzero"] {
s.Required = append(s.Required, info.name)
}
}
default:
if ignore {
// Ignore.
return nil, nil
}
return nil, fmt.Errorf("type %v is unsupported by jsonschema", t)
}
if allowNull && s.Type != "" {
s.Types = []string{"null", s.Type}
s.Type = ""
}
schemas[t] = s
return s, nil
}
// initialSchemaMap holds types from the standard library that have MarshalJSON methods.
var initialSchemaMap = make(map[reflect.Type]*Schema)
func init() {
ss := &Schema{Type: "string"}
initialSchemaMap[reflect.TypeFor[time.Time]()] = ss
initialSchemaMap[reflect.TypeFor[slog.Level]()] = ss
initialSchemaMap[reflect.TypeFor[big.Int]()] = &Schema{Types: []string{"null", "string"}}
initialSchemaMap[reflect.TypeFor[big.Rat]()] = ss
initialSchemaMap[reflect.TypeFor[big.Float]()] = ss
}
// Disallow jsonschema tag values beginning "WORD=", for future expansion.
var disallowedPrefixRegexp = regexp.MustCompile("^[^ \t\n]*=")

View file

@ -1,340 +0,0 @@
// 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_test
import (
"log/slog"
"math/big"
"reflect"
"strings"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/modelcontextprotocol/go-sdk/jsonschema"
)
type custom int
func forType[T any](ignore bool) *jsonschema.Schema {
var s *jsonschema.Schema
var err error
opts := &jsonschema.ForOptions{
IgnoreInvalidTypes: ignore,
TypeSchemas: map[any]*jsonschema.Schema{
custom(0): {Type: "custom"},
},
}
s, err = jsonschema.For[T](opts)
if err != nil {
panic(err)
}
return s
}
func TestFor(t *testing.T) {
type schema = jsonschema.Schema
type S struct {
B int `jsonschema:"bdesc"`
}
type test struct {
name string
got *jsonschema.Schema
want *jsonschema.Schema
}
tests := func(ignore bool) []test {
return []test{
{"string", forType[string](ignore), &schema{Type: "string"}},
{"int", forType[int](ignore), &schema{Type: "integer"}},
{"int16", forType[int16](ignore), &schema{Type: "integer"}},
{"uint32", forType[int16](ignore), &schema{Type: "integer"}},
{"float64", forType[float64](ignore), &schema{Type: "number"}},
{"bool", forType[bool](ignore), &schema{Type: "boolean"}},
{"time", forType[time.Time](ignore), &schema{Type: "string"}},
{"level", forType[slog.Level](ignore), &schema{Type: "string"}},
{"bigint", forType[big.Int](ignore), &schema{Types: []string{"null", "string"}}},
{"custom", forType[custom](ignore), &schema{Type: "custom"}},
{"intmap", forType[map[string]int](ignore), &schema{
Type: "object",
AdditionalProperties: &schema{Type: "integer"},
}},
{"anymap", forType[map[string]any](ignore), &schema{
Type: "object",
AdditionalProperties: &schema{},
}},
{
"struct",
forType[struct {
F int `json:"f" jsonschema:"fdesc"`
G []float64
P *bool `jsonschema:"pdesc"`
Skip string `json:"-"`
NoSkip string `json:",omitempty"`
unexported float64
unexported2 int `json:"No"`
}](ignore),
&schema{
Type: "object",
Properties: map[string]*schema{
"f": {Type: "integer", Description: "fdesc"},
"G": {Type: "array", Items: &schema{Type: "number"}},
"P": {Types: []string{"null", "boolean"}, Description: "pdesc"},
"NoSkip": {Type: "string"},
},
Required: []string{"f", "G", "P"},
AdditionalProperties: falseSchema(),
},
},
{
"no sharing",
forType[struct{ X, Y int }](ignore),
&schema{
Type: "object",
Properties: map[string]*schema{
"X": {Type: "integer"},
"Y": {Type: "integer"},
},
Required: []string{"X", "Y"},
AdditionalProperties: falseSchema(),
},
},
{
"nested and embedded",
forType[struct {
A S
S
}](ignore),
&schema{
Type: "object",
Properties: map[string]*schema{
"A": {
Type: "object",
Properties: map[string]*schema{
"B": {Type: "integer", Description: "bdesc"},
},
Required: []string{"B"},
AdditionalProperties: falseSchema(),
},
"S": {
Type: "object",
Properties: map[string]*schema{
"B": {Type: "integer", Description: "bdesc"},
},
Required: []string{"B"},
AdditionalProperties: falseSchema(),
},
},
Required: []string{"A", "S"},
AdditionalProperties: falseSchema(),
},
},
}
}
run := func(t *testing.T, tt test) {
if diff := cmp.Diff(tt.want, tt.got, cmpopts.IgnoreUnexported(jsonschema.Schema{})); diff != "" {
t.Fatalf("For mismatch (-want +got):\n%s", diff)
}
// These schemas should all resolve.
if _, err := tt.got.Resolve(nil); err != nil {
t.Fatalf("Resolving: %v", err)
}
}
t.Run("strict", func(t *testing.T) {
for _, test := range tests(false) {
t.Run(test.name, func(t *testing.T) { run(t, test) })
}
})
laxTests := append(tests(true), test{
"ignore",
forType[struct {
A int
B map[int]int
C func()
}](true),
&schema{
Type: "object",
Properties: map[string]*schema{
"A": {Type: "integer"},
},
Required: []string{"A"},
AdditionalProperties: falseSchema(),
},
})
t.Run("lax", func(t *testing.T) {
for _, test := range laxTests {
t.Run(test.name, func(t *testing.T) { run(t, test) })
}
})
}
func TestForType(t *testing.T) {
type schema = jsonschema.Schema
// ForType is virtually identical to For. Just test that options are handled properly.
opts := &jsonschema.ForOptions{
IgnoreInvalidTypes: true,
TypeSchemas: map[any]*jsonschema.Schema{
custom(0): {Type: "custom"},
},
}
type S struct {
I int
F func()
C custom
}
got, err := jsonschema.ForType(reflect.TypeOf(S{}), opts)
if err != nil {
t.Fatal(err)
}
want := &schema{
Type: "object",
Properties: map[string]*schema{
"I": {Type: "integer"},
"C": {Type: "custom"},
},
Required: []string{"I", "C"},
AdditionalProperties: falseSchema(),
}
if diff := cmp.Diff(want, got, cmpopts.IgnoreUnexported(schema{})); diff != "" {
t.Fatalf("ForType mismatch (-want +got):\n%s", diff)
}
}
func forErr[T any]() error {
_, err := jsonschema.For[T](nil)
return err
}
func TestForErrors(t *testing.T) {
type (
s1 struct {
Empty int `jsonschema:""`
}
s2 struct {
Bad int `jsonschema:"$foo=1,bar"`
}
)
for _, tt := range []struct {
got error
want string
}{
{forErr[map[int]int](), "unsupported map key type"},
{forErr[s1](), "empty jsonschema tag"},
{forErr[s2](), "must not begin with"},
{forErr[func()](), "unsupported"},
} {
if tt.got == nil {
t.Errorf("got nil, want error containing %q", tt.want)
} else if !strings.Contains(tt.got.Error(), tt.want) {
t.Errorf("got %q\nwant it to contain %q", tt.got, tt.want)
}
}
}
func TestForWithMutation(t *testing.T) {
// This test ensures that the cached schema is not mutated when the caller
// mutates the returned schema.
type S struct {
A int
}
type T struct {
A int `json:"A"`
B map[string]int
C []S
D [3]S
E *bool
}
s, err := jsonschema.For[T](nil)
if err != nil {
t.Fatalf("For: %v", err)
}
s.Required[0] = "mutated"
s.Properties["A"].Type = "mutated"
s.Properties["C"].Items.Type = "mutated"
s.Properties["D"].MaxItems = jsonschema.Ptr(10)
s.Properties["D"].MinItems = jsonschema.Ptr(10)
s.Properties["E"].Types[0] = "mutated"
s2, err := jsonschema.For[T](nil)
if err != nil {
t.Fatalf("For: %v", err)
}
if s2.Properties["A"].Type == "mutated" {
t.Fatalf("ForWithMutation: expected A.Type to not be mutated")
}
if s2.Properties["B"].AdditionalProperties.Type == "mutated" {
t.Fatalf("ForWithMutation: expected B.AdditionalProperties.Type to not be mutated")
}
if s2.Properties["C"].Items.Type == "mutated" {
t.Fatalf("ForWithMutation: expected C.Items.Type to not be mutated")
}
if *s2.Properties["D"].MaxItems == 10 {
t.Fatalf("ForWithMutation: expected D.MaxItems to not be mutated")
}
if *s2.Properties["D"].MinItems == 10 {
t.Fatalf("ForWithMutation: expected D.MinItems to not be mutated")
}
if s2.Properties["E"].Types[0] == "mutated" {
t.Fatalf("ForWithMutation: expected E.Types[0] to not be mutated")
}
if s2.Required[0] == "mutated" {
t.Fatalf("ForWithMutation: expected Required[0] to not be mutated")
}
}
type x struct {
Y y
}
type y struct {
X []x
}
func TestForWithCycle(t *testing.T) {
type a []*a
type b1 struct{ b *b1 } // unexported field should be skipped
type b2 struct{ B *b2 }
type c1 struct{ c map[string]*c1 } // unexported field should be skipped
type c2 struct{ C map[string]*c2 }
tests := []struct {
name string
shouldErr bool
fn func() error
}{
{"slice alias (a)", true, func() error { _, err := jsonschema.For[a](nil); return err }},
{"unexported self cycle (b1)", false, func() error { _, err := jsonschema.For[b1](nil); return err }},
{"exported self cycle (b2)", true, func() error { _, err := jsonschema.For[b2](nil); return err }},
{"unexported map self cycle (c1)", false, func() error { _, err := jsonschema.For[c1](nil); return err }},
{"exported map self cycle (c2)", true, func() error { _, err := jsonschema.For[c2](nil); return err }},
{"cross-cycle x -> y -> x", true, func() error { _, err := jsonschema.For[x](nil); return err }},
{"cross-cycle y -> x -> y", true, func() error { _, err := jsonschema.For[y](nil); return err }},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
err := test.fn()
if test.shouldErr && err == nil {
t.Errorf("expected cycle error, got nil")
}
if !test.shouldErr && err != nil {
t.Errorf("unexpected error: %v", err)
}
})
}
}
func falseSchema() *jsonschema.Schema {
return &jsonschema.Schema{Not: &jsonschema.Schema{}}
}

View file

@ -1,146 +0,0 @@
// 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.
// This file implements JSON Pointers.
// A JSON Pointer is a path that refers to one JSON value within another.
// If the path is empty, it refers to the root value.
// Otherwise, it is a sequence of slash-prefixed strings, like "/points/1/x",
// selecting successive properties (for JSON objects) or items (for JSON arrays).
// For example, when applied to this JSON value:
// {
// "points": [
// {"x": 1, "y": 2},
// {"x": 3, "y": 4}
// ]
// }
//
// the JSON Pointer "/points/1/x" refers to the number 3.
// See the spec at https://datatracker.ietf.org/doc/html/rfc6901.
package jsonschema
import (
"errors"
"fmt"
"reflect"
"strconv"
"strings"
)
var (
jsonPointerEscaper = strings.NewReplacer("~", "~0", "/", "~1")
jsonPointerUnescaper = strings.NewReplacer("~0", "~", "~1", "/")
)
func escapeJSONPointerSegment(s string) string {
return jsonPointerEscaper.Replace(s)
}
func unescapeJSONPointerSegment(s string) string {
return jsonPointerUnescaper.Replace(s)
}
// parseJSONPointer splits a JSON Pointer into a sequence of segments. It doesn't
// convert strings to numbers, because that depends on the traversal: a segment
// is treated as a number when applied to an array, but a string when applied to
// an object. See section 4 of the spec.
func parseJSONPointer(ptr string) (segments []string, err error) {
if ptr == "" {
return nil, nil
}
if ptr[0] != '/' {
return nil, fmt.Errorf("JSON Pointer %q does not begin with '/'", ptr)
}
// Unlike file paths, consecutive slashes are not coalesced.
// Split is nicer than Cut here, because it gets a final "/" right.
segments = strings.Split(ptr[1:], "/")
if strings.Contains(ptr, "~") {
// Undo the simple escaping rules that allow one to include a slash in a segment.
for i := range segments {
segments[i] = unescapeJSONPointerSegment(segments[i])
}
}
return segments, nil
}
// dereferenceJSONPointer returns the Schema that sptr points to within s,
// or an error if none.
// This implementation suffices for JSON Schema: pointers are applied only to Schemas,
// and refer only to Schemas.
func dereferenceJSONPointer(s *Schema, sptr string) (_ *Schema, err error) {
defer wrapf(&err, "JSON Pointer %q", sptr)
segments, err := parseJSONPointer(sptr)
if err != nil {
return nil, err
}
v := reflect.ValueOf(s)
for _, seg := range segments {
switch v.Kind() {
case reflect.Pointer:
v = v.Elem()
if !v.IsValid() {
return nil, errors.New("navigated to nil reference")
}
fallthrough // if valid, can only be a pointer to a Schema
case reflect.Struct:
// The segment must refer to a field in a Schema.
if v.Type() != reflect.TypeFor[Schema]() {
return nil, fmt.Errorf("navigated to non-Schema %s", v.Type())
}
v = lookupSchemaField(v, seg)
if !v.IsValid() {
return nil, fmt.Errorf("no schema field %q", seg)
}
case reflect.Slice, reflect.Array:
// The segment must be an integer without leading zeroes that refers to an item in the
// slice or array.
if seg == "-" {
return nil, errors.New("the JSON Pointer array segment '-' is not supported")
}
if len(seg) > 1 && seg[0] == '0' {
return nil, fmt.Errorf("segment %q has leading zeroes", seg)
}
n, err := strconv.Atoi(seg)
if err != nil {
return nil, fmt.Errorf("invalid int: %q", seg)
}
if n < 0 || n >= v.Len() {
return nil, fmt.Errorf("index %d is out of bounds for array of length %d", n, v.Len())
}
v = v.Index(n)
// Cannot be invalid.
case reflect.Map:
// The segment must be a key in the map.
v = v.MapIndex(reflect.ValueOf(seg))
if !v.IsValid() {
return nil, fmt.Errorf("no key %q in map", seg)
}
default:
return nil, fmt.Errorf("value %s (%s) is not a schema, slice or map", v, v.Type())
}
}
if s, ok := v.Interface().(*Schema); ok {
return s, nil
}
return nil, fmt.Errorf("does not refer to a schema, but to a %s", v.Type())
}
// lookupSchemaField returns the value of the field with the given name in v,
// or the zero value if there is no such field or it is not of type Schema or *Schema.
func lookupSchemaField(v reflect.Value, name string) reflect.Value {
if name == "type" {
// The "type" keyword may refer to Type or Types.
// At most one will be non-zero.
if t := v.FieldByName("Type"); !t.IsZero() {
return t
}
return v.FieldByName("Types")
}
if sf, ok := schemaFieldMap[name]; ok {
return v.FieldByIndex(sf.Index)
}
return reflect.Value{}
}

View file

@ -1,78 +0,0 @@
// 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 (
"strings"
"testing"
)
func TestDereferenceJSONPointer(t *testing.T) {
s := &Schema{
AllOf: []*Schema{{}, {}},
Defs: map[string]*Schema{
"": {Properties: map[string]*Schema{"": {}}},
"A": {},
"B": {
Defs: map[string]*Schema{
"X": {},
"Y": {},
},
},
"/~": {},
"~1": {},
},
}
for _, tt := range []struct {
ptr string
want any
}{
{"", s},
{"/$defs/A", s.Defs["A"]},
{"/$defs/B", s.Defs["B"]},
{"/$defs/B/$defs/X", s.Defs["B"].Defs["X"]},
{"/$defs//properties/", s.Defs[""].Properties[""]},
{"/allOf/1", s.AllOf[1]},
{"/$defs/~1~0", s.Defs["/~"]},
{"/$defs/~01", s.Defs["~1"]},
} {
got, err := dereferenceJSONPointer(s, tt.ptr)
if err != nil {
t.Fatal(err)
}
if got != tt.want {
t.Errorf("%s:\ngot %+v\nwant %+v", tt.ptr, got, tt.want)
}
}
}
func TestDerefernceJSONPointerErrors(t *testing.T) {
s := &Schema{
Type: "t",
Items: &Schema{},
Required: []string{"a"},
}
for _, tt := range []struct {
ptr string
want string // error must contain this string
}{
{"x", "does not begin"}, // parse error: no initial '/'
{"/minItems", "does not refer to a schema"},
{"/minItems/x", "navigated to nil"},
{"/required/-", "not supported"},
{"/required/01", "leading zeroes"},
{"/required/x", "invalid int"},
{"/required/1", "out of bounds"},
{"/properties/x", "no key"},
} {
_, err := dereferenceJSONPointer(s, tt.ptr)
if err == nil {
t.Errorf("%q: succeeded, want failure", tt.ptr)
} else if !strings.Contains(err.Error(), tt.want) {
t.Errorf("%q: error is %q, which does not contain %q", tt.ptr, err, tt.want)
}
}
}

View file

@ -1,45 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://json-schema.org/draft/2020-12/meta/applicator",
"$dynamicAnchor": "meta",
"title": "Applicator vocabulary meta-schema",
"type": ["object", "boolean"],
"properties": {
"prefixItems": { "$ref": "#/$defs/schemaArray" },
"items": { "$dynamicRef": "#meta" },
"contains": { "$dynamicRef": "#meta" },
"additionalProperties": { "$dynamicRef": "#meta" },
"properties": {
"type": "object",
"additionalProperties": { "$dynamicRef": "#meta" },
"default": {}
},
"patternProperties": {
"type": "object",
"additionalProperties": { "$dynamicRef": "#meta" },
"propertyNames": { "format": "regex" },
"default": {}
},
"dependentSchemas": {
"type": "object",
"additionalProperties": { "$dynamicRef": "#meta" },
"default": {}
},
"propertyNames": { "$dynamicRef": "#meta" },
"if": { "$dynamicRef": "#meta" },
"then": { "$dynamicRef": "#meta" },
"else": { "$dynamicRef": "#meta" },
"allOf": { "$ref": "#/$defs/schemaArray" },
"anyOf": { "$ref": "#/$defs/schemaArray" },
"oneOf": { "$ref": "#/$defs/schemaArray" },
"not": { "$dynamicRef": "#meta" }
},
"$defs": {
"schemaArray": {
"type": "array",
"minItems": 1,
"items": { "$dynamicRef": "#meta" }
}
}
}

View file

@ -1,14 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://json-schema.org/draft/2020-12/meta/content",
"$dynamicAnchor": "meta",
"title": "Content vocabulary meta-schema",
"type": ["object", "boolean"],
"properties": {
"contentEncoding": { "type": "string" },
"contentMediaType": { "type": "string" },
"contentSchema": { "$dynamicRef": "#meta" }
}
}

View file

@ -1,48 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://json-schema.org/draft/2020-12/meta/core",
"$dynamicAnchor": "meta",
"title": "Core vocabulary meta-schema",
"type": ["object", "boolean"],
"properties": {
"$id": {
"$ref": "#/$defs/uriReferenceString",
"$comment": "Non-empty fragments not allowed.",
"pattern": "^[^#]*#?$"
},
"$schema": { "$ref": "#/$defs/uriString" },
"$ref": { "$ref": "#/$defs/uriReferenceString" },
"$anchor": { "$ref": "#/$defs/anchorString" },
"$dynamicRef": { "$ref": "#/$defs/uriReferenceString" },
"$dynamicAnchor": { "$ref": "#/$defs/anchorString" },
"$vocabulary": {
"type": "object",
"propertyNames": { "$ref": "#/$defs/uriString" },
"additionalProperties": {
"type": "boolean"
}
},
"$comment": {
"type": "string"
},
"$defs": {
"type": "object",
"additionalProperties": { "$dynamicRef": "#meta" }
}
},
"$defs": {
"anchorString": {
"type": "string",
"pattern": "^[A-Za-z_][-A-Za-z0-9._]*$"
},
"uriString": {
"type": "string",
"format": "uri"
},
"uriReferenceString": {
"type": "string",
"format": "uri-reference"
}
}
}

View file

@ -1,11 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://json-schema.org/draft/2020-12/meta/format-annotation",
"$dynamicAnchor": "meta",
"title": "Format vocabulary meta-schema for annotation results",
"type": ["object", "boolean"],
"properties": {
"format": { "type": "string" }
}
}

View file

@ -1,34 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://json-schema.org/draft/2020-12/meta/meta-data",
"$dynamicAnchor": "meta",
"title": "Meta-data vocabulary meta-schema",
"type": ["object", "boolean"],
"properties": {
"title": {
"type": "string"
},
"description": {
"type": "string"
},
"default": true,
"deprecated": {
"type": "boolean",
"default": false
},
"readOnly": {
"type": "boolean",
"default": false
},
"writeOnly": {
"type": "boolean",
"default": false
},
"examples": {
"type": "array",
"items": true
}
}
}

View file

@ -1,12 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://json-schema.org/draft/2020-12/meta/unevaluated",
"$dynamicAnchor": "meta",
"title": "Unevaluated applicator vocabulary meta-schema",
"type": ["object", "boolean"],
"properties": {
"unevaluatedItems": { "$dynamicRef": "#meta" },
"unevaluatedProperties": { "$dynamicRef": "#meta" }
}
}

View file

@ -1,95 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://json-schema.org/draft/2020-12/meta/validation",
"$dynamicAnchor": "meta",
"title": "Validation vocabulary meta-schema",
"type": ["object", "boolean"],
"properties": {
"type": {
"anyOf": [
{ "$ref": "#/$defs/simpleTypes" },
{
"type": "array",
"items": { "$ref": "#/$defs/simpleTypes" },
"minItems": 1,
"uniqueItems": true
}
]
},
"const": true,
"enum": {
"type": "array",
"items": true
},
"multipleOf": {
"type": "number",
"exclusiveMinimum": 0
},
"maximum": {
"type": "number"
},
"exclusiveMaximum": {
"type": "number"
},
"minimum": {
"type": "number"
},
"exclusiveMinimum": {
"type": "number"
},
"maxLength": { "$ref": "#/$defs/nonNegativeInteger" },
"minLength": { "$ref": "#/$defs/nonNegativeIntegerDefault0" },
"pattern": {
"type": "string",
"format": "regex"
},
"maxItems": { "$ref": "#/$defs/nonNegativeInteger" },
"minItems": { "$ref": "#/$defs/nonNegativeIntegerDefault0" },
"uniqueItems": {
"type": "boolean",
"default": false
},
"maxContains": { "$ref": "#/$defs/nonNegativeInteger" },
"minContains": {
"$ref": "#/$defs/nonNegativeInteger",
"default": 1
},
"maxProperties": { "$ref": "#/$defs/nonNegativeInteger" },
"minProperties": { "$ref": "#/$defs/nonNegativeIntegerDefault0" },
"required": { "$ref": "#/$defs/stringArray" },
"dependentRequired": {
"type": "object",
"additionalProperties": {
"$ref": "#/$defs/stringArray"
}
}
},
"$defs": {
"nonNegativeInteger": {
"type": "integer",
"minimum": 0
},
"nonNegativeIntegerDefault0": {
"$ref": "#/$defs/nonNegativeInteger",
"default": 0
},
"simpleTypes": {
"enum": [
"array",
"boolean",
"integer",
"null",
"number",
"object",
"string"
]
},
"stringArray": {
"type": "array",
"items": { "type": "string" },
"uniqueItems": true,
"default": []
}
}
}

View file

@ -1,58 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://json-schema.org/draft/2020-12/schema",
"$vocabulary": {
"https://json-schema.org/draft/2020-12/vocab/core": true,
"https://json-schema.org/draft/2020-12/vocab/applicator": true,
"https://json-schema.org/draft/2020-12/vocab/unevaluated": true,
"https://json-schema.org/draft/2020-12/vocab/validation": true,
"https://json-schema.org/draft/2020-12/vocab/meta-data": true,
"https://json-schema.org/draft/2020-12/vocab/format-annotation": true,
"https://json-schema.org/draft/2020-12/vocab/content": true
},
"$dynamicAnchor": "meta",
"title": "Core and Validation specifications meta-schema",
"allOf": [
{"$ref": "meta/core"},
{"$ref": "meta/applicator"},
{"$ref": "meta/unevaluated"},
{"$ref": "meta/validation"},
{"$ref": "meta/meta-data"},
{"$ref": "meta/format-annotation"},
{"$ref": "meta/content"}
],
"type": ["object", "boolean"],
"$comment": "This meta-schema also defines keywords that have appeared in previous drafts in order to prevent incompatible extensions as they remain in common use.",
"properties": {
"definitions": {
"$comment": "\"definitions\" has been replaced by \"$defs\".",
"type": "object",
"additionalProperties": { "$dynamicRef": "#meta" },
"deprecated": true,
"default": {}
},
"dependencies": {
"$comment": "\"dependencies\" has been split and replaced by \"dependentSchemas\" and \"dependentRequired\" in order to serve their differing semantics.",
"type": "object",
"additionalProperties": {
"anyOf": [
{ "$dynamicRef": "#meta" },
{ "$ref": "meta/validation#/$defs/stringArray" }
]
},
"deprecated": true,
"default": {}
},
"$recursiveAnchor": {
"$comment": "\"$recursiveAnchor\" has been replaced by \"$dynamicAnchor\".",
"$ref": "meta/core#/$defs/anchorString",
"deprecated": true
},
"$recursiveRef": {
"$comment": "\"$recursiveRef\" has been replaced by \"$dynamicRef\".",
"$ref": "meta/core#/$defs/uriReferenceString",
"deprecated": true
}
}
}

View file

@ -1,548 +0,0 @@
// 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.
// This file deals with preparing a schema for validation, including various checks,
// optimizations, and the resolution of cross-schema references.
package jsonschema
import (
"errors"
"fmt"
"net/url"
"reflect"
"regexp"
"strings"
)
// A Resolved consists of a [Schema] along with associated information needed to
// validate documents against it.
// A Resolved has been validated against its meta-schema, and all its references
// (the $ref and $dynamicRef keywords) have been resolved to their referenced Schemas.
// Call [Schema.Resolve] to obtain a Resolved from a Schema.
type Resolved struct {
root *Schema
// map from $ids to their schemas
resolvedURIs map[string]*Schema
// map from schemas to additional info computed during resolution
resolvedInfos map[*Schema]*resolvedInfo
}
func newResolved(s *Schema) *Resolved {
return &Resolved{
root: s,
resolvedURIs: map[string]*Schema{},
resolvedInfos: map[*Schema]*resolvedInfo{},
}
}
// resolvedInfo holds information specific to a schema that is computed by [Schema.Resolve].
type resolvedInfo struct {
s *Schema
// The JSON Pointer path from the root schema to here.
// Used in errors.
path string
// The schema's base schema.
// If the schema is the root or has an ID, its base is itself.
// Otherwise, its base is the innermost enclosing schema whose base
// is itself.
// Intuitively, a base schema is one that can be referred to with a
// fragmentless URI.
base *Schema
// The URI for the schema, if it is the root or has an ID.
// Otherwise nil.
// Invariants:
// s.base.uri != nil.
// s.base == s <=> s.uri != nil
uri *url.URL
// The schema to which Ref refers.
resolvedRef *Schema
// If the schema has a dynamic ref, exactly one of the next two fields
// will be non-zero after successful resolution.
// The schema to which the dynamic ref refers when it acts lexically.
resolvedDynamicRef *Schema
// The anchor to look up on the stack when the dynamic ref acts dynamically.
dynamicRefAnchor string
// The following fields are independent of arguments to Schema.Resolved,
// so they could live on the Schema. We put them here for simplicity.
// The set of required properties.
isRequired map[string]bool
// Compiled regexps.
pattern *regexp.Regexp
patternProperties map[*regexp.Regexp]*Schema
// Map from anchors to subschemas.
anchors map[string]anchorInfo
}
// Schema returns the schema that was resolved.
// It must not be modified.
func (r *Resolved) Schema() *Schema { return r.root }
// schemaString returns a short string describing the schema.
func (r *Resolved) schemaString(s *Schema) string {
if s.ID != "" {
return s.ID
}
info := r.resolvedInfos[s]
if info.path != "" {
return info.path
}
return "<anonymous schema>"
}
// A Loader reads and unmarshals the schema at uri, if any.
type Loader func(uri *url.URL) (*Schema, error)
// ResolveOptions are options for [Schema.Resolve].
type ResolveOptions struct {
// BaseURI is the URI relative to which the root schema should be resolved.
// If non-empty, must be an absolute URI (one that starts with a scheme).
// It is resolved (in the URI sense; see [url.ResolveReference]) with root's
// $id property.
// If the resulting URI is not absolute, then the schema cannot contain
// relative URI references.
BaseURI string
// Loader loads schemas that are referred to by a $ref but are not under the
// root schema (remote references).
// If nil, resolving a remote reference will return an error.
Loader Loader
// ValidateDefaults determines whether to validate values of "default" keywords
// against their schemas.
// The [JSON Schema specification] does not require this, but it is
// recommended if defaults will be used.
//
// [JSON Schema specification]: https://json-schema.org/understanding-json-schema/reference/annotations
ValidateDefaults bool
}
// Resolve resolves all references within the schema and performs other tasks that
// prepare the schema for validation.
// If opts is nil, the default values are used.
// The schema must not be changed after Resolve is called.
// The same schema may be resolved multiple times.
func (root *Schema) Resolve(opts *ResolveOptions) (*Resolved, error) {
// There are up to five steps required to prepare a schema to validate.
// 1. Load: read the schema from somewhere and unmarshal it.
// This schema (root) may have been loaded or created in memory, but other schemas that
// come into the picture in step 4 will be loaded by the given loader.
// 2. Check: validate the schema against a meta-schema, and perform other well-formedness checks.
// Precompute some values along the way.
// 3. Resolve URIs: determine the base URI of the root and all its subschemas, and
// resolve (in the URI sense) all identifiers and anchors with their bases. This step results
// in a map from URIs to schemas within root.
// 4. Resolve references: all refs in the schemas are replaced with the schema they refer to.
// 5. (Optional.) If opts.ValidateDefaults is true, validate the defaults.
r := &resolver{loaded: map[string]*Resolved{}}
if opts != nil {
r.opts = *opts
}
var base *url.URL
if r.opts.BaseURI == "" {
base = &url.URL{} // so we can call ResolveReference on it
} else {
var err error
base, err = url.Parse(r.opts.BaseURI)
if err != nil {
return nil, fmt.Errorf("parsing base URI: %w", err)
}
}
if r.opts.Loader == nil {
r.opts.Loader = func(uri *url.URL) (*Schema, error) {
return nil, errors.New("cannot resolve remote schemas: no loader passed to Schema.Resolve")
}
}
resolved, err := r.resolve(root, base)
if err != nil {
return nil, err
}
if r.opts.ValidateDefaults {
if err := resolved.validateDefaults(); err != nil {
return nil, err
}
}
// TODO: before we return, throw away anything we don't need for validation.
return resolved, nil
}
// A resolver holds the state for resolution.
type resolver struct {
opts ResolveOptions
// A cache of loaded and partly resolved schemas. (They may not have had their
// refs resolved.) The cache ensures that the loader will never be called more
// than once with the same URI, and that reference cycles are handled properly.
loaded map[string]*Resolved
}
func (r *resolver) resolve(s *Schema, baseURI *url.URL) (*Resolved, error) {
if baseURI.Fragment != "" {
return nil, fmt.Errorf("base URI %s must not have a fragment", baseURI)
}
rs := newResolved(s)
if err := s.check(rs.resolvedInfos); err != nil {
return nil, err
}
if err := resolveURIs(rs, baseURI); err != nil {
return nil, err
}
// Remember the schema by both the URI we loaded it from and its canonical name,
// which may differ if the schema has an $id.
// We must set the map before calling resolveRefs, or ref cycles will cause unbounded recursion.
r.loaded[baseURI.String()] = rs
r.loaded[rs.resolvedInfos[s].uri.String()] = rs
if err := r.resolveRefs(rs); err != nil {
return nil, err
}
return rs, nil
}
func (root *Schema) check(infos map[*Schema]*resolvedInfo) error {
// Check for structural validity. Do this first and fail fast:
// bad structure will cause other code to panic.
if err := root.checkStructure(infos); err != nil {
return err
}
var errs []error
report := func(err error) { errs = append(errs, err) }
for ss := range root.all() {
ss.checkLocal(report, infos)
}
return errors.Join(errs...)
}
// checkStructure verifies that root and its subschemas form a tree.
// It also assigns each schema a unique path, to improve error messages.
func (root *Schema) checkStructure(infos map[*Schema]*resolvedInfo) error {
assert(len(infos) == 0, "non-empty infos")
var check func(reflect.Value, []byte) error
check = func(v reflect.Value, path []byte) error {
// For the purpose of error messages, the root schema has path "root"
// and other schemas' paths are their JSON Pointer from the root.
p := "root"
if len(path) > 0 {
p = string(path)
}
s := v.Interface().(*Schema)
if s == nil {
return fmt.Errorf("jsonschema: schema at %s is nil", p)
}
if info, ok := infos[s]; ok {
// We've seen s before.
// The schema graph at root is not a tree, but it needs to
// be because a schema's base must be unique.
// A cycle would also put Schema.all into an infinite recursion.
return fmt.Errorf("jsonschema: schemas at %s do not form a tree; %s appears more than once (also at %s)",
root, info.path, p)
}
infos[s] = &resolvedInfo{s: s, path: p}
for _, info := range schemaFieldInfos {
fv := v.Elem().FieldByIndex(info.sf.Index)
switch info.sf.Type {
case schemaType:
// A field that contains an individual schema.
// A nil is valid: it just means the field isn't present.
if !fv.IsNil() {
if err := check(fv, fmt.Appendf(path, "/%s", info.jsonName)); err != nil {
return err
}
}
case schemaSliceType:
for i := range fv.Len() {
if err := check(fv.Index(i), fmt.Appendf(path, "/%s/%d", info.jsonName, i)); err != nil {
return err
}
}
case schemaMapType:
iter := fv.MapRange()
for iter.Next() {
key := escapeJSONPointerSegment(iter.Key().String())
if err := check(iter.Value(), fmt.Appendf(path, "/%s/%s", info.jsonName, key)); err != nil {
return err
}
}
}
}
return nil
}
return check(reflect.ValueOf(root), make([]byte, 0, 256))
}
// checkLocal checks s for validity, independently of other schemas it may refer to.
// Since checking a regexp involves compiling it, checkLocal saves those compiled regexps
// in the schema for later use.
// It appends the errors it finds to errs.
func (s *Schema) checkLocal(report func(error), infos map[*Schema]*resolvedInfo) {
addf := func(format string, args ...any) {
msg := fmt.Sprintf(format, args...)
report(fmt.Errorf("jsonschema.Schema: %s: %s", s, msg))
}
if s == nil {
addf("nil subschema")
return
}
if err := s.basicChecks(); err != nil {
report(err)
return
}
// 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")
}
info := infos[s]
// Check and compile regexps.
if s.Pattern != "" {
re, err := regexp.Compile(s.Pattern)
if err != nil {
addf("pattern: %v", err)
} else {
info.pattern = re
}
}
if len(s.PatternProperties) > 0 {
info.patternProperties = map[*regexp.Regexp]*Schema{}
for reString, subschema := range s.PatternProperties {
re, err := regexp.Compile(reString)
if err != nil {
addf("patternProperties[%q]: %v", reString, err)
continue
}
info.patternProperties[re] = subschema
}
}
// Build a set of required properties, to avoid quadratic behavior when validating
// a struct.
if len(s.Required) > 0 {
info.isRequired = map[string]bool{}
for _, r := range s.Required {
info.isRequired[r] = true
}
}
}
// resolveURIs resolves the ids and anchors in all the schemas of root, relative
// to baseURI.
// See https://json-schema.org/draft/2020-12/json-schema-core#section-8.2, section
// 8.2.1.
//
// Every schema has a base URI and a parent base URI.
//
// The parent base URI is the base URI of the lexically enclosing schema, or for
// a root schema, the URI it was loaded from or the one supplied to [Schema.Resolve].
//
// If the schema has no $id property, the base URI of a schema is that of its parent.
// If the schema does have an $id, it must be a URI, possibly relative. The schema's
// base URI is the $id resolved (in the sense of [url.URL.ResolveReference]) against
// the parent base.
//
// As an example, consider this schema loaded from http://a.com/root.json (quotes omitted):
//
// {
// allOf: [
// {$id: "sub1.json", minLength: 5},
// {$id: "http://b.com", minimum: 10},
// {not: {maximum: 20}}
// ]
// }
//
// The base URIs are as follows. Schema locations are expressed in the JSON Pointer notation.
//
// schema base URI
// root http://a.com/root.json
// allOf/0 http://a.com/sub1.json
// allOf/1 http://b.com (absolute $id; doesn't matter that it's not under the loaded URI)
// allOf/2 http://a.com/root.json (inherited from parent)
// allOf/2/not http://a.com/root.json (inherited from parent)
func resolveURIs(rs *Resolved, baseURI *url.URL) error {
var resolve func(s, base *Schema) error
resolve = func(s, base *Schema) error {
info := rs.resolvedInfos[s]
baseInfo := rs.resolvedInfos[base]
// ids are scoped to the root.
if s.ID != "" {
// A non-empty ID establishes a new base.
idURI, err := url.Parse(s.ID)
if err != nil {
return err
}
if idURI.Fragment != "" {
return fmt.Errorf("$id %s must not have a fragment", s.ID)
}
// The base URI for this schema is its $id resolved against the parent base.
info.uri = baseInfo.uri.ResolveReference(idURI)
if !info.uri.IsAbs() {
return fmt.Errorf("$id %s does not resolve to an absolute URI (base is %q)", s.ID, baseInfo.uri)
}
rs.resolvedURIs[info.uri.String()] = s
base = s // needed for anchors
baseInfo = rs.resolvedInfos[base]
}
info.base = base
// Anchors and dynamic anchors are URI fragments that are scoped to their base.
// We treat them as keys in a map stored within the schema.
setAnchor := func(anchor string, dynamic bool) error {
if anchor != "" {
if _, ok := baseInfo.anchors[anchor]; ok {
return fmt.Errorf("duplicate anchor %q in %s", anchor, baseInfo.uri)
}
if baseInfo.anchors == nil {
baseInfo.anchors = map[string]anchorInfo{}
}
baseInfo.anchors[anchor] = anchorInfo{s, dynamic}
}
return nil
}
setAnchor(s.Anchor, false)
setAnchor(s.DynamicAnchor, true)
for c := range s.children() {
if err := resolve(c, base); err != nil {
return err
}
}
return nil
}
// Set the root URI to the base for now. If the root has an $id, this will change.
rs.resolvedInfos[rs.root].uri = baseURI
// The original base, even if changed, is still a valid way to refer to the root.
rs.resolvedURIs[baseURI.String()] = rs.root
return resolve(rs.root, rs.root)
}
// resolveRefs replaces every ref in the schemas with the schema it refers to.
// A reference that doesn't resolve within the schema may refer to some other schema
// that needs to be loaded.
func (r *resolver) resolveRefs(rs *Resolved) error {
for s := range rs.root.all() {
info := rs.resolvedInfos[s]
if s.Ref != "" {
refSchema, _, err := r.resolveRef(rs, s, s.Ref)
if err != nil {
return err
}
// Whether or not the anchor referred to by $ref fragment is dynamic,
// the ref still treats it lexically.
info.resolvedRef = refSchema
}
if s.DynamicRef != "" {
refSchema, frag, err := r.resolveRef(rs, s, s.DynamicRef)
if err != nil {
return err
}
if frag != "" {
// The dynamic ref's fragment points to a dynamic anchor.
// We must resolve the fragment at validation time.
info.dynamicRefAnchor = frag
} else {
// There is no dynamic anchor in the lexically referenced schema,
// so the dynamic ref behaves like a lexical ref.
info.resolvedDynamicRef = refSchema
}
}
}
return nil
}
// resolveRef resolves the reference ref, which is either s.Ref or s.DynamicRef.
func (r *resolver) resolveRef(rs *Resolved, s *Schema, ref string) (_ *Schema, dynamicFragment string, err error) {
refURI, err := url.Parse(ref)
if err != nil {
return nil, "", err
}
// URI-resolve the ref against the current base URI to get a complete URI.
base := rs.resolvedInfos[s].base
refURI = rs.resolvedInfos[base].uri.ResolveReference(refURI)
// The non-fragment part of a ref URI refers to the base URI of some schema.
// This part is the same for dynamic refs too: their non-fragment part resolves
// lexically.
u := *refURI
u.Fragment = ""
fraglessRefURI := &u
// Look it up locally.
referencedSchema := rs.resolvedURIs[fraglessRefURI.String()]
if referencedSchema == nil {
// The schema is remote. Maybe we've already loaded it.
// We assume that the non-fragment part of refURI refers to a top-level schema
// document. That is, we don't support the case exemplified by
// http://foo.com/bar.json/baz, where the document is in bar.json and
// the reference points to a subschema within it.
// TODO: support that case.
if lrs := r.loaded[fraglessRefURI.String()]; lrs != nil {
referencedSchema = lrs.root
} else {
// Try to load the schema.
ls, err := r.opts.Loader(fraglessRefURI)
if err != nil {
return nil, "", fmt.Errorf("loading %s: %w", fraglessRefURI, err)
}
lrs, err := r.resolve(ls, fraglessRefURI)
if err != nil {
return nil, "", err
}
referencedSchema = lrs.root
assert(referencedSchema != nil, "nil referenced schema")
// Copy the resolvedInfos from lrs into rs, without overwriting
// (hence we can't use maps.Insert).
for s, i := range lrs.resolvedInfos {
if rs.resolvedInfos[s] == nil {
rs.resolvedInfos[s] = i
}
}
}
}
frag := refURI.Fragment
// Look up frag in refSchema.
// frag is either a JSON Pointer or the name of an anchor.
// A JSON Pointer is either the empty string or begins with a '/',
// whereas anchors are always non-empty strings that don't contain slashes.
if frag != "" && !strings.HasPrefix(frag, "/") {
resInfo := rs.resolvedInfos[referencedSchema]
info, found := resInfo.anchors[frag]
if !found {
return nil, "", fmt.Errorf("no anchor %q in %s", frag, s)
}
if info.dynamic {
dynamicFragment = frag
}
return info.schema, dynamicFragment, nil
}
// frag is a JSON Pointer.
s, err = dereferenceJSONPointer(referencedSchema, frag)
return s, "", err
}

View file

@ -1,218 +0,0 @@
// 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 (
"errors"
"maps"
"net/url"
"regexp"
"slices"
"strings"
"testing"
)
func TestSchemaStructure(t *testing.T) {
check := func(s *Schema, want string) {
t.Helper()
infos := map[*Schema]*resolvedInfo{}
err := s.checkStructure(infos)
if err == nil || !strings.Contains(err.Error(), want) {
t.Errorf("checkStructure returned error %q, want %q", err, want)
}
}
dag := &Schema{Type: "number"}
dag = &Schema{Items: dag, Contains: dag}
check(dag, "do not form a tree")
tree := &Schema{Type: "number"}
tree.Items = tree
check(tree, "do not form a tree")
sliceNil := &Schema{PrefixItems: []*Schema{nil}}
check(sliceNil, "is nil")
sliceMap := &Schema{Properties: map[string]*Schema{"a": nil}}
check(sliceMap, "is nil")
}
func TestCheckLocal(t *testing.T) {
for _, tt := range []struct {
s *Schema
want string // error must be non-nil and match this regexp
}{
{
&Schema{Pattern: "]["},
"regexp",
},
{
&Schema{PatternProperties: map[string]*Schema{"*": {}}},
"regexp",
},
} {
_, err := tt.s.Resolve(nil)
if err == nil {
t.Errorf("%s: unexpectedly passed", tt.s.json())
continue
}
if !regexp.MustCompile(tt.want).MatchString(err.Error()) {
t.Errorf("checkLocal returned error\n%q\nwanted it to match\n%s\nregexp: %s",
tt.s.json(), err, tt.want)
}
}
}
func TestPaths(t *testing.T) {
// CheckStructure should assign paths to schemas.
// This test also verifies that Schema.all visits maps in sorted order.
root := &Schema{
Type: "string",
PrefixItems: []*Schema{{Type: "int"}, {Items: &Schema{Type: "null"}}},
Contains: &Schema{Properties: map[string]*Schema{
"~1": {Type: "boolean"},
"p": {},
}},
}
type item struct {
s *Schema
p string
}
want := []item{
{root, "root"},
{root.Contains, "/contains"},
{root.Contains.Properties["p"], "/contains/properties/p"},
{root.Contains.Properties["~1"], "/contains/properties/~01"},
{root.PrefixItems[0], "/prefixItems/0"},
{root.PrefixItems[1], "/prefixItems/1"},
{root.PrefixItems[1].Items, "/prefixItems/1/items"},
}
rs := newResolved(root)
if err := root.checkStructure(rs.resolvedInfos); err != nil {
t.Fatal(err)
}
var got []item
for s := range root.all() {
got = append(got, item{s, rs.resolvedInfos[s].path})
}
if !slices.Equal(got, want) {
t.Errorf("\ngot %v\nwant %v", got, want)
}
}
func TestResolveURIs(t *testing.T) {
for _, baseURI := range []string{"", "http://a.com"} {
t.Run(baseURI, func(t *testing.T) {
root := &Schema{
ID: "http://b.com",
Items: &Schema{
ID: "/foo.json",
},
Contains: &Schema{
ID: "/bar.json",
Anchor: "a",
DynamicAnchor: "da",
Items: &Schema{
Anchor: "b",
Items: &Schema{
// An ID shouldn't be a query param, but this tests
// resolving an ID with its parent.
ID: "?items",
Anchor: "c",
},
},
},
}
base, err := url.Parse(baseURI)
if err != nil {
t.Fatal(err)
}
rs := newResolved(root)
if err := root.check(rs.resolvedInfos); err != nil {
t.Fatal(err)
}
if err := resolveURIs(rs, base); err != nil {
t.Fatal(err)
}
wantIDs := map[string]*Schema{
baseURI: root,
"http://b.com/foo.json": root.Items,
"http://b.com/bar.json": root.Contains,
"http://b.com/bar.json?items": root.Contains.Items.Items,
}
if baseURI != root.ID {
wantIDs[root.ID] = root
}
wantAnchors := map[*Schema]map[string]anchorInfo{
root.Contains: {
"a": anchorInfo{root.Contains, false},
"da": anchorInfo{root.Contains, true},
"b": anchorInfo{root.Contains.Items, false},
},
root.Contains.Items.Items: {
"c": anchorInfo{root.Contains.Items.Items, false},
},
}
got := rs.resolvedURIs
gotKeys := slices.Sorted(maps.Keys(got))
wantKeys := slices.Sorted(maps.Keys(wantIDs))
if !slices.Equal(gotKeys, wantKeys) {
t.Errorf("ID keys:\ngot %q\nwant %q", gotKeys, wantKeys)
}
if !maps.Equal(got, wantIDs) {
t.Errorf("IDs:\ngot %+v\n\nwant %+v", got, wantIDs)
}
for s := range root.all() {
info := rs.resolvedInfos[s]
if want := wantAnchors[s]; want != nil {
if got := info.anchors; !maps.Equal(got, want) {
t.Errorf("anchors:\ngot %+v\n\nwant %+v", got, want)
}
} else if info.anchors != nil {
t.Errorf("non-nil anchors for %s", s)
}
}
})
}
}
func TestRefCycle(t *testing.T) {
// Verify that cycles of refs are OK.
// The test suite doesn't check this, surprisingly.
schemas := map[string]*Schema{
"root": {Ref: "a"},
"a": {Ref: "b"},
"b": {Ref: "a"},
}
loader := func(uri *url.URL) (*Schema, error) {
s, ok := schemas[uri.Path[1:]]
if !ok {
return nil, errors.New("not found")
}
return s, nil
}
rs, err := schemas["root"].Resolve(&ResolveOptions{Loader: loader})
if err != nil {
t.Fatal(err)
}
check := func(s *Schema, key string) {
t.Helper()
if rs.resolvedInfos[s].resolvedRef != schemas[key] {
t.Errorf("%s resolvedRef != schemas[%q]", s.json(), key)
}
}
check(rs.root, "a")
check(schemas["a"], "b")
check(schemas["b"], "a")
}

Some files were not shown because too many files have changed in this diff Show more