diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f2f8cb8e..54b36331 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,11 +1,11 @@ name: Test on: # Manual trigger - workflow_dispatch: + workflow_dispatch: push: branches: main pull_request: - + permissions: contents: read @@ -13,10 +13,10 @@ jobs: lint: runs-on: ubuntu-latest steps: - - name: Set up Go - uses: actions/setup-go@v5 - name: Check out code uses: actions/checkout@v4 + - name: Set up Go + uses: actions/setup-go@v5 - name: Check formatting run: | unformatted=$(gofmt -l .) @@ -26,18 +26,30 @@ jobs: exit 1 fi echo "All Go files are properly formatted" - + test: runs-on: ubuntu-latest strategy: matrix: - go: [ '1.23', '1.24' ] + go: [ '1.23', '1.24', '1.25.0-rc.3' ] steps: + - name: Check out code + uses: actions/checkout@v4 - name: Set up Go uses: actions/setup-go@v5 with: go-version: ${{ matrix.go }} - - name: Check out code - uses: actions/checkout@v4 - name: Test run: go test -v ./... + + race-test: + runs-on: ubuntu-latest + steps: + - name: Check out code + uses: actions/checkout@v4 + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: '1.24' + - name: Test with -race + run: go test -v -race ./... diff --git a/auth/auth.go b/auth/auth.go new file mode 100644 index 00000000..68873b48 --- /dev/null +++ b/auth/auth.go @@ -0,0 +1,96 @@ +// 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 auth + +import ( + "context" + "errors" + "net/http" + "slices" + "strings" + "time" +) + +type TokenInfo struct { + Scopes []string + Expiration time.Time +} + +type TokenVerifier func(ctx context.Context, token string) (*TokenInfo, error) + +type RequireBearerTokenOptions struct { + Scopes []string + ResourceMetadataURL string +} + +var ErrInvalidToken = errors.New("invalid token") + +type tokenInfoKey struct{} + +// RequireBearerToken returns a piece of middleware that verifies a bearer token using the verifier. +// If verification succeeds, the [TokenInfo] is added to the request's context and the request proceeds. +// If verification fails, the request fails with a 401 Unauthenticated, and the WWW-Authenticate header +// is populated to enable [protected resource metadata]. +// +// [protected resource metadata]: https://datatracker.ietf.org/doc/rfc9728 +func RequireBearerToken(verifier TokenVerifier, opts *RequireBearerTokenOptions) func(http.Handler) http.Handler { + // Based on typescript-sdk/src/server/auth/middleware/bearerAuth.ts. + + return func(handler http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + tokenInfo, errmsg, code := verify(r.Context(), verifier, opts, r.Header.Get("Authorization")) + if code != 0 { + if code == http.StatusUnauthorized || code == http.StatusForbidden { + if opts != nil && opts.ResourceMetadataURL != "" { + w.Header().Add("WWW-Authenticate", "Bearer resource_metadata="+opts.ResourceMetadataURL) + } + } + http.Error(w, errmsg, code) + return + } + r = r.WithContext(context.WithValue(r.Context(), tokenInfoKey{}, tokenInfo)) + handler.ServeHTTP(w, r) + }) + } +} + +func verify(ctx context.Context, verifier TokenVerifier, opts *RequireBearerTokenOptions, authHeader string) (_ *TokenInfo, errmsg string, code int) { + // Extract bearer token. + fields := strings.Fields(authHeader) + if len(fields) != 2 || strings.ToLower(fields[0]) != "bearer" { + return nil, "no bearer token", http.StatusUnauthorized + } + + // Verify the token and get information from it. + tokenInfo, err := verifier(ctx, fields[1]) + if err != nil { + if errors.Is(err, ErrInvalidToken) { + return nil, err.Error(), http.StatusUnauthorized + } + // TODO: the TS SDK distinguishes another error, OAuthError, and returns a 400. + // Investigate how that works. + // See typescript-sdk/src/server/auth/middleware/bearerAuth.ts. + return nil, err.Error(), http.StatusInternalServerError + } + + // Check scopes. + if opts != nil { + // Note: quadratic, but N is small. + for _, s := range opts.Scopes { + if !slices.Contains(tokenInfo.Scopes, s) { + return nil, "insufficient scope", http.StatusForbidden + } + } + } + + // Check expiration. + if tokenInfo.Expiration.IsZero() { + return nil, "token missing expiration", http.StatusUnauthorized + } + if tokenInfo.Expiration.Before(time.Now()) { + return nil, "token expired", http.StatusUnauthorized + } + return tokenInfo, "", 0 +} diff --git a/auth/auth_test.go b/auth/auth_test.go new file mode 100644 index 00000000..715b9bba --- /dev/null +++ b/auth/auth_test.go @@ -0,0 +1,70 @@ +// 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 auth + +import ( + "context" + "errors" + "testing" + "time" +) + +func TestVerify(t *testing.T) { + ctx := context.Background() + verifier := func(_ context.Context, token string) (*TokenInfo, error) { + switch token { + case "valid": + return &TokenInfo{Expiration: time.Now().Add(time.Hour)}, nil + case "invalid": + return nil, ErrInvalidToken + case "noexp": + return &TokenInfo{}, nil + case "expired": + return &TokenInfo{Expiration: time.Now().Add(-time.Hour)}, nil + default: + return nil, errors.New("unknown") + } + } + + for _, tt := range []struct { + name string + opts *RequireBearerTokenOptions + header string + wantMsg string + wantCode int + }{ + { + "valid", nil, "Bearer valid", + "", 0, + }, + { + "bad header", nil, "Barer valid", + "no bearer token", 401, + }, + { + "invalid", nil, "bearer invalid", + "invalid token", 401, + }, + { + "no expiration", nil, "Bearer noexp", + "token missing expiration", 401, + }, + { + "expired", nil, "Bearer expired", + "token expired", 401, + }, + { + "missing scope", &RequireBearerTokenOptions{Scopes: []string{"s1"}}, "Bearer valid", + "insufficient scope", 403, + }, + } { + t.Run(tt.name, func(t *testing.T) { + _, gotMsg, gotCode := verify(ctx, verifier, tt.opts, tt.header) + if gotMsg != tt.wantMsg || gotCode != tt.wantCode { + t.Errorf("got (%q, %d), want (%q, %d)", gotMsg, gotCode, tt.wantMsg, tt.wantCode) + } + }) + } +} diff --git a/design/design.md b/design/design.md index 33dc3a63..93dc5521 100644 --- a/design/design.md +++ b/design/design.md @@ -456,8 +456,8 @@ func (s *Server) AddReceivingMiddleware(middleware ...Middleware[*ServerSession] As an example, this code adds server-side logging: ```go -func withLogging(h mcp.MethodHandler[*ServerSession]) mcp.MethodHandler[*ServerSession]{ - return func(ctx context.Context, s *mcp.ServerSession, method string, params any) (res any, err error) { +func withLogging(h mcp.MethodHandler[*mcp.ServerSession]) mcp.MethodHandler[*mcp.ServerSession]{ + return func(ctx context.Context, s *mcp.ServerSession, method string, params mcp.Params) (res mcp.Result, err error) { log.Printf("request: %s %v", method, params) defer func() { log.Printf("response: %v, %v", res, err) }() return h(ctx, s , method, params) diff --git a/examples/client/listfeatures/main.go b/examples/client/listfeatures/main.go new file mode 100644 index 00000000..caf21bfe --- /dev/null +++ b/examples/client/listfeatures/main.go @@ -0,0 +1,65 @@ +// 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. + +// The listfeatures command lists all features of a stdio MCP server. +// +// Usage: listfeatures [] +// +// For example: +// +// listfeatures go run github.com/modelcontextprotocol/go-sdk/examples/server/hello +// +// or +// +// listfeatures npx @modelcontextprotocol/server-everything +package main + +import ( + "context" + "flag" + "fmt" + "iter" + "log" + "os" + "os/exec" + + "github.com/modelcontextprotocol/go-sdk/mcp" +) + +func main() { + flag.Parse() + args := flag.Args() + if len(args) == 0 { + fmt.Fprintf(os.Stderr, "Usage: listfeatures []") + fmt.Fprintf(os.Stderr, "List all features for a stdio MCP server") + fmt.Fprintln(os.Stderr) + fmt.Fprintf(os.Stderr, "Example: listfeatures npx @modelcontextprotocol/server-everything") + os.Exit(2) + } + + ctx := context.Background() + cmd := exec.Command(args[0], args[1:]...) + client := mcp.NewClient(&mcp.Implementation{Name: "mcp-client", Version: "v1.0.0"}, nil) + cs, err := client.Connect(ctx, mcp.NewCommandTransport(cmd)) + if err != nil { + log.Fatal(err) + } + defer cs.Close() + + printSection("tools", cs.Tools(ctx, nil), func(t *mcp.Tool) string { return t.Name }) + printSection("resources", cs.Resources(ctx, nil), func(r *mcp.Resource) string { return r.Name }) + printSection("resource templates", cs.ResourceTemplates(ctx, nil), func(r *mcp.ResourceTemplate) string { return r.Name }) + printSection("prompts", cs.Prompts(ctx, nil), func(p *mcp.Prompt) string { return p.Name }) +} + +func printSection[T any](name string, features iter.Seq2[T, error], featName func(T) string) { + fmt.Printf("%s:\n", name) + for feat, err := range features { + if err != nil { + log.Fatal(err) + } + fmt.Printf("\t%s\n", featName(feat)) + } + fmt.Println() +} diff --git a/examples/completion/main.go b/examples/server/completion/main.go similarity index 100% rename from examples/completion/main.go rename to examples/server/completion/main.go diff --git a/examples/custom-transport/main.go b/examples/server/custom-transport/main.go similarity index 100% rename from examples/custom-transport/main.go rename to examples/server/custom-transport/main.go diff --git a/examples/hello/main.go b/examples/server/hello/main.go similarity index 100% rename from examples/hello/main.go rename to examples/server/hello/main.go diff --git a/examples/memory/kb.go b/examples/server/memory/kb.go similarity index 100% rename from examples/memory/kb.go rename to examples/server/memory/kb.go diff --git a/examples/memory/kb_test.go b/examples/server/memory/kb_test.go similarity index 100% rename from examples/memory/kb_test.go rename to examples/server/memory/kb_test.go diff --git a/examples/memory/main.go b/examples/server/memory/main.go similarity index 100% rename from examples/memory/main.go rename to examples/server/memory/main.go diff --git a/examples/rate-limiting/go.mod b/examples/server/rate-limiting/go.mod similarity index 100% rename from examples/rate-limiting/go.mod rename to examples/server/rate-limiting/go.mod diff --git a/examples/rate-limiting/go.sum b/examples/server/rate-limiting/go.sum similarity index 100% rename from examples/rate-limiting/go.sum rename to examples/server/rate-limiting/go.sum diff --git a/examples/rate-limiting/main.go b/examples/server/rate-limiting/main.go similarity index 100% rename from examples/rate-limiting/main.go rename to examples/server/rate-limiting/main.go diff --git a/examples/sequentialthinking/README.md b/examples/server/sequentialthinking/README.md similarity index 100% rename from examples/sequentialthinking/README.md rename to examples/server/sequentialthinking/README.md diff --git a/examples/sequentialthinking/main.go b/examples/server/sequentialthinking/main.go similarity index 100% rename from examples/sequentialthinking/main.go rename to examples/server/sequentialthinking/main.go diff --git a/examples/sequentialthinking/main_test.go b/examples/server/sequentialthinking/main_test.go similarity index 100% rename from examples/sequentialthinking/main_test.go rename to examples/server/sequentialthinking/main_test.go diff --git a/examples/sse/main.go b/examples/server/sse/main.go similarity index 100% rename from examples/sse/main.go rename to examples/server/sse/main.go diff --git a/go.mod b/go.mod index 9bf8c151..17bddeb6 100644 --- a/go.mod +++ b/go.mod @@ -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 ) diff --git a/go.sum b/go.sum index 7d2f581d..a2edf9ad 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/internal/jsonrpc2/conn.go b/internal/jsonrpc2/conn.go index fbe0688b..6f48c9ba 100644 --- a/internal/jsonrpc2/conn.go +++ b/internal/jsonrpc2/conn.go @@ -65,8 +65,7 @@ type Connection struct { state inFlightState // accessed only in updateInFlight done chan struct{} // closed (under stateMu) when state.closed is true and all goroutines have completed - writer chan Writer // 1-buffered; stores the writer when not in use - + writer Writer handler Handler onInternalError func(error) @@ -214,12 +213,11 @@ func NewConnection(ctx context.Context, cfg ConnectionConfig) *Connection { c := &Connection{ state: inFlightState{closer: cfg.Closer}, done: make(chan struct{}), - writer: make(chan Writer, 1), + writer: cfg.Writer, onDone: cfg.OnDone, onInternalError: cfg.OnInternalError, } c.handler = cfg.Bind(c) - c.writer <- cfg.Writer c.start(ctx, cfg.Reader, cfg.Preempter) return c } @@ -239,7 +237,6 @@ func bindConnection(bindCtx context.Context, rwc io.ReadWriteCloser, binder Bind c := &Connection{ state: inFlightState{closer: rwc}, done: make(chan struct{}), - writer: make(chan Writer, 1), onDone: onDone, } // It's tempting to set a finalizer on c to verify that the state has gone @@ -259,7 +256,7 @@ func bindConnection(bindCtx context.Context, rwc io.ReadWriteCloser, binder Bind } c.onInternalError = options.OnInternalError - c.writer <- framer.Writer(rwc) + c.writer = framer.Writer(rwc) reader := framer.Reader(rwc) c.start(ctx, reader, options.Preempter) return c @@ -728,9 +725,7 @@ func (c *Connection) processResult(from any, req *incomingRequest, result any, e // write is used by all things that write outgoing messages, including replies. // it makes sure that writes are atomic func (c *Connection) write(ctx context.Context, msg Message) error { - writer := <-c.writer - defer func() { c.writer <- writer }() - err := writer.Write(ctx, msg) + err := c.writer.Write(ctx, msg) if err != nil && ctx.Err() == nil { // The call to Write failed, and since ctx.Err() is nil we can't attribute diff --git a/internal/jsonrpc2/frame.go b/internal/jsonrpc2/frame.go index d5bbe75b..46fcc9db 100644 --- a/internal/jsonrpc2/frame.go +++ b/internal/jsonrpc2/frame.go @@ -12,12 +12,14 @@ import ( "io" "strconv" "strings" + "sync" ) // Reader abstracts the transport mechanics from the JSON RPC protocol. // A Conn reads messages from the reader it was provided on construction, // and assumes that each call to Read fully transfers a single message, // or returns an error. +// // A reader is not safe for concurrent use, it is expected it will be used by // a single Conn in a safe manner. type Reader interface { @@ -29,8 +31,9 @@ type Reader interface { // A Conn writes messages using the writer it was provided on construction, // and assumes that each call to Write fully transfers a single message, // or returns an error. -// A writer is not safe for concurrent use, it is expected it will be used by -// a single Conn in a safe manner. +// +// A writer must be safe for concurrent use, as writes may occur concurrently +// in practice: libraries may make calls or respond to requests asynchronously. type Writer interface { // Write sends a message to the stream. Write(context.Context, Message) error @@ -62,7 +65,10 @@ func RawFramer() Framer { return rawFramer{} } type rawFramer struct{} type rawReader struct{ in *json.Decoder } -type rawWriter struct{ out io.Writer } +type rawWriter struct { + mu sync.Mutex + out io.Writer +} func (rawFramer) Reader(rw io.Reader) Reader { return &rawReader{in: json.NewDecoder(rw)} @@ -92,10 +98,14 @@ func (w *rawWriter) Write(ctx context.Context, msg Message) error { return ctx.Err() default: } + data, err := EncodeMessage(msg) if err != nil { return fmt.Errorf("marshaling message: %v", err) } + + w.mu.Lock() + defer w.mu.Unlock() _, err = w.out.Write(data) return err } @@ -107,7 +117,10 @@ func HeaderFramer() Framer { return headerFramer{} } type headerFramer struct{} type headerReader struct{ in *bufio.Reader } -type headerWriter struct{ out io.Writer } +type headerWriter struct { + mu sync.Mutex + out io.Writer +} func (headerFramer) Reader(rw io.Reader) Reader { return &headerReader{in: bufio.NewReader(rw)} @@ -180,6 +193,9 @@ func (w *headerWriter) Write(ctx context.Context, msg Message) error { return ctx.Err() default: } + w.mu.Lock() + defer w.mu.Unlock() + data, err := EncodeMessage(msg) if err != nil { return fmt.Errorf("marshaling message: %v", err) diff --git a/internal/util/util.go b/internal/util/util.go index f8c5baf9..4b5c325f 100644 --- a/internal/util/util.go +++ b/internal/util/util.go @@ -8,9 +8,7 @@ import ( "cmp" "fmt" "iter" - "reflect" "slices" - "strings" ) // Helpers below are copied from gopls' moremaps package. @@ -38,43 +36,6 @@ func KeySlice[M ~map[K]V, K comparable, V any](m M) []K { return r } -type JSONInfo struct { - Omit bool // unexported or first tag element is "-" - Name string // Go field name or first tag element. Empty if Omit is true. - Settings map[string]bool // "omitempty", "omitzero", etc. -} - -// FieldJSONInfo reports information about how encoding/json -// handles the given struct field. -// If the field is unexported, JSONInfo.Omit is true and no other JSONInfo field -// is populated. -// If the field is exported and has no tag, then Name is the field's name and all -// other fields are false. -// Otherwise, the information is obtained from the tag. -func FieldJSONInfo(f reflect.StructField) JSONInfo { - if !f.IsExported() { - return JSONInfo{Omit: true} - } - info := JSONInfo{Name: f.Name} - if tag, ok := f.Tag.Lookup("json"); ok { - name, rest, found := strings.Cut(tag, ",") - // "-" means omit, but "-," means the name is "-" - if name == "-" && !found { - return JSONInfo{Omit: true} - } - if name != "" { - info.Name = name - } - if len(rest) > 0 { - info.Settings = map[string]bool{} - for _, s := range strings.Split(rest, ",") { - info.Settings[s] = true - } - } - } - return info -} - // Wrapf wraps *errp with the given formatted message if *errp is not nil. func Wrapf(errp *error, format string, args ...any) { if *errp != nil { diff --git a/internal/util/util_test.go b/internal/util/util_test.go deleted file mode 100644 index 6a2b8676..00000000 --- a/internal/util/util_test.go +++ /dev/null @@ -1,38 +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 util - -import ( - "reflect" - "testing" -) - -func TestJSONInfo(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 := []JSONInfo{ - {Name: "A"}, - {Name: "B"}, - {Omit: true}, - {Name: "-"}, - {Name: "echo"}, - {Name: "foxtrot", Settings: map[string]bool{"omitempty": true}}, - {Omit: true}, - } - tt := reflect.TypeFor[S]() - for i := range tt.NumField() { - got := FieldJSONInfo(tt.Field(i)) - if !reflect.DeepEqual(got, want[i]) { - t.Errorf("got %+v, want %+v", got, want[i]) - } - } -} diff --git a/jsonschema/LICENSE b/jsonschema/LICENSE deleted file mode 100644 index 508be926..00000000 --- a/jsonschema/LICENSE +++ /dev/null @@ -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. diff --git a/jsonschema/README.md b/jsonschema/README.md deleted file mode 100644 index f316bedd..00000000 --- a/jsonschema/README.md +++ /dev/null @@ -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. - diff --git a/jsonschema/annotations.go b/jsonschema/annotations.go deleted file mode 100644 index a7ede1c6..00000000 --- a/jsonschema/annotations.go +++ /dev/null @@ -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 -} diff --git a/jsonschema/doc.go b/jsonschema/doc.go deleted file mode 100644 index 0f0ba441..00000000 --- a/jsonschema/doc.go +++ /dev/null @@ -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 diff --git a/jsonschema/infer.go b/jsonschema/infer.go deleted file mode 100644 index 7b6b7e2b..00000000 --- a/jsonschema/infer.go +++ /dev/null @@ -1,234 +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" - "math/big" - "reflect" - "regexp" - "time" - - "github.com/modelcontextprotocol/go-sdk/internal/util" -) - -// 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 := make(map[reflect.Type]*Schema) - // Add types from the standard library that have MarshalJSON methods. - ss := &Schema{Type: "string"} - schemas[reflect.TypeFor[time.Time]()] = ss - schemas[reflect.TypeFor[slog.Level]()] = ss - schemas[reflect.TypeFor[big.Int]()] = &Schema{Types: []string{"null", "string"}} - schemas[reflect.TypeFor[big.Rat]()] = ss - schemas[reflect.TypeFor[big.Float]()] = ss - - // 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 -} - -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 := util.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 -} - -// Disallow jsonschema tag values beginning "WORD=", for future expansion. -var disallowedPrefixRegexp = regexp.MustCompile("^[^ \t\n]*=") diff --git a/jsonschema/infer_test.go b/jsonschema/infer_test.go deleted file mode 100644 index 1a0895b4..00000000 --- a/jsonschema/infer_test.go +++ /dev/null @@ -1,305 +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" - "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("ForType 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 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{}} -} diff --git a/jsonschema/json_pointer.go b/jsonschema/json_pointer.go deleted file mode 100644 index 7310b9b4..00000000 --- a/jsonschema/json_pointer.go +++ /dev/null @@ -1,148 +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" - - "github.com/modelcontextprotocol/go-sdk/internal/util" -) - -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 util.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{} -} diff --git a/jsonschema/json_pointer_test.go b/jsonschema/json_pointer_test.go deleted file mode 100644 index 54b84bed..00000000 --- a/jsonschema/json_pointer_test.go +++ /dev/null @@ -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) - } - } -} diff --git a/jsonschema/meta-schemas/draft2020-12/meta/applicator.json b/jsonschema/meta-schemas/draft2020-12/meta/applicator.json deleted file mode 100644 index f4775974..00000000 --- a/jsonschema/meta-schemas/draft2020-12/meta/applicator.json +++ /dev/null @@ -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" } - } - } -} diff --git a/jsonschema/meta-schemas/draft2020-12/meta/content.json b/jsonschema/meta-schemas/draft2020-12/meta/content.json deleted file mode 100644 index 76e3760d..00000000 --- a/jsonschema/meta-schemas/draft2020-12/meta/content.json +++ /dev/null @@ -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" } - } -} diff --git a/jsonschema/meta-schemas/draft2020-12/meta/core.json b/jsonschema/meta-schemas/draft2020-12/meta/core.json deleted file mode 100644 index 69186228..00000000 --- a/jsonschema/meta-schemas/draft2020-12/meta/core.json +++ /dev/null @@ -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" - } - } -} diff --git a/jsonschema/meta-schemas/draft2020-12/meta/format-annotation.json b/jsonschema/meta-schemas/draft2020-12/meta/format-annotation.json deleted file mode 100644 index 3479e669..00000000 --- a/jsonschema/meta-schemas/draft2020-12/meta/format-annotation.json +++ /dev/null @@ -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" } - } -} diff --git a/jsonschema/meta-schemas/draft2020-12/meta/meta-data.json b/jsonschema/meta-schemas/draft2020-12/meta/meta-data.json deleted file mode 100644 index 4049ab21..00000000 --- a/jsonschema/meta-schemas/draft2020-12/meta/meta-data.json +++ /dev/null @@ -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 - } - } -} diff --git a/jsonschema/meta-schemas/draft2020-12/meta/unevaluated.json b/jsonschema/meta-schemas/draft2020-12/meta/unevaluated.json deleted file mode 100644 index 93779e54..00000000 --- a/jsonschema/meta-schemas/draft2020-12/meta/unevaluated.json +++ /dev/null @@ -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" } - } -} diff --git a/jsonschema/meta-schemas/draft2020-12/meta/validation.json b/jsonschema/meta-schemas/draft2020-12/meta/validation.json deleted file mode 100644 index ebb75db7..00000000 --- a/jsonschema/meta-schemas/draft2020-12/meta/validation.json +++ /dev/null @@ -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": [] - } - } -} diff --git a/jsonschema/meta-schemas/draft2020-12/schema.json b/jsonschema/meta-schemas/draft2020-12/schema.json deleted file mode 100644 index d5e2d31c..00000000 --- a/jsonschema/meta-schemas/draft2020-12/schema.json +++ /dev/null @@ -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 - } - } -} diff --git a/jsonschema/resolve.go b/jsonschema/resolve.go deleted file mode 100644 index cc551e79..00000000 --- a/jsonschema/resolve.go +++ /dev/null @@ -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 "" -} - -// 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 -} diff --git a/jsonschema/resolve_test.go b/jsonschema/resolve_test.go deleted file mode 100644 index 36aa424b..00000000 --- a/jsonschema/resolve_test.go +++ /dev/null @@ -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") -} diff --git a/jsonschema/schema.go b/jsonschema/schema.go deleted file mode 100644 index 1d60de12..00000000 --- a/jsonschema/schema.go +++ /dev/null @@ -1,425 +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 ( - "bytes" - "cmp" - "encoding/json" - "errors" - "fmt" - "iter" - "maps" - "math" - "reflect" - "slices" - - "github.com/modelcontextprotocol/go-sdk/internal/util" -) - -// A Schema is a JSON schema object. -// It corresponds to the 2020-12 draft, as described in https://json-schema.org/draft/2020-12, -// specifically: -// - https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-01 -// - https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-01 -// -// A Schema value may have non-zero values for more than one field: -// all relevant non-zero fields are used for validation. -// There is one exception to provide more Go type-safety: the Type and Types fields -// are mutually exclusive. -// -// Since this struct is a Go representation of a JSON value, it inherits JSON's -// distinction between nil and empty. Nil slices and maps are considered absent, -// but empty ones are present and affect validation. For example, -// -// Schema{Enum: nil} -// -// is equivalent to an empty schema, so it validates every instance. But -// -// Schema{Enum: []any{}} -// -// requires equality to some slice element, so it vacuously rejects every instance. -type Schema struct { - // core - ID string `json:"$id,omitempty"` - Schema string `json:"$schema,omitempty"` - Ref string `json:"$ref,omitempty"` - Comment string `json:"$comment,omitempty"` - Defs map[string]*Schema `json:"$defs,omitempty"` - // definitions is deprecated but still allowed. It is a synonym for $defs. - Definitions map[string]*Schema `json:"definitions,omitempty"` - - Anchor string `json:"$anchor,omitempty"` - DynamicAnchor string `json:"$dynamicAnchor,omitempty"` - DynamicRef string `json:"$dynamicRef,omitempty"` - Vocabulary map[string]bool `json:"$vocabulary,omitempty"` - - // metadata - Title string `json:"title,omitempty"` - Description string `json:"description,omitempty"` - Default json.RawMessage `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. - Type string `json:"-"` - Types []string `json:"-"` - Enum []any `json:"enum,omitempty"` - // Const is *any because a JSON null (Go nil) is a valid value. - Const *any `json:"const,omitempty"` - MultipleOf *float64 `json:"multipleOf,omitempty"` - Minimum *float64 `json:"minimum,omitempty"` - Maximum *float64 `json:"maximum,omitempty"` - ExclusiveMinimum *float64 `json:"exclusiveMinimum,omitempty"` - ExclusiveMaximum *float64 `json:"exclusiveMaximum,omitempty"` - MinLength *int `json:"minLength,omitempty"` - MaxLength *int `json:"maxLength,omitempty"` - Pattern string `json:"pattern,omitempty"` - - // arrays - PrefixItems []*Schema `json:"prefixItems,omitempty"` - Items *Schema `json:"items,omitempty"` - MinItems *int `json:"minItems,omitempty"` - MaxItems *int `json:"maxItems,omitempty"` - AdditionalItems *Schema `json:"additionalItems,omitempty"` - UniqueItems bool `json:"uniqueItems,omitempty"` - Contains *Schema `json:"contains,omitempty"` - MinContains *int `json:"minContains,omitempty"` // *int, not int: default is 1, not 0 - MaxContains *int `json:"maxContains,omitempty"` - UnevaluatedItems *Schema `json:"unevaluatedItems,omitempty"` - - // objects - MinProperties *int `json:"minProperties,omitempty"` - MaxProperties *int `json:"maxProperties,omitempty"` - Required []string `json:"required,omitempty"` - DependentRequired map[string][]string `json:"dependentRequired,omitempty"` - Properties map[string]*Schema `json:"properties,omitempty"` - PatternProperties map[string]*Schema `json:"patternProperties,omitempty"` - AdditionalProperties *Schema `json:"additionalProperties,omitempty"` - PropertyNames *Schema `json:"propertyNames,omitempty"` - UnevaluatedProperties *Schema `json:"unevaluatedProperties,omitempty"` - - // logic - AllOf []*Schema `json:"allOf,omitempty"` - AnyOf []*Schema `json:"anyOf,omitempty"` - OneOf []*Schema `json:"oneOf,omitempty"` - Not *Schema `json:"not,omitempty"` - - // conditional - If *Schema `json:"if,omitempty"` - Then *Schema `json:"then,omitempty"` - 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"` - - // Extra allows for additional keywords beyond those specified. - Extra map[string]any `json:"-"` -} - -// falseSchema returns a new Schema tree that fails to validate any value. -func falseSchema() *Schema { - return &Schema{Not: &Schema{}} -} - -// anchorInfo records the subschema to which an anchor refers, and whether -// the anchor keyword is $anchor or $dynamicAnchor. -type anchorInfo struct { - schema *Schema - dynamic bool -} - -// String returns a short description of the schema. -func (s *Schema) String() string { - if s.ID != "" { - return s.ID - } - if a := cmp.Or(s.Anchor, s.DynamicAnchor); a != "" { - return fmt.Sprintf("anchor %s", a) - } - return "" -} - -// CloneSchemas returns a copy of s. -// The copy is shallow except for sub-schemas, which are themelves copied with CloneSchemas. -// This allows both s and s.CloneSchemas() to appear as sub-schemas in the same parent. -func (s *Schema) CloneSchemas() *Schema { - if s == nil { - return nil - } - s2 := *s - v := reflect.ValueOf(&s2) - for _, info := range schemaFieldInfos { - fv := v.Elem().FieldByIndex(info.sf.Index) - switch info.sf.Type { - case schemaType: - sscss := fv.Interface().(*Schema) - fv.Set(reflect.ValueOf(sscss.CloneSchemas())) - - case schemaSliceType: - slice := fv.Interface().([]*Schema) - slice = slices.Clone(slice) - for i, ss := range slice { - slice[i] = ss.CloneSchemas() - } - fv.Set(reflect.ValueOf(slice)) - - case schemaMapType: - m := fv.Interface().(map[string]*Schema) - m = maps.Clone(m) - for k, ss := range m { - m[k] = ss.CloneSchemas() - } - fv.Set(reflect.ValueOf(m)) - } - } - return &s2 -} - -func (s *Schema) basicChecks() error { - if s.Type != "" && s.Types != nil { - return errors.New("both Type and Types are set; at most one should be") - } - if s.Defs != nil && s.Definitions != nil { - return errors.New("both Defs and Definitions are set; at most one should be") - } - return nil -} - -type schemaWithoutMethods Schema // doesn't implement json.{Unm,M}arshaler - -func (s *Schema) MarshalJSON() ([]byte, error) { - if err := s.basicChecks(); err != nil { - return nil, err - } - // Marshal either Type or Types as "type". - var typ any - switch { - case s.Type != "": - typ = s.Type - case s.Types != nil: - typ = s.Types - } - ms := struct { - Type any `json:"type,omitempty"` - *schemaWithoutMethods - }{ - Type: typ, - schemaWithoutMethods: (*schemaWithoutMethods)(s), - } - return marshalStructWithMap(&ms, "Extra") -} - -func (s *Schema) UnmarshalJSON(data []byte) error { - // A JSON boolean is a valid schema. - var b bool - if err := json.Unmarshal(data, &b); err == nil { - if b { - // true is the empty schema, which validates everything. - *s = Schema{} - } else { - // false is the schema that validates nothing. - *s = *falseSchema() - } - return nil - } - - ms := struct { - Type json.RawMessage `json:"type,omitempty"` - Const json.RawMessage `json:"const,omitempty"` - MinLength *integer `json:"minLength,omitempty"` - MaxLength *integer `json:"maxLength,omitempty"` - MinItems *integer `json:"minItems,omitempty"` - MaxItems *integer `json:"maxItems,omitempty"` - MinProperties *integer `json:"minProperties,omitempty"` - MaxProperties *integer `json:"maxProperties,omitempty"` - MinContains *integer `json:"minContains,omitempty"` - MaxContains *integer `json:"maxContains,omitempty"` - - *schemaWithoutMethods - }{ - schemaWithoutMethods: (*schemaWithoutMethods)(s), - } - if err := unmarshalStructWithMap(data, &ms, "Extra"); err != nil { - return err - } - // Unmarshal "type" as either Type or Types. - var err error - if len(ms.Type) > 0 { - switch ms.Type[0] { - case '"': - err = json.Unmarshal(ms.Type, &s.Type) - case '[': - err = json.Unmarshal(ms.Type, &s.Types) - default: - err = fmt.Errorf(`invalid value for "type": %q`, ms.Type) - } - } - if 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 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 - } - - set := func(dst **int, src *integer) { - if src != nil { - *dst = Ptr(int(*src)) - } - } - - set(&s.MinLength, ms.MinLength) - set(&s.MaxLength, ms.MaxLength) - set(&s.MinItems, ms.MinItems) - set(&s.MaxItems, ms.MaxItems) - set(&s.MinProperties, ms.MinProperties) - set(&s.MaxProperties, ms.MaxProperties) - set(&s.MinContains, ms.MinContains) - set(&s.MaxContains, ms.MaxContains) - - return nil -} - -type integer int32 // for the integer-valued fields of Schema - -func (ip *integer) UnmarshalJSON(data []byte) error { - if len(data) == 0 { - // nothing to do - return nil - } - // If there is a decimal point, src is a floating-point number. - var i int64 - if bytes.ContainsRune(data, '.') { - var f float64 - if err := json.Unmarshal(data, &f); err != nil { - return errors.New("not a number") - } - i = int64(f) - if float64(i) != f { - return errors.New("not an integer value") - } - } else { - if err := json.Unmarshal(data, &i); err != nil { - return errors.New("cannot be unmarshaled into an int") - } - } - // Ensure behavior is the same on both 32-bit and 64-bit systems. - if i < math.MinInt32 || i > math.MaxInt32 { - return errors.New("integer is out of range") - } - *ip = integer(i) - return nil -} - -// Ptr returns a pointer to a new variable whose value is x. -func Ptr[T any](x T) *T { return &x } - -// every applies f preorder to every schema under s including s. -// The second argument to f is the path to the schema appended to the argument path. -// It stops when f returns false. -func (s *Schema) every(f func(*Schema) bool) bool { - return f(s) && s.everyChild(func(s *Schema) bool { return s.every(f) }) -} - -// everyChild reports whether f is true for every immediate child schema of s. -func (s *Schema) everyChild(f func(*Schema) bool) bool { - v := reflect.ValueOf(s) - 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. - c := fv.Interface().(*Schema) - if c != nil && !f(c) { - return false - } - - case schemaSliceType: - slice := fv.Interface().([]*Schema) - for _, c := range slice { - if !f(c) { - return false - } - } - - case schemaMapType: - // Sort keys for determinism. - m := fv.Interface().(map[string]*Schema) - for _, k := range slices.Sorted(maps.Keys(m)) { - if !f(m[k]) { - return false - } - } - } - } - return true -} - -// all wraps every in an iterator. -func (s *Schema) all() iter.Seq[*Schema] { - return func(yield func(*Schema) bool) { s.every(yield) } -} - -// children wraps everyChild in an iterator. -func (s *Schema) children() iter.Seq[*Schema] { - return func(yield func(*Schema) bool) { s.everyChild(yield) } -} - -var ( - schemaType = reflect.TypeFor[*Schema]() - schemaSliceType = reflect.TypeFor[[]*Schema]() - schemaMapType = reflect.TypeFor[map[string]*Schema]() -) - -type structFieldInfo struct { - sf reflect.StructField - jsonName string -} - -var ( - // the visible fields of Schema that have a JSON name, sorted by that name - schemaFieldInfos []structFieldInfo - // map from JSON name to field - schemaFieldMap = map[string]reflect.StructField{} -) - -func init() { - for _, sf := range reflect.VisibleFields(reflect.TypeFor[Schema]()) { - info := util.FieldJSONInfo(sf) - if !info.Omit { - schemaFieldInfos = append(schemaFieldInfos, structFieldInfo{sf, info.Name}) - } - } - slices.SortFunc(schemaFieldInfos, func(i1, i2 structFieldInfo) int { - return cmp.Compare(i1.jsonName, i2.jsonName) - }) - for _, info := range schemaFieldInfos { - schemaFieldMap[info.jsonName] = info.sf - } -} diff --git a/jsonschema/schema_test.go b/jsonschema/schema_test.go deleted file mode 100644 index 4b6df511..00000000 --- a/jsonschema/schema_test.go +++ /dev/null @@ -1,175 +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 ( - "encoding/json" - "fmt" - "math" - "regexp" - "testing" -) - -func TestGoRoundTrip(t *testing.T) { - // Verify that Go representations round-trip. - for _, s := range []*Schema{ - {Type: "null"}, - {Types: []string{"null", "number"}}, - {Type: "string", MinLength: Ptr(20)}, - {Minimum: Ptr(20.0)}, - {Items: &Schema{Type: "integer"}}, - {Const: Ptr(any(0))}, - {Const: Ptr(any(nil))}, - {Const: Ptr(any([]int{}))}, - {Const: Ptr(any(map[string]any{}))}, - {Default: mustMarshal(1)}, - {Default: mustMarshal(nil)}, - {Extra: map[string]any{"test": "value"}}, - } { - data, err := json.Marshal(s) - if err != nil { - t.Fatal(err) - } - var got *Schema - mustUnmarshal(t, data, &got) - if !Equal(got, s) { - t.Errorf("got %s, want %s", got.json(), s.json()) - if got.Const != nil && s.Const != nil { - t.Logf("Consts: got %#v (%[1]T), want %#v (%[2]T)", *got.Const, *s.Const) - } - } - } -} - -func TestJSONRoundTrip(t *testing.T) { - // Verify that JSON texts for schemas marshal into equivalent forms. - // We don't expect everything to round-trip perfectly. For example, "true" and "false" - // will turn into their object equivalents. - // But most things should. - // Some of these cases test Schema.{UnM,M}arshalJSON. - // Most of others follow from the behavior of encoding/json, but they are still - // valuable as regression tests of this package's behavior. - for _, tt := range []struct { - in, want string - }{ - {`true`, `{}`}, // boolean schemas become object schemas - {`false`, `{"not":{}}`}, - {`{"type":"", "enum":null}`, `{}`}, // empty fields are omitted - {`{"minimum":1}`, `{"minimum":1}`}, - {`{"minimum":1.0}`, `{"minimum":1}`}, // floating-point integers lose their fractional part - {`{"minLength":1.0}`, `{"minLength":1}`}, // some floats are unmarshaled into ints, but you can't tell - { - // map keys are sorted - `{"$vocabulary":{"b":true, "a":false}}`, - `{"$vocabulary":{"a":false,"b":true}}`, - }, - {`{"unk":0}`, `{"unk":0}`}, // unknown fields are not dropped - { - // known and unknown fields are not dropped - // note that the order will be by the declaration order in the anonymous struct inside MarshalJSON - `{"comment":"test","type":"example","unk":0}`, - `{"type":"example","comment":"test","unk":0}`, - }, - {`{"extra":0}`, `{"extra":0}`}, // extra is not a special keyword and should not be dropped - {`{"Extra":0}`, `{"Extra":0}`}, // Extra is not a special keyword and should not be dropped - } { - var s Schema - mustUnmarshal(t, []byte(tt.in), &s) - data, err := json.Marshal(&s) - if err != nil { - t.Fatal(err) - } - if got := string(data); got != tt.want { - t.Errorf("%s:\ngot %s\nwant %s", tt.in, got, tt.want) - } - } -} - -func TestUnmarshalErrors(t *testing.T) { - for _, tt := range []struct { - in string - want string // error must match this regexp - }{ - {`1`, "cannot unmarshal number"}, - {`{"type":1}`, `invalid value for "type"`}, - {`{"minLength":1.5}`, `not an integer value`}, - {`{"maxLength":1.5}`, `not an integer value`}, - {`{"minItems":1.5}`, `not an integer value`}, - {`{"maxItems":1.5}`, `not an integer value`}, - {`{"minProperties":1.5}`, `not an integer value`}, - {`{"maxProperties":1.5}`, `not an integer value`}, - {`{"minContains":1.5}`, `not an integer value`}, - {`{"maxContains":1.5}`, `not an integer value`}, - {fmt.Sprintf(`{"maxContains":%d}`, int64(math.MaxInt32+1)), `out of range`}, - {`{"minLength":9e99}`, `cannot be unmarshaled`}, - {`{"minLength":"1.5"}`, `not a number`}, - } { - var s Schema - err := json.Unmarshal([]byte(tt.in), &s) - if err == nil { - t.Fatalf("%s: no error but expected one", tt.in) - } - if !regexp.MustCompile(tt.want).MatchString(err.Error()) { - t.Errorf("%s: error %q does not match %q", tt.in, err, tt.want) - } - - } -} - -func mustUnmarshal(t *testing.T, data []byte, ptr any) { - t.Helper() - if err := json.Unmarshal(data, ptr); err != nil { - t.Fatal(err) - } -} - -// json returns the schema in json format. -func (s *Schema) json() string { - data, err := json.Marshal(s) - if err != nil { - return fmt.Sprintf("", err) - } - return string(data) -} - -// json returns the schema in json format, indented. -func (s *Schema) jsonIndent() string { - data, err := json.MarshalIndent(s, "", " ") - if err != nil { - return fmt.Sprintf("", err) - } - return string(data) -} - -func TestCloneSchemas(t *testing.T) { - ss1 := &Schema{Type: "string"} - ss2 := &Schema{Type: "integer"} - ss3 := &Schema{Type: "boolean"} - ss4 := &Schema{Type: "number"} - ss5 := &Schema{Contains: ss4} - - s1 := Schema{ - Contains: ss1, - PrefixItems: []*Schema{ss2, ss3}, - Properties: map[string]*Schema{"a": ss5}, - } - s2 := s1.CloneSchemas() - - // The clones should appear identical. - if g, w := s1.json(), s2.json(); g != w { - t.Errorf("\ngot %s\nwant %s", g, w) - } - // None of the schemas should overlap. - schemas1 := map[*Schema]bool{ss1: true, ss2: true, ss3: true, ss4: true, ss5: true} - for ss := range s2.all() { - if schemas1[ss] { - t.Errorf("uncloned schema %s", ss.json()) - } - } - // s1's original schemas should be intact. - if s1.Contains != ss1 || s1.PrefixItems[0] != ss2 || s1.PrefixItems[1] != ss3 || ss5.Contains != ss4 || s1.Properties["a"] != ss5 { - t.Errorf("s1 modified") - } -} diff --git a/jsonschema/testdata/draft2020-12/README.md b/jsonschema/testdata/draft2020-12/README.md deleted file mode 100644 index dbc397dd..00000000 --- a/jsonschema/testdata/draft2020-12/README.md +++ /dev/null @@ -1,15 +0,0 @@ -# JSON Schema test suite for 2020-12 - -These files were copied from -https://github.com/json-schema-org/JSON-Schema-Test-Suite/tree/83e866b46c9f9e7082fd51e83a61c5f2145a1ab7/tests/draft2020-12. - -The following files were omitted: - -content.json: it is not required to validate content fields -(https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-00#rfc.section.8.1). - -format.json: it is not required to validate format fields (https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-00#rfc.section.7.1). - -vocabulary.json: this package doesn't support explicit vocabularies, other than the 2020-12 draft. - -The "optional" directory: this package doesn't implement any optional features. diff --git a/jsonschema/testdata/draft2020-12/additionalProperties.json b/jsonschema/testdata/draft2020-12/additionalProperties.json deleted file mode 100644 index 9618575e..00000000 --- a/jsonschema/testdata/draft2020-12/additionalProperties.json +++ /dev/null @@ -1,219 +0,0 @@ -[ - { - "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 - } - ] - } -] diff --git a/jsonschema/testdata/draft2020-12/allOf.json b/jsonschema/testdata/draft2020-12/allOf.json deleted file mode 100644 index 9e87903f..00000000 --- a/jsonschema/testdata/draft2020-12/allOf.json +++ /dev/null @@ -1,312 +0,0 @@ -[ - { - "description": "allOf", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "allOf": [ - { - "properties": { - "bar": {"type": "integer"} - }, - "required": ["bar"] - }, - { - "properties": { - "foo": {"type": "string"} - }, - "required": ["foo"] - } - ] - }, - "tests": [ - { - "description": "allOf", - "data": {"foo": "baz", "bar": 2}, - "valid": true - }, - { - "description": "mismatch second", - "data": {"foo": "baz"}, - "valid": false - }, - { - "description": "mismatch first", - "data": {"bar": 2}, - "valid": false - }, - { - "description": "wrong type", - "data": {"foo": "baz", "bar": "quux"}, - "valid": false - } - ] - }, - { - "description": "allOf with base schema", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "properties": {"bar": {"type": "integer"}}, - "required": ["bar"], - "allOf" : [ - { - "properties": { - "foo": {"type": "string"} - }, - "required": ["foo"] - }, - { - "properties": { - "baz": {"type": "null"} - }, - "required": ["baz"] - } - ] - }, - "tests": [ - { - "description": "valid", - "data": {"foo": "quux", "bar": 2, "baz": null}, - "valid": true - }, - { - "description": "mismatch base schema", - "data": {"foo": "quux", "baz": null}, - "valid": false - }, - { - "description": "mismatch first allOf", - "data": {"bar": 2, "baz": null}, - "valid": false - }, - { - "description": "mismatch second allOf", - "data": {"foo": "quux", "bar": 2}, - "valid": false - }, - { - "description": "mismatch both", - "data": {"bar": 2}, - "valid": false - } - ] - }, - { - "description": "allOf simple types", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "allOf": [ - {"maximum": 30}, - {"minimum": 20} - ] - }, - "tests": [ - { - "description": "valid", - "data": 25, - "valid": true - }, - { - "description": "mismatch one", - "data": 35, - "valid": false - } - ] - }, - { - "description": "allOf with boolean schemas, all true", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "allOf": [true, true] - }, - "tests": [ - { - "description": "any value is valid", - "data": "foo", - "valid": true - } - ] - }, - { - "description": "allOf with boolean schemas, some false", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "allOf": [true, false] - }, - "tests": [ - { - "description": "any value is invalid", - "data": "foo", - "valid": false - } - ] - }, - { - "description": "allOf with boolean schemas, all false", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "allOf": [false, false] - }, - "tests": [ - { - "description": "any value is invalid", - "data": "foo", - "valid": false - } - ] - }, - { - "description": "allOf with one empty schema", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "allOf": [ - {} - ] - }, - "tests": [ - { - "description": "any data is valid", - "data": 1, - "valid": true - } - ] - }, - { - "description": "allOf with two empty schemas", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "allOf": [ - {}, - {} - ] - }, - "tests": [ - { - "description": "any data is valid", - "data": 1, - "valid": true - } - ] - }, - { - "description": "allOf with the first empty schema", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "allOf": [ - {}, - { "type": "number" } - ] - }, - "tests": [ - { - "description": "number is valid", - "data": 1, - "valid": true - }, - { - "description": "string is invalid", - "data": "foo", - "valid": false - } - ] - }, - { - "description": "allOf with the last empty schema", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "allOf": [ - { "type": "number" }, - {} - ] - }, - "tests": [ - { - "description": "number is valid", - "data": 1, - "valid": true - }, - { - "description": "string is invalid", - "data": "foo", - "valid": false - } - ] - }, - { - "description": "nested allOf, to check validation semantics", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "allOf": [ - { - "allOf": [ - { - "type": "null" - } - ] - } - ] - }, - "tests": [ - { - "description": "null is valid", - "data": null, - "valid": true - }, - { - "description": "anything non-null is invalid", - "data": 123, - "valid": false - } - ] - }, - { - "description": "allOf combined with anyOf, oneOf", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "allOf": [ { "multipleOf": 2 } ], - "anyOf": [ { "multipleOf": 3 } ], - "oneOf": [ { "multipleOf": 5 } ] - }, - "tests": [ - { - "description": "allOf: false, anyOf: false, oneOf: false", - "data": 1, - "valid": false - }, - { - "description": "allOf: false, anyOf: false, oneOf: true", - "data": 5, - "valid": false - }, - { - "description": "allOf: false, anyOf: true, oneOf: false", - "data": 3, - "valid": false - }, - { - "description": "allOf: false, anyOf: true, oneOf: true", - "data": 15, - "valid": false - }, - { - "description": "allOf: true, anyOf: false, oneOf: false", - "data": 2, - "valid": false - }, - { - "description": "allOf: true, anyOf: false, oneOf: true", - "data": 10, - "valid": false - }, - { - "description": "allOf: true, anyOf: true, oneOf: false", - "data": 6, - "valid": false - }, - { - "description": "allOf: true, anyOf: true, oneOf: true", - "data": 30, - "valid": true - } - ] - } -] diff --git a/jsonschema/testdata/draft2020-12/anchor.json b/jsonschema/testdata/draft2020-12/anchor.json deleted file mode 100644 index 99143fa1..00000000 --- a/jsonschema/testdata/draft2020-12/anchor.json +++ /dev/null @@ -1,120 +0,0 @@ -[ - { - "description": "Location-independent identifier", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$ref": "#foo", - "$defs": { - "A": { - "$anchor": "foo", - "type": "integer" - } - } - }, - "tests": [ - { - "data": 1, - "description": "match", - "valid": true - }, - { - "data": "a", - "description": "mismatch", - "valid": false - } - ] - }, - { - "description": "Location-independent identifier with absolute URI", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$ref": "http://localhost:1234/draft2020-12/bar#foo", - "$defs": { - "A": { - "$id": "http://localhost:1234/draft2020-12/bar", - "$anchor": "foo", - "type": "integer" - } - } - }, - "tests": [ - { - "data": 1, - "description": "match", - "valid": true - }, - { - "data": "a", - "description": "mismatch", - "valid": false - } - ] - }, - { - "description": "Location-independent identifier with base URI change in subschema", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "http://localhost:1234/draft2020-12/root", - "$ref": "http://localhost:1234/draft2020-12/nested.json#foo", - "$defs": { - "A": { - "$id": "nested.json", - "$defs": { - "B": { - "$anchor": "foo", - "type": "integer" - } - } - } - } - }, - "tests": [ - { - "data": 1, - "description": "match", - "valid": true - }, - { - "data": "a", - "description": "mismatch", - "valid": false - } - ] - }, - { - "description": "same $anchor with different base uri", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "http://localhost:1234/draft2020-12/foobar", - "$defs": { - "A": { - "$id": "child1", - "allOf": [ - { - "$id": "child2", - "$anchor": "my_anchor", - "type": "number" - }, - { - "$anchor": "my_anchor", - "type": "string" - } - ] - } - }, - "$ref": "child1#my_anchor" - }, - "tests": [ - { - "description": "$ref resolves to /$defs/A/allOf/1", - "data": "a", - "valid": true - }, - { - "description": "$ref does not resolve to /$defs/A/allOf/0", - "data": 1, - "valid": false - } - ] - } -] diff --git a/jsonschema/testdata/draft2020-12/anyOf.json b/jsonschema/testdata/draft2020-12/anyOf.json deleted file mode 100644 index 89b192db..00000000 --- a/jsonschema/testdata/draft2020-12/anyOf.json +++ /dev/null @@ -1,203 +0,0 @@ -[ - { - "description": "anyOf", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "anyOf": [ - { - "type": "integer" - }, - { - "minimum": 2 - } - ] - }, - "tests": [ - { - "description": "first anyOf valid", - "data": 1, - "valid": true - }, - { - "description": "second anyOf valid", - "data": 2.5, - "valid": true - }, - { - "description": "both anyOf valid", - "data": 3, - "valid": true - }, - { - "description": "neither anyOf valid", - "data": 1.5, - "valid": false - } - ] - }, - { - "description": "anyOf with base schema", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "string", - "anyOf" : [ - { - "maxLength": 2 - }, - { - "minLength": 4 - } - ] - }, - "tests": [ - { - "description": "mismatch base schema", - "data": 3, - "valid": false - }, - { - "description": "one anyOf valid", - "data": "foobar", - "valid": true - }, - { - "description": "both anyOf invalid", - "data": "foo", - "valid": false - } - ] - }, - { - "description": "anyOf with boolean schemas, all true", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "anyOf": [true, true] - }, - "tests": [ - { - "description": "any value is valid", - "data": "foo", - "valid": true - } - ] - }, - { - "description": "anyOf with boolean schemas, some true", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "anyOf": [true, false] - }, - "tests": [ - { - "description": "any value is valid", - "data": "foo", - "valid": true - } - ] - }, - { - "description": "anyOf with boolean schemas, all false", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "anyOf": [false, false] - }, - "tests": [ - { - "description": "any value is invalid", - "data": "foo", - "valid": false - } - ] - }, - { - "description": "anyOf complex types", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "anyOf": [ - { - "properties": { - "bar": {"type": "integer"} - }, - "required": ["bar"] - }, - { - "properties": { - "foo": {"type": "string"} - }, - "required": ["foo"] - } - ] - }, - "tests": [ - { - "description": "first anyOf valid (complex)", - "data": {"bar": 2}, - "valid": true - }, - { - "description": "second anyOf valid (complex)", - "data": {"foo": "baz"}, - "valid": true - }, - { - "description": "both anyOf valid (complex)", - "data": {"foo": "baz", "bar": 2}, - "valid": true - }, - { - "description": "neither anyOf valid (complex)", - "data": {"foo": 2, "bar": "quux"}, - "valid": false - } - ] - }, - { - "description": "anyOf with one empty schema", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "anyOf": [ - { "type": "number" }, - {} - ] - }, - "tests": [ - { - "description": "string is valid", - "data": "foo", - "valid": true - }, - { - "description": "number is valid", - "data": 123, - "valid": true - } - ] - }, - { - "description": "nested anyOf, to check validation semantics", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "anyOf": [ - { - "anyOf": [ - { - "type": "null" - } - ] - } - ] - }, - "tests": [ - { - "description": "null is valid", - "data": null, - "valid": true - }, - { - "description": "anything non-null is invalid", - "data": 123, - "valid": false - } - ] - } -] diff --git a/jsonschema/testdata/draft2020-12/boolean_schema.json b/jsonschema/testdata/draft2020-12/boolean_schema.json deleted file mode 100644 index 6d40f23f..00000000 --- a/jsonschema/testdata/draft2020-12/boolean_schema.json +++ /dev/null @@ -1,104 +0,0 @@ -[ - { - "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 - } - ] - } -] diff --git a/jsonschema/testdata/draft2020-12/const.json b/jsonschema/testdata/draft2020-12/const.json deleted file mode 100644 index 50be86a0..00000000 --- a/jsonschema/testdata/draft2020-12/const.json +++ /dev/null @@ -1,387 +0,0 @@ -[ - { - "description": "const validation", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "const": 2 - }, - "tests": [ - { - "description": "same value is valid", - "data": 2, - "valid": true - }, - { - "description": "another value is invalid", - "data": 5, - "valid": false - }, - { - "description": "another type is invalid", - "data": "a", - "valid": false - } - ] - }, - { - "description": "const with object", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "const": {"foo": "bar", "baz": "bax"} - }, - "tests": [ - { - "description": "same object is valid", - "data": {"foo": "bar", "baz": "bax"}, - "valid": true - }, - { - "description": "same object with different property order is valid", - "data": {"baz": "bax", "foo": "bar"}, - "valid": true - }, - { - "description": "another object is invalid", - "data": {"foo": "bar"}, - "valid": false - }, - { - "description": "another type is invalid", - "data": [1, 2], - "valid": false - } - ] - }, - { - "description": "const with array", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "const": [{ "foo": "bar" }] - }, - "tests": [ - { - "description": "same array is valid", - "data": [{"foo": "bar"}], - "valid": true - }, - { - "description": "another array item is invalid", - "data": [2], - "valid": false - }, - { - "description": "array with additional items is invalid", - "data": [1, 2, 3], - "valid": false - } - ] - }, - { - "description": "const with null", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "const": null - }, - "tests": [ - { - "description": "null is valid", - "data": null, - "valid": true - }, - { - "description": "not null is invalid", - "data": 0, - "valid": false - } - ] - }, - { - "description": "const with false does not match 0", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "const": false - }, - "tests": [ - { - "description": "false is valid", - "data": false, - "valid": true - }, - { - "description": "integer zero is invalid", - "data": 0, - "valid": false - }, - { - "description": "float zero is invalid", - "data": 0.0, - "valid": false - } - ] - }, - { - "description": "const with true does not match 1", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "const": true - }, - "tests": [ - { - "description": "true is valid", - "data": true, - "valid": true - }, - { - "description": "integer one is invalid", - "data": 1, - "valid": false - }, - { - "description": "float one is invalid", - "data": 1.0, - "valid": false - } - ] - }, - { - "description": "const with [false] does not match [0]", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "const": [false] - }, - "tests": [ - { - "description": "[false] is valid", - "data": [false], - "valid": true - }, - { - "description": "[0] is invalid", - "data": [0], - "valid": false - }, - { - "description": "[0.0] is invalid", - "data": [0.0], - "valid": false - } - ] - }, - { - "description": "const with [true] does not match [1]", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "const": [true] - }, - "tests": [ - { - "description": "[true] is valid", - "data": [true], - "valid": true - }, - { - "description": "[1] is invalid", - "data": [1], - "valid": false - }, - { - "description": "[1.0] is invalid", - "data": [1.0], - "valid": false - } - ] - }, - { - "description": "const with {\"a\": false} does not match {\"a\": 0}", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "const": {"a": false} - }, - "tests": [ - { - "description": "{\"a\": false} is valid", - "data": {"a": false}, - "valid": true - }, - { - "description": "{\"a\": 0} is invalid", - "data": {"a": 0}, - "valid": false - }, - { - "description": "{\"a\": 0.0} is invalid", - "data": {"a": 0.0}, - "valid": false - } - ] - }, - { - "description": "const with {\"a\": true} does not match {\"a\": 1}", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "const": {"a": true} - }, - "tests": [ - { - "description": "{\"a\": true} is valid", - "data": {"a": true}, - "valid": true - }, - { - "description": "{\"a\": 1} is invalid", - "data": {"a": 1}, - "valid": false - }, - { - "description": "{\"a\": 1.0} is invalid", - "data": {"a": 1.0}, - "valid": false - } - ] - }, - { - "description": "const with 0 does not match other zero-like types", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "const": 0 - }, - "tests": [ - { - "description": "false is invalid", - "data": false, - "valid": false - }, - { - "description": "integer zero is valid", - "data": 0, - "valid": true - }, - { - "description": "float zero is valid", - "data": 0.0, - "valid": true - }, - { - "description": "empty object is invalid", - "data": {}, - "valid": false - }, - { - "description": "empty array is invalid", - "data": [], - "valid": false - }, - { - "description": "empty string is invalid", - "data": "", - "valid": false - } - ] - }, - { - "description": "const with 1 does not match true", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "const": 1 - }, - "tests": [ - { - "description": "true is invalid", - "data": true, - "valid": false - }, - { - "description": "integer one is valid", - "data": 1, - "valid": true - }, - { - "description": "float one is valid", - "data": 1.0, - "valid": true - } - ] - }, - { - "description": "const with -2.0 matches integer and float types", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "const": -2.0 - }, - "tests": [ - { - "description": "integer -2 is valid", - "data": -2, - "valid": true - }, - { - "description": "integer 2 is invalid", - "data": 2, - "valid": false - }, - { - "description": "float -2.0 is valid", - "data": -2.0, - "valid": true - }, - { - "description": "float 2.0 is invalid", - "data": 2.0, - "valid": false - }, - { - "description": "float -2.00001 is invalid", - "data": -2.00001, - "valid": false - } - ] - }, - { - "description": "float and integers are equal up to 64-bit representation limits", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "const": 9007199254740992 - }, - "tests": [ - { - "description": "integer is valid", - "data": 9007199254740992, - "valid": true - }, - { - "description": "integer minus one is invalid", - "data": 9007199254740991, - "valid": false - }, - { - "description": "float is valid", - "data": 9007199254740992.0, - "valid": true - }, - { - "description": "float minus one is invalid", - "data": 9007199254740991.0, - "valid": false - } - ] - }, - { - "description": "nul characters in strings", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "const": "hello\u0000there" - }, - "tests": [ - { - "description": "match string with nul", - "data": "hello\u0000there", - "valid": true - }, - { - "description": "do not match string lacking nul", - "data": "hellothere", - "valid": false - } - ] - } -] diff --git a/jsonschema/testdata/draft2020-12/contains.json b/jsonschema/testdata/draft2020-12/contains.json deleted file mode 100644 index 08a00a75..00000000 --- a/jsonschema/testdata/draft2020-12/contains.json +++ /dev/null @@ -1,176 +0,0 @@ -[ - { - "description": "contains keyword validation", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "contains": {"minimum": 5} - }, - "tests": [ - { - "description": "array with item matching schema (5) is valid", - "data": [3, 4, 5], - "valid": true - }, - { - "description": "array with item matching schema (6) is valid", - "data": [3, 4, 6], - "valid": true - }, - { - "description": "array with two items matching schema (5, 6) is valid", - "data": [3, 4, 5, 6], - "valid": true - }, - { - "description": "array without items matching schema is invalid", - "data": [2, 3, 4], - "valid": false - }, - { - "description": "empty array is invalid", - "data": [], - "valid": false - }, - { - "description": "not array is valid", - "data": {}, - "valid": true - } - ] - }, - { - "description": "contains keyword with const keyword", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "contains": { "const": 5 } - }, - "tests": [ - { - "description": "array with item 5 is valid", - "data": [3, 4, 5], - "valid": true - }, - { - "description": "array with two items 5 is valid", - "data": [3, 4, 5, 5], - "valid": true - }, - { - "description": "array without item 5 is invalid", - "data": [1, 2, 3, 4], - "valid": false - } - ] - }, - { - "description": "contains keyword with boolean schema true", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "contains": true - }, - "tests": [ - { - "description": "any non-empty array is valid", - "data": ["foo"], - "valid": true - }, - { - "description": "empty array is invalid", - "data": [], - "valid": false - } - ] - }, - { - "description": "contains keyword with boolean schema false", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "contains": false - }, - "tests": [ - { - "description": "any non-empty array is invalid", - "data": ["foo"], - "valid": false - }, - { - "description": "empty array is invalid", - "data": [], - "valid": false - }, - { - "description": "non-arrays are valid", - "data": "contains does not apply to strings", - "valid": true - } - ] - }, - { - "description": "items + contains", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "items": { "multipleOf": 2 }, - "contains": { "multipleOf": 3 } - }, - "tests": [ - { - "description": "matches items, does not match contains", - "data": [ 2, 4, 8 ], - "valid": false - }, - { - "description": "does not match items, matches contains", - "data": [ 3, 6, 9 ], - "valid": false - }, - { - "description": "matches both items and contains", - "data": [ 6, 12 ], - "valid": true - }, - { - "description": "matches neither items nor contains", - "data": [ 1, 5 ], - "valid": false - } - ] - }, - { - "description": "contains with false if subschema", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "contains": { - "if": false, - "else": true - } - }, - "tests": [ - { - "description": "any non-empty array is valid", - "data": ["foo"], - "valid": true - }, - { - "description": "empty array is invalid", - "data": [], - "valid": false - } - ] - }, - { - "description": "contains with null instance elements", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "contains": { - "type": "null" - } - }, - "tests": [ - { - "description": "allows null items", - "data": [ null ], - "valid": true - } - ] - } -] diff --git a/jsonschema/testdata/draft2020-12/default.json b/jsonschema/testdata/draft2020-12/default.json deleted file mode 100644 index ceb3ae27..00000000 --- a/jsonschema/testdata/draft2020-12/default.json +++ /dev/null @@ -1,82 +0,0 @@ -[ - { - "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/testdata/draft2020-12/defs.json b/jsonschema/testdata/draft2020-12/defs.json deleted file mode 100644 index da2a503b..00000000 --- a/jsonschema/testdata/draft2020-12/defs.json +++ /dev/null @@ -1,21 +0,0 @@ -[ - { - "description": "validate definition against metaschema", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$ref": "https://json-schema.org/draft/2020-12/schema" - }, - "tests": [ - { - "description": "valid definition schema", - "data": {"$defs": {"foo": {"type": "integer"}}}, - "valid": true - }, - { - "description": "invalid definition schema", - "data": {"$defs": {"foo": {"type": 1}}}, - "valid": false - } - ] - } -] diff --git a/jsonschema/testdata/draft2020-12/dependentRequired.json b/jsonschema/testdata/draft2020-12/dependentRequired.json deleted file mode 100644 index 2baa38e9..00000000 --- a/jsonschema/testdata/draft2020-12/dependentRequired.json +++ /dev/null @@ -1,152 +0,0 @@ -[ - { - "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 - } - ] - } -] diff --git a/jsonschema/testdata/draft2020-12/dependentSchemas.json b/jsonschema/testdata/draft2020-12/dependentSchemas.json deleted file mode 100644 index 1c5f0574..00000000 --- a/jsonschema/testdata/draft2020-12/dependentSchemas.json +++ /dev/null @@ -1,171 +0,0 @@ -[ - { - "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 - } - ] - } -] diff --git a/jsonschema/testdata/draft2020-12/dynamicRef.json b/jsonschema/testdata/draft2020-12/dynamicRef.json deleted file mode 100644 index ffa211ba..00000000 --- a/jsonschema/testdata/draft2020-12/dynamicRef.json +++ /dev/null @@ -1,815 +0,0 @@ -[ - { - "description": "A $dynamicRef to a $dynamicAnchor in the same schema resource behaves like a normal $ref to an $anchor", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://test.json-schema.org/dynamicRef-dynamicAnchor-same-schema/root", - "type": "array", - "items": { "$dynamicRef": "#items" }, - "$defs": { - "foo": { - "$dynamicAnchor": "items", - "type": "string" - } - } - }, - "tests": [ - { - "description": "An array of strings is valid", - "data": ["foo", "bar"], - "valid": true - }, - { - "description": "An array containing non-strings is invalid", - "data": ["foo", 42], - "valid": false - } - ] - }, - { - "description": "A $dynamicRef to an $anchor in the same schema resource behaves like a normal $ref to an $anchor", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://test.json-schema.org/dynamicRef-anchor-same-schema/root", - "type": "array", - "items": { "$dynamicRef": "#items" }, - "$defs": { - "foo": { - "$anchor": "items", - "type": "string" - } - } - }, - "tests": [ - { - "description": "An array of strings is valid", - "data": ["foo", "bar"], - "valid": true - }, - { - "description": "An array containing non-strings is invalid", - "data": ["foo", 42], - "valid": false - } - ] - }, - { - "description": "A $ref to a $dynamicAnchor in the same schema resource behaves like a normal $ref to an $anchor", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://test.json-schema.org/ref-dynamicAnchor-same-schema/root", - "type": "array", - "items": { "$ref": "#items" }, - "$defs": { - "foo": { - "$dynamicAnchor": "items", - "type": "string" - } - } - }, - "tests": [ - { - "description": "An array of strings is valid", - "data": ["foo", "bar"], - "valid": true - }, - { - "description": "An array containing non-strings is invalid", - "data": ["foo", 42], - "valid": false - } - ] - }, - { - "description": "A $dynamicRef resolves to the first $dynamicAnchor still in scope that is encountered when the schema is evaluated", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://test.json-schema.org/typical-dynamic-resolution/root", - "$ref": "list", - "$defs": { - "foo": { - "$dynamicAnchor": "items", - "type": "string" - }, - "list": { - "$id": "list", - "type": "array", - "items": { "$dynamicRef": "#items" }, - "$defs": { - "items": { - "$comment": "This is only needed to satisfy the bookending requirement", - "$dynamicAnchor": "items" - } - } - } - } - }, - "tests": [ - { - "description": "An array of strings is valid", - "data": ["foo", "bar"], - "valid": true - }, - { - "description": "An array containing non-strings is invalid", - "data": ["foo", 42], - "valid": false - } - ] - }, - { - "description": "A $dynamicRef without anchor in fragment behaves identical to $ref", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://test.json-schema.org/dynamicRef-without-anchor/root", - "$ref": "list", - "$defs": { - "foo": { - "$dynamicAnchor": "items", - "type": "string" - }, - "list": { - "$id": "list", - "type": "array", - "items": { "$dynamicRef": "#/$defs/items" }, - "$defs": { - "items": { - "$comment": "This is only needed to satisfy the bookending requirement", - "$dynamicAnchor": "items", - "type": "number" - } - } - } - } - }, - "tests": [ - { - "description": "An array of strings is invalid", - "data": ["foo", "bar"], - "valid": false - }, - { - "description": "An array of numbers is valid", - "data": [24, 42], - "valid": true - } - ] - }, - { - "description": "A $dynamicRef with intermediate scopes that don't include a matching $dynamicAnchor does not affect dynamic scope resolution", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://test.json-schema.org/dynamic-resolution-with-intermediate-scopes/root", - "$ref": "intermediate-scope", - "$defs": { - "foo": { - "$dynamicAnchor": "items", - "type": "string" - }, - "intermediate-scope": { - "$id": "intermediate-scope", - "$ref": "list" - }, - "list": { - "$id": "list", - "type": "array", - "items": { "$dynamicRef": "#items" }, - "$defs": { - "items": { - "$comment": "This is only needed to satisfy the bookending requirement", - "$dynamicAnchor": "items" - } - } - } - } - }, - "tests": [ - { - "description": "An array of strings is valid", - "data": ["foo", "bar"], - "valid": true - }, - { - "description": "An array containing non-strings is invalid", - "data": ["foo", 42], - "valid": false - } - ] - }, - { - "description": "An $anchor with the same name as a $dynamicAnchor is not used for dynamic scope resolution", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://test.json-schema.org/dynamic-resolution-ignores-anchors/root", - "$ref": "list", - "$defs": { - "foo": { - "$anchor": "items", - "type": "string" - }, - "list": { - "$id": "list", - "type": "array", - "items": { "$dynamicRef": "#items" }, - "$defs": { - "items": { - "$comment": "This is only needed to satisfy the bookending requirement", - "$dynamicAnchor": "items" - } - } - } - } - }, - "tests": [ - { - "description": "Any array is valid", - "data": ["foo", 42], - "valid": true - } - ] - }, - { - "description": "A $dynamicRef without a matching $dynamicAnchor in the same schema resource behaves like a normal $ref to $anchor", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://test.json-schema.org/dynamic-resolution-without-bookend/root", - "$ref": "list", - "$defs": { - "foo": { - "$dynamicAnchor": "items", - "type": "string" - }, - "list": { - "$id": "list", - "type": "array", - "items": { "$dynamicRef": "#items" }, - "$defs": { - "items": { - "$comment": "This is only needed to give the reference somewhere to resolve to when it behaves like $ref", - "$anchor": "items" - } - } - } - } - }, - "tests": [ - { - "description": "Any array is valid", - "data": ["foo", 42], - "valid": true - } - ] - }, - { - "description": "A $dynamicRef with a non-matching $dynamicAnchor in the same schema resource behaves like a normal $ref to $anchor", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://test.json-schema.org/unmatched-dynamic-anchor/root", - "$ref": "list", - "$defs": { - "foo": { - "$dynamicAnchor": "items", - "type": "string" - }, - "list": { - "$id": "list", - "type": "array", - "items": { "$dynamicRef": "#items" }, - "$defs": { - "items": { - "$comment": "This is only needed to give the reference somewhere to resolve to when it behaves like $ref", - "$anchor": "items", - "$dynamicAnchor": "foo" - } - } - } - } - }, - "tests": [ - { - "description": "Any array is valid", - "data": ["foo", 42], - "valid": true - } - ] - }, - { - "description": "A $dynamicRef that initially resolves to a schema with a matching $dynamicAnchor resolves to the first $dynamicAnchor in the dynamic scope", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://test.json-schema.org/relative-dynamic-reference/root", - "$dynamicAnchor": "meta", - "type": "object", - "properties": { - "foo": { "const": "pass" } - }, - "$ref": "extended", - "$defs": { - "extended": { - "$id": "extended", - "$dynamicAnchor": "meta", - "type": "object", - "properties": { - "bar": { "$ref": "bar" } - } - }, - "bar": { - "$id": "bar", - "type": "object", - "properties": { - "baz": { "$dynamicRef": "extended#meta" } - } - } - } - }, - "tests": [ - { - "description": "The recursive part is valid against the root", - "data": { - "foo": "pass", - "bar": { - "baz": { "foo": "pass" } - } - }, - "valid": true - }, - { - "description": "The recursive part is not valid against the root", - "data": { - "foo": "pass", - "bar": { - "baz": { "foo": "fail" } - } - }, - "valid": false - } - ] - }, - { - "description": "A $dynamicRef that initially resolves to a schema without a matching $dynamicAnchor behaves like a normal $ref to $anchor", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://test.json-schema.org/relative-dynamic-reference-without-bookend/root", - "$dynamicAnchor": "meta", - "type": "object", - "properties": { - "foo": { "const": "pass" } - }, - "$ref": "extended", - "$defs": { - "extended": { - "$id": "extended", - "$anchor": "meta", - "type": "object", - "properties": { - "bar": { "$ref": "bar" } - } - }, - "bar": { - "$id": "bar", - "type": "object", - "properties": { - "baz": { "$dynamicRef": "extended#meta" } - } - } - } - }, - "tests": [ - { - "description": "The recursive part doesn't need to validate against the root", - "data": { - "foo": "pass", - "bar": { - "baz": { "foo": "fail" } - } - }, - "valid": true - } - ] - }, - { - "description": "multiple dynamic paths to the $dynamicRef keyword", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://test.json-schema.org/dynamic-ref-with-multiple-paths/main", - "if": { - "properties": { - "kindOfList": { "const": "numbers" } - }, - "required": ["kindOfList"] - }, - "then": { "$ref": "numberList" }, - "else": { "$ref": "stringList" }, - - "$defs": { - "genericList": { - "$id": "genericList", - "properties": { - "list": { - "items": { "$dynamicRef": "#itemType" } - } - }, - "$defs": { - "defaultItemType": { - "$comment": "Only needed to satisfy bookending requirement", - "$dynamicAnchor": "itemType" - } - } - }, - "numberList": { - "$id": "numberList", - "$defs": { - "itemType": { - "$dynamicAnchor": "itemType", - "type": "number" - } - }, - "$ref": "genericList" - }, - "stringList": { - "$id": "stringList", - "$defs": { - "itemType": { - "$dynamicAnchor": "itemType", - "type": "string" - } - }, - "$ref": "genericList" - } - } - }, - "tests": [ - { - "description": "number list with number values", - "data": { - "kindOfList": "numbers", - "list": [1.1] - }, - "valid": true - }, - { - "description": "number list with string values", - "data": { - "kindOfList": "numbers", - "list": ["foo"] - }, - "valid": false - }, - { - "description": "string list with number values", - "data": { - "kindOfList": "strings", - "list": [1.1] - }, - "valid": false - }, - { - "description": "string list with string values", - "data": { - "kindOfList": "strings", - "list": ["foo"] - }, - "valid": true - } - ] - }, - { - "description": "after leaving a dynamic scope, it is not used by a $dynamicRef", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://test.json-schema.org/dynamic-ref-leaving-dynamic-scope/main", - "if": { - "$id": "first_scope", - "$defs": { - "thingy": { - "$comment": "this is first_scope#thingy", - "$dynamicAnchor": "thingy", - "type": "number" - } - } - }, - "then": { - "$id": "second_scope", - "$ref": "start", - "$defs": { - "thingy": { - "$comment": "this is second_scope#thingy, the final destination of the $dynamicRef", - "$dynamicAnchor": "thingy", - "type": "null" - } - } - }, - "$defs": { - "start": { - "$comment": "this is the landing spot from $ref", - "$id": "start", - "$dynamicRef": "inner_scope#thingy" - }, - "thingy": { - "$comment": "this is the first stop for the $dynamicRef", - "$id": "inner_scope", - "$dynamicAnchor": "thingy", - "type": "string" - } - } - }, - "tests": [ - { - "description": "string matches /$defs/thingy, but the $dynamicRef does not stop here", - "data": "a string", - "valid": false - }, - { - "description": "first_scope is not in dynamic scope for the $dynamicRef", - "data": 42, - "valid": false - }, - { - "description": "/then/$defs/thingy is the final stop for the $dynamicRef", - "data": null, - "valid": true - } - ] - }, - { - "description": "strict-tree schema, guards against misspelled properties", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "http://localhost:1234/draft2020-12/strict-tree.json", - "$dynamicAnchor": "node", - - "$ref": "tree.json", - "unevaluatedProperties": false - }, - "tests": [ - { - "description": "instance with misspelled field", - "data": { - "children": [{ - "daat": 1 - }] - }, - "valid": false - }, - { - "description": "instance with correct field", - "data": { - "children": [{ - "data": 1 - }] - }, - "valid": true - } - ] - }, - { - "description": "tests for implementation dynamic anchor and reference link", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "http://localhost:1234/draft2020-12/strict-extendible.json", - "$ref": "extendible-dynamic-ref.json", - "$defs": { - "elements": { - "$dynamicAnchor": "elements", - "properties": { - "a": true - }, - "required": ["a"], - "additionalProperties": false - } - } - }, - "tests": [ - { - "description": "incorrect parent schema", - "data": { - "a": true - }, - "valid": false - }, - { - "description": "incorrect extended schema", - "data": { - "elements": [ - { "b": 1 } - ] - }, - "valid": false - }, - { - "description": "correct extended schema", - "data": { - "elements": [ - { "a": 1 } - ] - }, - "valid": true - } - ] - }, - { - "description": "$ref and $dynamicAnchor are independent of order - $defs first", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "http://localhost:1234/draft2020-12/strict-extendible-allof-defs-first.json", - "allOf": [ - { - "$ref": "extendible-dynamic-ref.json" - }, - { - "$defs": { - "elements": { - "$dynamicAnchor": "elements", - "properties": { - "a": true - }, - "required": ["a"], - "additionalProperties": false - } - } - } - ] - }, - "tests": [ - { - "description": "incorrect parent schema", - "data": { - "a": true - }, - "valid": false - }, - { - "description": "incorrect extended schema", - "data": { - "elements": [ - { "b": 1 } - ] - }, - "valid": false - }, - { - "description": "correct extended schema", - "data": { - "elements": [ - { "a": 1 } - ] - }, - "valid": true - } - ] - }, - { - "description": "$ref and $dynamicAnchor are independent of order - $ref first", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "http://localhost:1234/draft2020-12/strict-extendible-allof-ref-first.json", - "allOf": [ - { - "$defs": { - "elements": { - "$dynamicAnchor": "elements", - "properties": { - "a": true - }, - "required": ["a"], - "additionalProperties": false - } - } - }, - { - "$ref": "extendible-dynamic-ref.json" - } - ] - }, - "tests": [ - { - "description": "incorrect parent schema", - "data": { - "a": true - }, - "valid": false - }, - { - "description": "incorrect extended schema", - "data": { - "elements": [ - { "b": 1 } - ] - }, - "valid": false - }, - { - "description": "correct extended schema", - "data": { - "elements": [ - { "a": 1 } - ] - }, - "valid": true - } - ] - }, - { - "description": "$ref to $dynamicRef finds detached $dynamicAnchor", - "schema": { - "$ref": "http://localhost:1234/draft2020-12/detached-dynamicref.json#/$defs/foo" - }, - "tests": [ - { - "description": "number is valid", - "data": 1, - "valid": true - }, - { - "description": "non-number is invalid", - "data": "a", - "valid": false - } - ] - }, - { - "description": "$dynamicRef points to a boolean schema", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$defs": { - "true": true, - "false": false - }, - "properties": { - "true": { - "$dynamicRef": "#/$defs/true" - }, - "false": { - "$dynamicRef": "#/$defs/false" - } - } - }, - "tests": [ - { - "description": "follow $dynamicRef to a true schema", - "data": { "true": 1 }, - "valid": true - }, - { - "description": "follow $dynamicRef to a false schema", - "data": { "false": 1 }, - "valid": false - } - ] - }, - { - "description": "$dynamicRef skips over intermediate resources - direct reference", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://test.json-schema.org/dynamic-ref-skips-intermediate-resource/main", - "type": "object", - "properties": { - "bar-item": { - "$ref": "item" - } - }, - "$defs": { - "bar": { - "$id": "bar", - "type": "array", - "items": { - "$ref": "item" - }, - "$defs": { - "item": { - "$id": "item", - "type": "object", - "properties": { - "content": { - "$dynamicRef": "#content" - } - }, - "$defs": { - "defaultContent": { - "$dynamicAnchor": "content", - "type": "integer" - } - } - }, - "content": { - "$dynamicAnchor": "content", - "type": "string" - } - } - } - } - }, - "tests": [ - { - "description": "integer property passes", - "data": { "bar-item": { "content": 42 } }, - "valid": true - }, - { - "description": "string property fails", - "data": { "bar-item": { "content": "value" } }, - "valid": false - } - ] - } -] diff --git a/jsonschema/testdata/draft2020-12/enum.json b/jsonschema/testdata/draft2020-12/enum.json deleted file mode 100644 index c8f35eac..00000000 --- a/jsonschema/testdata/draft2020-12/enum.json +++ /dev/null @@ -1,358 +0,0 @@ -[ - { - "description": "simple enum validation", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "enum": [1, 2, 3] - }, - "tests": [ - { - "description": "one of the enum is valid", - "data": 1, - "valid": true - }, - { - "description": "something else is invalid", - "data": 4, - "valid": false - } - ] - }, - { - "description": "heterogeneous enum validation", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "enum": [6, "foo", [], true, {"foo": 12}] - }, - "tests": [ - { - "description": "one of the enum is valid", - "data": [], - "valid": true - }, - { - "description": "something else is invalid", - "data": null, - "valid": false - }, - { - "description": "objects are deep compared", - "data": {"foo": false}, - "valid": false - }, - { - "description": "valid object matches", - "data": {"foo": 12}, - "valid": true - }, - { - "description": "extra properties in object is invalid", - "data": {"foo": 12, "boo": 42}, - "valid": false - } - ] - }, - { - "description": "heterogeneous enum-with-null validation", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "enum": [6, null] - }, - "tests": [ - { - "description": "null is valid", - "data": null, - "valid": true - }, - { - "description": "number is valid", - "data": 6, - "valid": true - }, - { - "description": "something else is invalid", - "data": "test", - "valid": false - } - ] - }, - { - "description": "enums in properties", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "type":"object", - "properties": { - "foo": {"enum":["foo"]}, - "bar": {"enum":["bar"]} - }, - "required": ["bar"] - }, - "tests": [ - { - "description": "both properties are valid", - "data": {"foo":"foo", "bar":"bar"}, - "valid": true - }, - { - "description": "wrong foo value", - "data": {"foo":"foot", "bar":"bar"}, - "valid": false - }, - { - "description": "wrong bar value", - "data": {"foo":"foo", "bar":"bart"}, - "valid": false - }, - { - "description": "missing optional property is valid", - "data": {"bar":"bar"}, - "valid": true - }, - { - "description": "missing required property is invalid", - "data": {"foo":"foo"}, - "valid": false - }, - { - "description": "missing all properties is invalid", - "data": {}, - "valid": false - } - ] - }, - { - "description": "enum with escaped characters", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "enum": ["foo\nbar", "foo\rbar"] - }, - "tests": [ - { - "description": "member 1 is valid", - "data": "foo\nbar", - "valid": true - }, - { - "description": "member 2 is valid", - "data": "foo\rbar", - "valid": true - }, - { - "description": "another string is invalid", - "data": "abc", - "valid": false - } - ] - }, - { - "description": "enum with false does not match 0", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "enum": [false] - }, - "tests": [ - { - "description": "false is valid", - "data": false, - "valid": true - }, - { - "description": "integer zero is invalid", - "data": 0, - "valid": false - }, - { - "description": "float zero is invalid", - "data": 0.0, - "valid": false - } - ] - }, - { - "description": "enum with [false] does not match [0]", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "enum": [[false]] - }, - "tests": [ - { - "description": "[false] is valid", - "data": [false], - "valid": true - }, - { - "description": "[0] is invalid", - "data": [0], - "valid": false - }, - { - "description": "[0.0] is invalid", - "data": [0.0], - "valid": false - } - ] - }, - { - "description": "enum with true does not match 1", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "enum": [true] - }, - "tests": [ - { - "description": "true is valid", - "data": true, - "valid": true - }, - { - "description": "integer one is invalid", - "data": 1, - "valid": false - }, - { - "description": "float one is invalid", - "data": 1.0, - "valid": false - } - ] - }, - { - "description": "enum with [true] does not match [1]", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "enum": [[true]] - }, - "tests": [ - { - "description": "[true] is valid", - "data": [true], - "valid": true - }, - { - "description": "[1] is invalid", - "data": [1], - "valid": false - }, - { - "description": "[1.0] is invalid", - "data": [1.0], - "valid": false - } - ] - }, - { - "description": "enum with 0 does not match false", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "enum": [0] - }, - "tests": [ - { - "description": "false is invalid", - "data": false, - "valid": false - }, - { - "description": "integer zero is valid", - "data": 0, - "valid": true - }, - { - "description": "float zero is valid", - "data": 0.0, - "valid": true - } - ] - }, - { - "description": "enum with [0] does not match [false]", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "enum": [[0]] - }, - "tests": [ - { - "description": "[false] is invalid", - "data": [false], - "valid": false - }, - { - "description": "[0] is valid", - "data": [0], - "valid": true - }, - { - "description": "[0.0] is valid", - "data": [0.0], - "valid": true - } - ] - }, - { - "description": "enum with 1 does not match true", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "enum": [1] - }, - "tests": [ - { - "description": "true is invalid", - "data": true, - "valid": false - }, - { - "description": "integer one is valid", - "data": 1, - "valid": true - }, - { - "description": "float one is valid", - "data": 1.0, - "valid": true - } - ] - }, - { - "description": "enum with [1] does not match [true]", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "enum": [[1]] - }, - "tests": [ - { - "description": "[true] is invalid", - "data": [true], - "valid": false - }, - { - "description": "[1] is valid", - "data": [1], - "valid": true - }, - { - "description": "[1.0] is valid", - "data": [1.0], - "valid": true - } - ] - }, - { - "description": "nul characters in strings", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "enum": [ "hello\u0000there" ] - }, - "tests": [ - { - "description": "match string with nul", - "data": "hello\u0000there", - "valid": true - }, - { - "description": "do not match string lacking nul", - "data": "hellothere", - "valid": false - } - ] - } -] diff --git a/jsonschema/testdata/draft2020-12/exclusiveMaximum.json b/jsonschema/testdata/draft2020-12/exclusiveMaximum.json deleted file mode 100644 index 05db2335..00000000 --- a/jsonschema/testdata/draft2020-12/exclusiveMaximum.json +++ /dev/null @@ -1,31 +0,0 @@ -[ - { - "description": "exclusiveMaximum validation", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "exclusiveMaximum": 3.0 - }, - "tests": [ - { - "description": "below the exclusiveMaximum is valid", - "data": 2.2, - "valid": true - }, - { - "description": "boundary point is invalid", - "data": 3.0, - "valid": false - }, - { - "description": "above the exclusiveMaximum is invalid", - "data": 3.5, - "valid": false - }, - { - "description": "ignores non-numbers", - "data": "x", - "valid": true - } - ] - } -] diff --git a/jsonschema/testdata/draft2020-12/exclusiveMinimum.json b/jsonschema/testdata/draft2020-12/exclusiveMinimum.json deleted file mode 100644 index 00af9d7f..00000000 --- a/jsonschema/testdata/draft2020-12/exclusiveMinimum.json +++ /dev/null @@ -1,31 +0,0 @@ -[ - { - "description": "exclusiveMinimum validation", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "exclusiveMinimum": 1.1 - }, - "tests": [ - { - "description": "above the exclusiveMinimum is valid", - "data": 1.2, - "valid": true - }, - { - "description": "boundary point is invalid", - "data": 1.1, - "valid": false - }, - { - "description": "below the exclusiveMinimum is invalid", - "data": 0.6, - "valid": false - }, - { - "description": "ignores non-numbers", - "data": "x", - "valid": true - } - ] - } -] diff --git a/jsonschema/testdata/draft2020-12/if-then-else.json b/jsonschema/testdata/draft2020-12/if-then-else.json deleted file mode 100644 index 1c35d7e6..00000000 --- a/jsonschema/testdata/draft2020-12/if-then-else.json +++ /dev/null @@ -1,268 +0,0 @@ -[ - { - "description": "ignore if without then or else", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "if": { - "const": 0 - } - }, - "tests": [ - { - "description": "valid when valid against lone if", - "data": 0, - "valid": true - }, - { - "description": "valid when invalid against lone if", - "data": "hello", - "valid": true - } - ] - }, - { - "description": "ignore then without if", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "then": { - "const": 0 - } - }, - "tests": [ - { - "description": "valid when valid against lone then", - "data": 0, - "valid": true - }, - { - "description": "valid when invalid against lone then", - "data": "hello", - "valid": true - } - ] - }, - { - "description": "ignore else without if", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "else": { - "const": 0 - } - }, - "tests": [ - { - "description": "valid when valid against lone else", - "data": 0, - "valid": true - }, - { - "description": "valid when invalid against lone else", - "data": "hello", - "valid": true - } - ] - }, - { - "description": "if and then without else", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "if": { - "exclusiveMaximum": 0 - }, - "then": { - "minimum": -10 - } - }, - "tests": [ - { - "description": "valid through then", - "data": -1, - "valid": true - }, - { - "description": "invalid through then", - "data": -100, - "valid": false - }, - { - "description": "valid when if test fails", - "data": 3, - "valid": true - } - ] - }, - { - "description": "if and else without then", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "if": { - "exclusiveMaximum": 0 - }, - "else": { - "multipleOf": 2 - } - }, - "tests": [ - { - "description": "valid when if test passes", - "data": -1, - "valid": true - }, - { - "description": "valid through else", - "data": 4, - "valid": true - }, - { - "description": "invalid through else", - "data": 3, - "valid": false - } - ] - }, - { - "description": "validate against correct branch, then vs else", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "if": { - "exclusiveMaximum": 0 - }, - "then": { - "minimum": -10 - }, - "else": { - "multipleOf": 2 - } - }, - "tests": [ - { - "description": "valid through then", - "data": -1, - "valid": true - }, - { - "description": "invalid through then", - "data": -100, - "valid": false - }, - { - "description": "valid through else", - "data": 4, - "valid": true - }, - { - "description": "invalid through else", - "data": 3, - "valid": false - } - ] - }, - { - "description": "non-interference across combined schemas", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "allOf": [ - { - "if": { - "exclusiveMaximum": 0 - } - }, - { - "then": { - "minimum": -10 - } - }, - { - "else": { - "multipleOf": 2 - } - } - ] - }, - "tests": [ - { - "description": "valid, but would have been invalid through then", - "data": -100, - "valid": true - }, - { - "description": "valid, but would have been invalid through else", - "data": 3, - "valid": true - } - ] - }, - { - "description": "if with boolean schema true", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "if": true, - "then": { "const": "then" }, - "else": { "const": "else" } - }, - "tests": [ - { - "description": "boolean schema true in if always chooses the then path (valid)", - "data": "then", - "valid": true - }, - { - "description": "boolean schema true in if always chooses the then path (invalid)", - "data": "else", - "valid": false - } - ] - }, - { - "description": "if with boolean schema false", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "if": false, - "then": { "const": "then" }, - "else": { "const": "else" } - }, - "tests": [ - { - "description": "boolean schema false in if always chooses the else path (invalid)", - "data": "then", - "valid": false - }, - { - "description": "boolean schema false in if always chooses the else path (valid)", - "data": "else", - "valid": true - } - ] - }, - { - "description": "if appears at the end when serialized (keyword processing sequence)", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "then": { "const": "yes" }, - "else": { "const": "other" }, - "if": { "maxLength": 4 } - }, - "tests": [ - { - "description": "yes redirects to then and passes", - "data": "yes", - "valid": true - }, - { - "description": "other redirects to else and passes", - "data": "other", - "valid": true - }, - { - "description": "no redirects to then and fails", - "data": "no", - "valid": false - }, - { - "description": "invalid redirects to else and fails", - "data": "invalid", - "valid": false - } - ] - } -] diff --git a/jsonschema/testdata/draft2020-12/infinite-loop-detection.json b/jsonschema/testdata/draft2020-12/infinite-loop-detection.json deleted file mode 100644 index 46f157a3..00000000 --- a/jsonschema/testdata/draft2020-12/infinite-loop-detection.json +++ /dev/null @@ -1,37 +0,0 @@ -[ - { - "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 - } - ] - } -] diff --git a/jsonschema/testdata/draft2020-12/items.json b/jsonschema/testdata/draft2020-12/items.json deleted file mode 100644 index 6a3e1cf2..00000000 --- a/jsonschema/testdata/draft2020-12/items.json +++ /dev/null @@ -1,304 +0,0 @@ -[ - { - "description": "a schema given for items", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "items": {"type": "integer"} - }, - "tests": [ - { - "description": "valid items", - "data": [ 1, 2, 3 ], - "valid": true - }, - { - "description": "wrong type of items", - "data": [1, "x"], - "valid": false - }, - { - "description": "ignores non-arrays", - "data": {"foo" : "bar"}, - "valid": true - }, - { - "description": "JavaScript pseudo-array is valid", - "data": { - "0": "invalid", - "length": 1 - }, - "valid": true - } - ] - }, - { - "description": "items with boolean schema (true)", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "items": true - }, - "tests": [ - { - "description": "any array is valid", - "data": [ 1, "foo", true ], - "valid": true - }, - { - "description": "empty array is valid", - "data": [], - "valid": true - } - ] - }, - { - "description": "items with boolean schema (false)", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "items": false - }, - "tests": [ - { - "description": "any non-empty array is invalid", - "data": [ 1, "foo", true ], - "valid": false - }, - { - "description": "empty array is valid", - "data": [], - "valid": true - } - ] - }, - { - "description": "items and subitems", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$defs": { - "item": { - "type": "array", - "items": false, - "prefixItems": [ - { "$ref": "#/$defs/sub-item" }, - { "$ref": "#/$defs/sub-item" } - ] - }, - "sub-item": { - "type": "object", - "required": ["foo"] - } - }, - "type": "array", - "items": false, - "prefixItems": [ - { "$ref": "#/$defs/item" }, - { "$ref": "#/$defs/item" }, - { "$ref": "#/$defs/item" } - ] - }, - "tests": [ - { - "description": "valid items", - "data": [ - [ {"foo": null}, {"foo": null} ], - [ {"foo": null}, {"foo": null} ], - [ {"foo": null}, {"foo": null} ] - ], - "valid": true - }, - { - "description": "too many items", - "data": [ - [ {"foo": null}, {"foo": null} ], - [ {"foo": null}, {"foo": null} ], - [ {"foo": null}, {"foo": null} ], - [ {"foo": null}, {"foo": null} ] - ], - "valid": false - }, - { - "description": "too many sub-items", - "data": [ - [ {"foo": null}, {"foo": null}, {"foo": null} ], - [ {"foo": null}, {"foo": null} ], - [ {"foo": null}, {"foo": null} ] - ], - "valid": false - }, - { - "description": "wrong item", - "data": [ - {"foo": null}, - [ {"foo": null}, {"foo": null} ], - [ {"foo": null}, {"foo": null} ] - ], - "valid": false - }, - { - "description": "wrong sub-item", - "data": [ - [ {}, {"foo": null} ], - [ {"foo": null}, {"foo": null} ], - [ {"foo": null}, {"foo": null} ] - ], - "valid": false - }, - { - "description": "fewer items is valid", - "data": [ - [ {"foo": null} ], - [ {"foo": null} ] - ], - "valid": true - } - ] - }, - { - "description": "nested items", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "array", - "items": { - "type": "array", - "items": { - "type": "array", - "items": { - "type": "array", - "items": { - "type": "number" - } - } - } - } - }, - "tests": [ - { - "description": "valid nested array", - "data": [[[[1]], [[2],[3]]], [[[4], [5], [6]]]], - "valid": true - }, - { - "description": "nested array with invalid type", - "data": [[[["1"]], [[2],[3]]], [[[4], [5], [6]]]], - "valid": false - }, - { - "description": "not deep enough", - "data": [[[1], [2],[3]], [[4], [5], [6]]], - "valid": false - } - ] - }, - { - "description": "prefixItems with no additional items allowed", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "prefixItems": [{}, {}, {}], - "items": false - }, - "tests": [ - { - "description": "empty array", - "data": [ ], - "valid": true - }, - { - "description": "fewer number of items present (1)", - "data": [ 1 ], - "valid": true - }, - { - "description": "fewer number of items present (2)", - "data": [ 1, 2 ], - "valid": true - }, - { - "description": "equal number of items present", - "data": [ 1, 2, 3 ], - "valid": true - }, - { - "description": "additional items are not permitted", - "data": [ 1, 2, 3, 4 ], - "valid": false - } - ] - }, - { - "description": "items does not look in applicators, valid case", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "allOf": [ - { "prefixItems": [ { "minimum": 3 } ] } - ], - "items": { "minimum": 5 } - }, - "tests": [ - { - "description": "prefixItems in allOf does not constrain items, invalid case", - "data": [ 3, 5 ], - "valid": false - }, - { - "description": "prefixItems in allOf does not constrain items, valid case", - "data": [ 5, 5 ], - "valid": true - } - ] - }, - { - "description": "prefixItems validation adjusts the starting index for items", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "prefixItems": [ { "type": "string" } ], - "items": { "type": "integer" } - }, - "tests": [ - { - "description": "valid items", - "data": [ "x", 2, 3 ], - "valid": true - }, - { - "description": "wrong type of second item", - "data": [ "x", "y" ], - "valid": false - } - ] - }, - { - "description": "items with heterogeneous array", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "prefixItems": [{}], - "items": false - }, - "tests": [ - { - "description": "heterogeneous invalid instance", - "data": [ "foo", "bar", 37 ], - "valid": false - }, - { - "description": "valid instance", - "data": [ null ], - "valid": true - } - ] - }, - { - "description": "items with null instance elements", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "items": { - "type": "null" - } - }, - "tests": [ - { - "description": "allows null elements", - "data": [ null ], - "valid": true - } - ] - } -] diff --git a/jsonschema/testdata/draft2020-12/maxContains.json b/jsonschema/testdata/draft2020-12/maxContains.json deleted file mode 100644 index 8cd3ca74..00000000 --- a/jsonschema/testdata/draft2020-12/maxContains.json +++ /dev/null @@ -1,102 +0,0 @@ -[ - { - "description": "maxContains without contains is ignored", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "maxContains": 1 - }, - "tests": [ - { - "description": "one item valid against lone maxContains", - "data": [ 1 ], - "valid": true - }, - { - "description": "two items still valid against lone maxContains", - "data": [ 1, 2 ], - "valid": true - } - ] - }, - { - "description": "maxContains with contains", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "contains": {"const": 1}, - "maxContains": 1 - }, - "tests": [ - { - "description": "empty data", - "data": [ ], - "valid": false - }, - { - "description": "all elements match, valid maxContains", - "data": [ 1 ], - "valid": true - }, - { - "description": "all elements match, invalid maxContains", - "data": [ 1, 1 ], - "valid": false - }, - { - "description": "some elements match, valid maxContains", - "data": [ 1, 2 ], - "valid": true - }, - { - "description": "some elements match, invalid maxContains", - "data": [ 1, 2, 1 ], - "valid": false - } - ] - }, - { - "description": "maxContains with contains, value with a decimal", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "contains": {"const": 1}, - "maxContains": 1.0 - }, - "tests": [ - { - "description": "one element matches, valid maxContains", - "data": [ 1 ], - "valid": true - }, - { - "description": "too many elements match, invalid maxContains", - "data": [ 1, 1 ], - "valid": false - } - ] - }, - { - "description": "minContains < maxContains", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "contains": {"const": 1}, - "minContains": 1, - "maxContains": 3 - }, - "tests": [ - { - "description": "actual < minContains < maxContains", - "data": [ ], - "valid": false - }, - { - "description": "minContains < actual < maxContains", - "data": [ 1, 1 ], - "valid": true - }, - { - "description": "minContains < maxContains < actual", - "data": [ 1, 1, 1, 1 ], - "valid": false - } - ] - } -] diff --git a/jsonschema/testdata/draft2020-12/maxItems.json b/jsonschema/testdata/draft2020-12/maxItems.json deleted file mode 100644 index f6a6b7c9..00000000 --- a/jsonschema/testdata/draft2020-12/maxItems.json +++ /dev/null @@ -1,50 +0,0 @@ -[ - { - "description": "maxItems validation", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "maxItems": 2 - }, - "tests": [ - { - "description": "shorter is valid", - "data": [1], - "valid": true - }, - { - "description": "exact length is valid", - "data": [1, 2], - "valid": true - }, - { - "description": "too long is invalid", - "data": [1, 2, 3], - "valid": false - }, - { - "description": "ignores non-arrays", - "data": "foobar", - "valid": true - } - ] - }, - { - "description": "maxItems validation with a decimal", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "maxItems": 2.0 - }, - "tests": [ - { - "description": "shorter is valid", - "data": [1], - "valid": true - }, - { - "description": "too long is invalid", - "data": [1, 2, 3], - "valid": false - } - ] - } -] diff --git a/jsonschema/testdata/draft2020-12/maxLength.json b/jsonschema/testdata/draft2020-12/maxLength.json deleted file mode 100644 index 7462726d..00000000 --- a/jsonschema/testdata/draft2020-12/maxLength.json +++ /dev/null @@ -1,55 +0,0 @@ -[ - { - "description": "maxLength validation", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "maxLength": 2 - }, - "tests": [ - { - "description": "shorter is valid", - "data": "f", - "valid": true - }, - { - "description": "exact length is valid", - "data": "fo", - "valid": true - }, - { - "description": "too long is invalid", - "data": "foo", - "valid": false - }, - { - "description": "ignores non-strings", - "data": 100, - "valid": true - }, - { - "description": "two graphemes is long enough", - "data": "\uD83D\uDCA9\uD83D\uDCA9", - "valid": true - } - ] - }, - { - "description": "maxLength validation with a decimal", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "maxLength": 2.0 - }, - "tests": [ - { - "description": "shorter is valid", - "data": "f", - "valid": true - }, - { - "description": "too long is invalid", - "data": "foo", - "valid": false - } - ] - } -] diff --git a/jsonschema/testdata/draft2020-12/maxProperties.json b/jsonschema/testdata/draft2020-12/maxProperties.json deleted file mode 100644 index 73ae7316..00000000 --- a/jsonschema/testdata/draft2020-12/maxProperties.json +++ /dev/null @@ -1,79 +0,0 @@ -[ - { - "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 - } - ] - } -] diff --git a/jsonschema/testdata/draft2020-12/maximum.json b/jsonschema/testdata/draft2020-12/maximum.json deleted file mode 100644 index b99a541e..00000000 --- a/jsonschema/testdata/draft2020-12/maximum.json +++ /dev/null @@ -1,60 +0,0 @@ -[ - { - "description": "maximum validation", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "maximum": 3.0 - }, - "tests": [ - { - "description": "below the maximum is valid", - "data": 2.6, - "valid": true - }, - { - "description": "boundary point is valid", - "data": 3.0, - "valid": true - }, - { - "description": "above the maximum is invalid", - "data": 3.5, - "valid": false - }, - { - "description": "ignores non-numbers", - "data": "x", - "valid": true - } - ] - }, - { - "description": "maximum validation with unsigned integer", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "maximum": 300 - }, - "tests": [ - { - "description": "below the maximum is invalid", - "data": 299.97, - "valid": true - }, - { - "description": "boundary point integer is valid", - "data": 300, - "valid": true - }, - { - "description": "boundary point float is valid", - "data": 300.00, - "valid": true - }, - { - "description": "above the maximum is invalid", - "data": 300.5, - "valid": false - } - ] - } -] diff --git a/jsonschema/testdata/draft2020-12/minContains.json b/jsonschema/testdata/draft2020-12/minContains.json deleted file mode 100644 index ee72d7d6..00000000 --- a/jsonschema/testdata/draft2020-12/minContains.json +++ /dev/null @@ -1,224 +0,0 @@ -[ - { - "description": "minContains without contains is ignored", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "minContains": 1 - }, - "tests": [ - { - "description": "one item valid against lone minContains", - "data": [ 1 ], - "valid": true - }, - { - "description": "zero items still valid against lone minContains", - "data": [], - "valid": true - } - ] - }, - { - "description": "minContains=1 with contains", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "contains": {"const": 1}, - "minContains": 1 - }, - "tests": [ - { - "description": "empty data", - "data": [ ], - "valid": false - }, - { - "description": "no elements match", - "data": [ 2 ], - "valid": false - }, - { - "description": "single element matches, valid minContains", - "data": [ 1 ], - "valid": true - }, - { - "description": "some elements match, valid minContains", - "data": [ 1, 2 ], - "valid": true - }, - { - "description": "all elements match, valid minContains", - "data": [ 1, 1 ], - "valid": true - } - ] - }, - { - "description": "minContains=2 with contains", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "contains": {"const": 1}, - "minContains": 2 - }, - "tests": [ - { - "description": "empty data", - "data": [ ], - "valid": false - }, - { - "description": "all elements match, invalid minContains", - "data": [ 1 ], - "valid": false - }, - { - "description": "some elements match, invalid minContains", - "data": [ 1, 2 ], - "valid": false - }, - { - "description": "all elements match, valid minContains (exactly as needed)", - "data": [ 1, 1 ], - "valid": true - }, - { - "description": "all elements match, valid minContains (more than needed)", - "data": [ 1, 1, 1 ], - "valid": true - }, - { - "description": "some elements match, valid minContains", - "data": [ 1, 2, 1 ], - "valid": true - } - ] - }, - { - "description": "minContains=2 with contains with a decimal value", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "contains": {"const": 1}, - "minContains": 2.0 - }, - "tests": [ - { - "description": "one element matches, invalid minContains", - "data": [ 1 ], - "valid": false - }, - { - "description": "both elements match, valid minContains", - "data": [ 1, 1 ], - "valid": true - } - ] - }, - { - "description": "maxContains = minContains", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "contains": {"const": 1}, - "maxContains": 2, - "minContains": 2 - }, - "tests": [ - { - "description": "empty data", - "data": [ ], - "valid": false - }, - { - "description": "all elements match, invalid minContains", - "data": [ 1 ], - "valid": false - }, - { - "description": "all elements match, invalid maxContains", - "data": [ 1, 1, 1 ], - "valid": false - }, - { - "description": "all elements match, valid maxContains and minContains", - "data": [ 1, 1 ], - "valid": true - } - ] - }, - { - "description": "maxContains < minContains", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "contains": {"const": 1}, - "maxContains": 1, - "minContains": 3 - }, - "tests": [ - { - "description": "empty data", - "data": [ ], - "valid": false - }, - { - "description": "invalid minContains", - "data": [ 1 ], - "valid": false - }, - { - "description": "invalid maxContains", - "data": [ 1, 1, 1 ], - "valid": false - }, - { - "description": "invalid maxContains and minContains", - "data": [ 1, 1 ], - "valid": false - } - ] - }, - { - "description": "minContains = 0", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "contains": {"const": 1}, - "minContains": 0 - }, - "tests": [ - { - "description": "empty data", - "data": [ ], - "valid": true - }, - { - "description": "minContains = 0 makes contains always pass", - "data": [ 2 ], - "valid": true - } - ] - }, - { - "description": "minContains = 0 with maxContains", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "contains": {"const": 1}, - "minContains": 0, - "maxContains": 1 - }, - "tests": [ - { - "description": "empty data", - "data": [ ], - "valid": true - }, - { - "description": "not more than maxContains", - "data": [ 1 ], - "valid": true - }, - { - "description": "too many", - "data": [ 1, 1 ], - "valid": false - } - ] - } -] diff --git a/jsonschema/testdata/draft2020-12/minItems.json b/jsonschema/testdata/draft2020-12/minItems.json deleted file mode 100644 index 9d6a8b6d..00000000 --- a/jsonschema/testdata/draft2020-12/minItems.json +++ /dev/null @@ -1,50 +0,0 @@ -[ - { - "description": "minItems validation", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "minItems": 1 - }, - "tests": [ - { - "description": "longer is valid", - "data": [1, 2], - "valid": true - }, - { - "description": "exact length is valid", - "data": [1], - "valid": true - }, - { - "description": "too short is invalid", - "data": [], - "valid": false - }, - { - "description": "ignores non-arrays", - "data": "", - "valid": true - } - ] - }, - { - "description": "minItems validation with a decimal", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "minItems": 1.0 - }, - "tests": [ - { - "description": "longer is valid", - "data": [1, 2], - "valid": true - }, - { - "description": "too short is invalid", - "data": [], - "valid": false - } - ] - } -] diff --git a/jsonschema/testdata/draft2020-12/minLength.json b/jsonschema/testdata/draft2020-12/minLength.json deleted file mode 100644 index 5076c5a9..00000000 --- a/jsonschema/testdata/draft2020-12/minLength.json +++ /dev/null @@ -1,55 +0,0 @@ -[ - { - "description": "minLength validation", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "minLength": 2 - }, - "tests": [ - { - "description": "longer is valid", - "data": "foo", - "valid": true - }, - { - "description": "exact length is valid", - "data": "fo", - "valid": true - }, - { - "description": "too short is invalid", - "data": "f", - "valid": false - }, - { - "description": "ignores non-strings", - "data": 1, - "valid": true - }, - { - "description": "one grapheme is not long enough", - "data": "\uD83D\uDCA9", - "valid": false - } - ] - }, - { - "description": "minLength validation with a decimal", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "minLength": 2.0 - }, - "tests": [ - { - "description": "longer is valid", - "data": "foo", - "valid": true - }, - { - "description": "too short is invalid", - "data": "f", - "valid": false - } - ] - } -] diff --git a/jsonschema/testdata/draft2020-12/minProperties.json b/jsonschema/testdata/draft2020-12/minProperties.json deleted file mode 100644 index a753ad35..00000000 --- a/jsonschema/testdata/draft2020-12/minProperties.json +++ /dev/null @@ -1,60 +0,0 @@ -[ - { - "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 - } - ] - } -] diff --git a/jsonschema/testdata/draft2020-12/minimum.json b/jsonschema/testdata/draft2020-12/minimum.json deleted file mode 100644 index dc440527..00000000 --- a/jsonschema/testdata/draft2020-12/minimum.json +++ /dev/null @@ -1,75 +0,0 @@ -[ - { - "description": "minimum validation", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "minimum": 1.1 - }, - "tests": [ - { - "description": "above the minimum is valid", - "data": 2.6, - "valid": true - }, - { - "description": "boundary point is valid", - "data": 1.1, - "valid": true - }, - { - "description": "below the minimum is invalid", - "data": 0.6, - "valid": false - }, - { - "description": "ignores non-numbers", - "data": "x", - "valid": true - } - ] - }, - { - "description": "minimum validation with signed integer", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "minimum": -2 - }, - "tests": [ - { - "description": "negative above the minimum is valid", - "data": -1, - "valid": true - }, - { - "description": "positive above the minimum is valid", - "data": 0, - "valid": true - }, - { - "description": "boundary point is valid", - "data": -2, - "valid": true - }, - { - "description": "boundary point with float is valid", - "data": -2.0, - "valid": true - }, - { - "description": "float below the minimum is invalid", - "data": -2.0001, - "valid": false - }, - { - "description": "int below the minimum is invalid", - "data": -3, - "valid": false - }, - { - "description": "ignores non-numbers", - "data": "x", - "valid": true - } - ] - } -] diff --git a/jsonschema/testdata/draft2020-12/multipleOf.json b/jsonschema/testdata/draft2020-12/multipleOf.json deleted file mode 100644 index 92d6979b..00000000 --- a/jsonschema/testdata/draft2020-12/multipleOf.json +++ /dev/null @@ -1,97 +0,0 @@ -[ - { - "description": "by int", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "multipleOf": 2 - }, - "tests": [ - { - "description": "int by int", - "data": 10, - "valid": true - }, - { - "description": "int by int fail", - "data": 7, - "valid": false - }, - { - "description": "ignores non-numbers", - "data": "foo", - "valid": true - } - ] - }, - { - "description": "by number", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "multipleOf": 1.5 - }, - "tests": [ - { - "description": "zero is multiple of anything", - "data": 0, - "valid": true - }, - { - "description": "4.5 is multiple of 1.5", - "data": 4.5, - "valid": true - }, - { - "description": "35 is not multiple of 1.5", - "data": 35, - "valid": false - } - ] - }, - { - "description": "by small number", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "multipleOf": 0.0001 - }, - "tests": [ - { - "description": "0.0075 is multiple of 0.0001", - "data": 0.0075, - "valid": true - }, - { - "description": "0.00751 is not multiple of 0.0001", - "data": 0.00751, - "valid": false - } - ] - }, - { - "description": "float division = inf", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "integer", "multipleOf": 0.123456789 - }, - "tests": [ - { - "description": "always invalid, but naive implementations may raise an overflow error", - "data": 1e308, - "valid": false - } - ] - }, - { - "description": "small multiple of large integer", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "integer", "multipleOf": 1e-8 - }, - "tests": [ - { - "description": "any integer is a multiple of 1e-8", - "data": 12391239123, - "valid": true - } - ] - } -] diff --git a/jsonschema/testdata/draft2020-12/not.json b/jsonschema/testdata/draft2020-12/not.json deleted file mode 100644 index 346d4a7e..00000000 --- a/jsonschema/testdata/draft2020-12/not.json +++ /dev/null @@ -1,301 +0,0 @@ -[ - { - "description": "not", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "not": {"type": "integer"} - }, - "tests": [ - { - "description": "allowed", - "data": "foo", - "valid": true - }, - { - "description": "disallowed", - "data": 1, - "valid": false - } - ] - }, - { - "description": "not multiple types", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "not": {"type": ["integer", "boolean"]} - }, - "tests": [ - { - "description": "valid", - "data": "foo", - "valid": true - }, - { - "description": "mismatch", - "data": 1, - "valid": false - }, - { - "description": "other mismatch", - "data": true, - "valid": false - } - ] - }, - { - "description": "not more complex schema", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "not": { - "type": "object", - "properties": { - "foo": { - "type": "string" - } - } - } - }, - "tests": [ - { - "description": "match", - "data": 1, - "valid": true - }, - { - "description": "other match", - "data": {"foo": 1}, - "valid": true - }, - { - "description": "mismatch", - "data": {"foo": "bar"}, - "valid": false - } - ] - }, - { - "description": "forbidden property", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "properties": { - "foo": { - "not": {} - } - } - }, - "tests": [ - { - "description": "property present", - "data": {"foo": 1, "bar": 2}, - "valid": false - }, - { - "description": "property absent", - "data": {"bar": 1, "baz": 2}, - "valid": true - } - ] - }, - { - "description": "forbid everything with empty schema", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "not": {} - }, - "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 - } - ] - }, - { - "description": "forbid everything with boolean schema true", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "not": true - }, - "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 - } - ] - }, - { - "description": "allow everything with boolean schema false", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "not": false - }, - "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": "double negation", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "not": { "not": {} } - }, - "tests": [ - { - "description": "any value is valid", - "data": "foo", - "valid": true - } - ] - }, - { - "description": "collect annotations inside a 'not', even if collection is disabled", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "not": { - "$comment": "this subschema must still produce annotations internally, even though the 'not' will ultimately discard them", - "anyOf": [ - true, - { "properties": { "foo": true } } - ], - "unevaluatedProperties": false - } - }, - "tests": [ - { - "description": "unevaluated property", - "data": { "bar": 1 }, - "valid": true - }, - { - "description": "annotations are still collected inside a 'not'", - "data": { "foo": 1 }, - "valid": false - } - ] - } -] diff --git a/jsonschema/testdata/draft2020-12/oneOf.json b/jsonschema/testdata/draft2020-12/oneOf.json deleted file mode 100644 index 7a7c7ffe..00000000 --- a/jsonschema/testdata/draft2020-12/oneOf.json +++ /dev/null @@ -1,293 +0,0 @@ -[ - { - "description": "oneOf", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "oneOf": [ - { - "type": "integer" - }, - { - "minimum": 2 - } - ] - }, - "tests": [ - { - "description": "first oneOf valid", - "data": 1, - "valid": true - }, - { - "description": "second oneOf valid", - "data": 2.5, - "valid": true - }, - { - "description": "both oneOf valid", - "data": 3, - "valid": false - }, - { - "description": "neither oneOf valid", - "data": 1.5, - "valid": false - } - ] - }, - { - "description": "oneOf with base schema", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "string", - "oneOf" : [ - { - "minLength": 2 - }, - { - "maxLength": 4 - } - ] - }, - "tests": [ - { - "description": "mismatch base schema", - "data": 3, - "valid": false - }, - { - "description": "one oneOf valid", - "data": "foobar", - "valid": true - }, - { - "description": "both oneOf valid", - "data": "foo", - "valid": false - } - ] - }, - { - "description": "oneOf with boolean schemas, all true", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "oneOf": [true, true, true] - }, - "tests": [ - { - "description": "any value is invalid", - "data": "foo", - "valid": false - } - ] - }, - { - "description": "oneOf with boolean schemas, one true", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "oneOf": [true, false, false] - }, - "tests": [ - { - "description": "any value is valid", - "data": "foo", - "valid": true - } - ] - }, - { - "description": "oneOf with boolean schemas, more than one true", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "oneOf": [true, true, false] - }, - "tests": [ - { - "description": "any value is invalid", - "data": "foo", - "valid": false - } - ] - }, - { - "description": "oneOf with boolean schemas, all false", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "oneOf": [false, false, false] - }, - "tests": [ - { - "description": "any value is invalid", - "data": "foo", - "valid": false - } - ] - }, - { - "description": "oneOf complex types", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "oneOf": [ - { - "properties": { - "bar": {"type": "integer"} - }, - "required": ["bar"] - }, - { - "properties": { - "foo": {"type": "string"} - }, - "required": ["foo"] - } - ] - }, - "tests": [ - { - "description": "first oneOf valid (complex)", - "data": {"bar": 2}, - "valid": true - }, - { - "description": "second oneOf valid (complex)", - "data": {"foo": "baz"}, - "valid": true - }, - { - "description": "both oneOf valid (complex)", - "data": {"foo": "baz", "bar": 2}, - "valid": false - }, - { - "description": "neither oneOf valid (complex)", - "data": {"foo": 2, "bar": "quux"}, - "valid": false - } - ] - }, - { - "description": "oneOf with empty schema", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "oneOf": [ - { "type": "number" }, - {} - ] - }, - "tests": [ - { - "description": "one valid - valid", - "data": "foo", - "valid": true - }, - { - "description": "both valid - invalid", - "data": 123, - "valid": false - } - ] - }, - { - "description": "oneOf with required", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "object", - "oneOf": [ - { "required": ["foo", "bar"] }, - { "required": ["foo", "baz"] } - ] - }, - "tests": [ - { - "description": "both invalid - invalid", - "data": {"bar": 2}, - "valid": false - }, - { - "description": "first valid - valid", - "data": {"foo": 1, "bar": 2}, - "valid": true - }, - { - "description": "second valid - valid", - "data": {"foo": 1, "baz": 3}, - "valid": true - }, - { - "description": "both valid - invalid", - "data": {"foo": 1, "bar": 2, "baz" : 3}, - "valid": false - } - ] - }, - { - "description": "oneOf with missing optional property", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "oneOf": [ - { - "properties": { - "bar": true, - "baz": true - }, - "required": ["bar"] - }, - { - "properties": { - "foo": true - }, - "required": ["foo"] - } - ] - }, - "tests": [ - { - "description": "first oneOf valid", - "data": {"bar": 8}, - "valid": true - }, - { - "description": "second oneOf valid", - "data": {"foo": "foo"}, - "valid": true - }, - { - "description": "both oneOf valid", - "data": {"foo": "foo", "bar": 8}, - "valid": false - }, - { - "description": "neither oneOf valid", - "data": {"baz": "quux"}, - "valid": false - } - ] - }, - { - "description": "nested oneOf, to check validation semantics", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "oneOf": [ - { - "oneOf": [ - { - "type": "null" - } - ] - } - ] - }, - "tests": [ - { - "description": "null is valid", - "data": null, - "valid": true - }, - { - "description": "anything non-null is invalid", - "data": 123, - "valid": false - } - ] - } -] diff --git a/jsonschema/testdata/draft2020-12/pattern.json b/jsonschema/testdata/draft2020-12/pattern.json deleted file mode 100644 index af0b8d89..00000000 --- a/jsonschema/testdata/draft2020-12/pattern.json +++ /dev/null @@ -1,65 +0,0 @@ -[ - { - "description": "pattern validation", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "pattern": "^a*$" - }, - "tests": [ - { - "description": "a matching pattern is valid", - "data": "aaa", - "valid": true - }, - { - "description": "a non-matching pattern is invalid", - "data": "abc", - "valid": false - }, - { - "description": "ignores booleans", - "data": true, - "valid": true - }, - { - "description": "ignores integers", - "data": 123, - "valid": true - }, - { - "description": "ignores floats", - "data": 1.0, - "valid": true - }, - { - "description": "ignores objects", - "data": {}, - "valid": true - }, - { - "description": "ignores arrays", - "data": [], - "valid": true - }, - { - "description": "ignores null", - "data": null, - "valid": true - } - ] - }, - { - "description": "pattern is not anchored", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "pattern": "a+" - }, - "tests": [ - { - "description": "matches a substring", - "data": "xxaayy", - "valid": true - } - ] - } -] diff --git a/jsonschema/testdata/draft2020-12/patternProperties.json b/jsonschema/testdata/draft2020-12/patternProperties.json deleted file mode 100644 index 81829c71..00000000 --- a/jsonschema/testdata/draft2020-12/patternProperties.json +++ /dev/null @@ -1,176 +0,0 @@ -[ - { - "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 - } - ] - } -] diff --git a/jsonschema/testdata/draft2020-12/prefixItems.json b/jsonschema/testdata/draft2020-12/prefixItems.json deleted file mode 100644 index 0adfc069..00000000 --- a/jsonschema/testdata/draft2020-12/prefixItems.json +++ /dev/null @@ -1,104 +0,0 @@ -[ - { - "description": "a schema given for prefixItems", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "prefixItems": [ - {"type": "integer"}, - {"type": "string"} - ] - }, - "tests": [ - { - "description": "correct types", - "data": [ 1, "foo" ], - "valid": true - }, - { - "description": "wrong types", - "data": [ "foo", 1 ], - "valid": false - }, - { - "description": "incomplete array of items", - "data": [ 1 ], - "valid": true - }, - { - "description": "array with additional items", - "data": [ 1, "foo", true ], - "valid": true - }, - { - "description": "empty array", - "data": [ ], - "valid": true - }, - { - "description": "JavaScript pseudo-array is valid", - "data": { - "0": "invalid", - "1": "valid", - "length": 2 - }, - "valid": true - } - ] - }, - { - "description": "prefixItems with boolean schemas", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "prefixItems": [true, false] - }, - "tests": [ - { - "description": "array with one item is valid", - "data": [ 1 ], - "valid": true - }, - { - "description": "array with two items is invalid", - "data": [ 1, "foo" ], - "valid": false - }, - { - "description": "empty array is valid", - "data": [], - "valid": true - } - ] - }, - { - "description": "additional items are allowed by default", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "prefixItems": [{"type": "integer"}] - }, - "tests": [ - { - "description": "only the first item is validated", - "data": [1, "foo", false], - "valid": true - } - ] - }, - { - "description": "prefixItems with null instance elements", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "prefixItems": [ - { - "type": "null" - } - ] - }, - "tests": [ - { - "description": "allows null elements", - "data": [ null ], - "valid": true - } - ] - } -] diff --git a/jsonschema/testdata/draft2020-12/properties.json b/jsonschema/testdata/draft2020-12/properties.json deleted file mode 100644 index 523dcde7..00000000 --- a/jsonschema/testdata/draft2020-12/properties.json +++ /dev/null @@ -1,242 +0,0 @@ -[ - { - "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 - } - ] - } -] diff --git a/jsonschema/testdata/draft2020-12/propertyNames.json b/jsonschema/testdata/draft2020-12/propertyNames.json deleted file mode 100644 index b4780088..00000000 --- a/jsonschema/testdata/draft2020-12/propertyNames.json +++ /dev/null @@ -1,168 +0,0 @@ -[ - { - "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 - } - ] - } -] diff --git a/jsonschema/testdata/draft2020-12/ref.json b/jsonschema/testdata/draft2020-12/ref.json deleted file mode 100644 index 0ac02fb9..00000000 --- a/jsonschema/testdata/draft2020-12/ref.json +++ /dev/null @@ -1,1052 +0,0 @@ -[ - { - "description": "root pointer ref", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "properties": { - "foo": {"$ref": "#"} - }, - "additionalProperties": false - }, - "tests": [ - { - "description": "match", - "data": {"foo": false}, - "valid": true - }, - { - "description": "recursive match", - "data": {"foo": {"foo": false}}, - "valid": true - }, - { - "description": "mismatch", - "data": {"bar": false}, - "valid": false - }, - { - "description": "recursive mismatch", - "data": {"foo": {"bar": false}}, - "valid": false - } - ] - }, - { - "description": "relative pointer ref to object", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "properties": { - "foo": {"type": "integer"}, - "bar": {"$ref": "#/properties/foo"} - } - }, - "tests": [ - { - "description": "match", - "data": {"bar": 3}, - "valid": true - }, - { - "description": "mismatch", - "data": {"bar": true}, - "valid": false - } - ] - }, - { - "description": "relative pointer ref to array", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "prefixItems": [ - {"type": "integer"}, - {"$ref": "#/prefixItems/0"} - ] - }, - "tests": [ - { - "description": "match array", - "data": [1, 2], - "valid": true - }, - { - "description": "mismatch array", - "data": [1, "foo"], - "valid": false - } - ] - }, - { - "description": "escaped pointer ref", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$defs": { - "tilde~field": {"type": "integer"}, - "slash/field": {"type": "integer"}, - "percent%field": {"type": "integer"} - }, - "properties": { - "tilde": {"$ref": "#/$defs/tilde~0field"}, - "slash": {"$ref": "#/$defs/slash~1field"}, - "percent": {"$ref": "#/$defs/percent%25field"} - } - }, - "tests": [ - { - "description": "slash invalid", - "data": {"slash": "aoeu"}, - "valid": false - }, - { - "description": "tilde invalid", - "data": {"tilde": "aoeu"}, - "valid": false - }, - { - "description": "percent invalid", - "data": {"percent": "aoeu"}, - "valid": false - }, - { - "description": "slash valid", - "data": {"slash": 123}, - "valid": true - }, - { - "description": "tilde valid", - "data": {"tilde": 123}, - "valid": true - }, - { - "description": "percent valid", - "data": {"percent": 123}, - "valid": true - } - ] - }, - { - "description": "nested refs", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$defs": { - "a": {"type": "integer"}, - "b": {"$ref": "#/$defs/a"}, - "c": {"$ref": "#/$defs/b"} - }, - "$ref": "#/$defs/c" - }, - "tests": [ - { - "description": "nested ref valid", - "data": 5, - "valid": true - }, - { - "description": "nested ref invalid", - "data": "a", - "valid": false - } - ] - }, - { - "description": "ref applies alongside sibling keywords", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$defs": { - "reffed": { - "type": "array" - } - }, - "properties": { - "foo": { - "$ref": "#/$defs/reffed", - "maxItems": 2 - } - } - }, - "tests": [ - { - "description": "ref valid, maxItems valid", - "data": { "foo": [] }, - "valid": true - }, - { - "description": "ref valid, maxItems invalid", - "data": { "foo": [1, 2, 3] }, - "valid": false - }, - { - "description": "ref invalid", - "data": { "foo": "string" }, - "valid": false - } - ] - }, - { - "description": "remote ref, containing refs itself", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$ref": "https://json-schema.org/draft/2020-12/schema" - }, - "tests": [ - { - "description": "remote ref valid", - "data": {"minLength": 1}, - "valid": true - }, - { - "description": "remote ref invalid", - "data": {"minLength": -1}, - "valid": false - } - ] - }, - { - "description": "property named $ref that is not a reference", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "properties": { - "$ref": {"type": "string"} - } - }, - "tests": [ - { - "description": "property named $ref valid", - "data": {"$ref": "a"}, - "valid": true - }, - { - "description": "property named $ref invalid", - "data": {"$ref": 2}, - "valid": false - } - ] - }, - { - "description": "property named $ref, containing an actual $ref", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "properties": { - "$ref": {"$ref": "#/$defs/is-string"} - }, - "$defs": { - "is-string": { - "type": "string" - } - } - }, - "tests": [ - { - "description": "property named $ref valid", - "data": {"$ref": "a"}, - "valid": true - }, - { - "description": "property named $ref invalid", - "data": {"$ref": 2}, - "valid": false - } - ] - }, - { - "description": "$ref to boolean schema true", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$ref": "#/$defs/bool", - "$defs": { - "bool": true - } - }, - "tests": [ - { - "description": "any value is valid", - "data": "foo", - "valid": true - } - ] - }, - { - "description": "$ref to boolean schema false", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$ref": "#/$defs/bool", - "$defs": { - "bool": false - } - }, - "tests": [ - { - "description": "any value is invalid", - "data": "foo", - "valid": false - } - ] - }, - { - "description": "Recursive references between schemas", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "http://localhost:1234/draft2020-12/tree", - "description": "tree of nodes", - "type": "object", - "properties": { - "meta": {"type": "string"}, - "nodes": { - "type": "array", - "items": {"$ref": "node"} - } - }, - "required": ["meta", "nodes"], - "$defs": { - "node": { - "$id": "http://localhost:1234/draft2020-12/node", - "description": "node", - "type": "object", - "properties": { - "value": {"type": "number"}, - "subtree": {"$ref": "tree"} - }, - "required": ["value"] - } - } - }, - "tests": [ - { - "description": "valid tree", - "data": { - "meta": "root", - "nodes": [ - { - "value": 1, - "subtree": { - "meta": "child", - "nodes": [ - {"value": 1.1}, - {"value": 1.2} - ] - } - }, - { - "value": 2, - "subtree": { - "meta": "child", - "nodes": [ - {"value": 2.1}, - {"value": 2.2} - ] - } - } - ] - }, - "valid": true - }, - { - "description": "invalid tree", - "data": { - "meta": "root", - "nodes": [ - { - "value": 1, - "subtree": { - "meta": "child", - "nodes": [ - {"value": "string is invalid"}, - {"value": 1.2} - ] - } - }, - { - "value": 2, - "subtree": { - "meta": "child", - "nodes": [ - {"value": 2.1}, - {"value": 2.2} - ] - } - } - ] - }, - "valid": false - } - ] - }, - { - "description": "refs with quote", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "properties": { - "foo\"bar": {"$ref": "#/$defs/foo%22bar"} - }, - "$defs": { - "foo\"bar": {"type": "number"} - } - }, - "tests": [ - { - "description": "object with numbers is valid", - "data": { - "foo\"bar": 1 - }, - "valid": true - }, - { - "description": "object with strings is invalid", - "data": { - "foo\"bar": "1" - }, - "valid": false - } - ] - }, - { - "description": "ref creates new scope when adjacent to keywords", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$defs": { - "A": { - "unevaluatedProperties": false - } - }, - "properties": { - "prop1": { - "type": "string" - } - }, - "$ref": "#/$defs/A" - }, - "tests": [ - { - "description": "referenced subschema doesn't see annotations from properties", - "data": { - "prop1": "match" - }, - "valid": false - } - ] - }, - { - "description": "naive replacement of $ref with its destination is not correct", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$defs": { - "a_string": { "type": "string" } - }, - "enum": [ - { "$ref": "#/$defs/a_string" } - ] - }, - "tests": [ - { - "description": "do not evaluate the $ref inside the enum, matching any string", - "data": "this is a string", - "valid": false - }, - { - "description": "do not evaluate the $ref inside the enum, definition exact match", - "data": { "type": "string" }, - "valid": false - }, - { - "description": "match the enum exactly", - "data": { "$ref": "#/$defs/a_string" }, - "valid": true - } - ] - }, - { - "description": "refs with relative uris and defs", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "http://example.com/schema-relative-uri-defs1.json", - "properties": { - "foo": { - "$id": "schema-relative-uri-defs2.json", - "$defs": { - "inner": { - "properties": { - "bar": { "type": "string" } - } - } - }, - "$ref": "#/$defs/inner" - } - }, - "$ref": "schema-relative-uri-defs2.json" - }, - "tests": [ - { - "description": "invalid on inner field", - "data": { - "foo": { - "bar": 1 - }, - "bar": "a" - }, - "valid": false - }, - { - "description": "invalid on outer field", - "data": { - "foo": { - "bar": "a" - }, - "bar": 1 - }, - "valid": false - }, - { - "description": "valid on both fields", - "data": { - "foo": { - "bar": "a" - }, - "bar": "a" - }, - "valid": true - } - ] - }, - { - "description": "relative refs with absolute uris and defs", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "http://example.com/schema-refs-absolute-uris-defs1.json", - "properties": { - "foo": { - "$id": "http://example.com/schema-refs-absolute-uris-defs2.json", - "$defs": { - "inner": { - "properties": { - "bar": { "type": "string" } - } - } - }, - "$ref": "#/$defs/inner" - } - }, - "$ref": "schema-refs-absolute-uris-defs2.json" - }, - "tests": [ - { - "description": "invalid on inner field", - "data": { - "foo": { - "bar": 1 - }, - "bar": "a" - }, - "valid": false - }, - { - "description": "invalid on outer field", - "data": { - "foo": { - "bar": "a" - }, - "bar": 1 - }, - "valid": false - }, - { - "description": "valid on both fields", - "data": { - "foo": { - "bar": "a" - }, - "bar": "a" - }, - "valid": true - } - ] - }, - { - "description": "$id must be resolved against nearest parent, not just immediate parent", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "http://example.com/a.json", - "$defs": { - "x": { - "$id": "http://example.com/b/c.json", - "not": { - "$defs": { - "y": { - "$id": "d.json", - "type": "number" - } - } - } - } - }, - "allOf": [ - { - "$ref": "http://example.com/b/d.json" - } - ] - }, - "tests": [ - { - "description": "number is valid", - "data": 1, - "valid": true - }, - { - "description": "non-number is invalid", - "data": "a", - "valid": false - } - ] - }, - { - "description": "order of evaluation: $id and $ref", - "schema": { - "$comment": "$id must be evaluated before $ref to get the proper $ref destination", - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://example.com/draft2020-12/ref-and-id1/base.json", - "$ref": "int.json", - "$defs": { - "bigint": { - "$comment": "canonical uri: https://example.com/ref-and-id1/int.json", - "$id": "int.json", - "maximum": 10 - }, - "smallint": { - "$comment": "canonical uri: https://example.com/ref-and-id1-int.json", - "$id": "/draft2020-12/ref-and-id1-int.json", - "maximum": 2 - } - } - }, - "tests": [ - { - "description": "data is valid against first definition", - "data": 5, - "valid": true - }, - { - "description": "data is invalid against first definition", - "data": 50, - "valid": false - } - ] - }, - { - "description": "order of evaluation: $id and $anchor and $ref", - "schema": { - "$comment": "$id must be evaluated before $ref to get the proper $ref destination", - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://example.com/draft2020-12/ref-and-id2/base.json", - "$ref": "#bigint", - "$defs": { - "bigint": { - "$comment": "canonical uri: /ref-and-id2/base.json#/$defs/bigint; another valid uri for this location: /ref-and-id2/base.json#bigint", - "$anchor": "bigint", - "maximum": 10 - }, - "smallint": { - "$comment": "canonical uri: https://example.com/ref-and-id2#/$defs/smallint; another valid uri for this location: https://example.com/ref-and-id2/#bigint", - "$id": "https://example.com/draft2020-12/ref-and-id2/", - "$anchor": "bigint", - "maximum": 2 - } - } - }, - "tests": [ - { - "description": "data is valid against first definition", - "data": 5, - "valid": true - }, - { - "description": "data is invalid against first definition", - "data": 50, - "valid": false - } - ] - }, - { - "description": "simple URN base URI with $ref via the URN", - "schema": { - "$comment": "URIs do not have to have HTTP(s) schemes", - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "urn:uuid:deadbeef-1234-ffff-ffff-4321feebdaed", - "minimum": 30, - "properties": { - "foo": {"$ref": "urn:uuid:deadbeef-1234-ffff-ffff-4321feebdaed"} - } - }, - "tests": [ - { - "description": "valid under the URN IDed schema", - "data": {"foo": 37}, - "valid": true - }, - { - "description": "invalid under the URN IDed schema", - "data": {"foo": 12}, - "valid": false - } - ] - }, - { - "description": "simple URN base URI with JSON pointer", - "schema": { - "$comment": "URIs do not have to have HTTP(s) schemes", - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "urn:uuid:deadbeef-1234-00ff-ff00-4321feebdaed", - "properties": { - "foo": {"$ref": "#/$defs/bar"} - }, - "$defs": { - "bar": {"type": "string"} - } - }, - "tests": [ - { - "description": "a string is valid", - "data": {"foo": "bar"}, - "valid": true - }, - { - "description": "a non-string is invalid", - "data": {"foo": 12}, - "valid": false - } - ] - }, - { - "description": "URN base URI with NSS", - "schema": { - "$comment": "RFC 8141 §2.2", - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "urn:example:1/406/47452/2", - "properties": { - "foo": {"$ref": "#/$defs/bar"} - }, - "$defs": { - "bar": {"type": "string"} - } - }, - "tests": [ - { - "description": "a string is valid", - "data": {"foo": "bar"}, - "valid": true - }, - { - "description": "a non-string is invalid", - "data": {"foo": 12}, - "valid": false - } - ] - }, - { - "description": "URN base URI with r-component", - "schema": { - "$comment": "RFC 8141 §2.3.1", - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "urn:example:foo-bar-baz-qux?+CCResolve:cc=uk", - "properties": { - "foo": {"$ref": "#/$defs/bar"} - }, - "$defs": { - "bar": {"type": "string"} - } - }, - "tests": [ - { - "description": "a string is valid", - "data": {"foo": "bar"}, - "valid": true - }, - { - "description": "a non-string is invalid", - "data": {"foo": 12}, - "valid": false - } - ] - }, - { - "description": "URN base URI with q-component", - "schema": { - "$comment": "RFC 8141 §2.3.2", - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "urn:example:weather?=op=map&lat=39.56&lon=-104.85&datetime=1969-07-21T02:56:15Z", - "properties": { - "foo": {"$ref": "#/$defs/bar"} - }, - "$defs": { - "bar": {"type": "string"} - } - }, - "tests": [ - { - "description": "a string is valid", - "data": {"foo": "bar"}, - "valid": true - }, - { - "description": "a non-string is invalid", - "data": {"foo": 12}, - "valid": false - } - ] - }, - { - "description": "URN base URI with URN and JSON pointer ref", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "urn:uuid:deadbeef-1234-0000-0000-4321feebdaed", - "properties": { - "foo": {"$ref": "urn:uuid:deadbeef-1234-0000-0000-4321feebdaed#/$defs/bar"} - }, - "$defs": { - "bar": {"type": "string"} - } - }, - "tests": [ - { - "description": "a string is valid", - "data": {"foo": "bar"}, - "valid": true - }, - { - "description": "a non-string is invalid", - "data": {"foo": 12}, - "valid": false - } - ] - }, - { - "description": "URN base URI with URN and anchor ref", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "urn:uuid:deadbeef-1234-ff00-00ff-4321feebdaed", - "properties": { - "foo": {"$ref": "urn:uuid:deadbeef-1234-ff00-00ff-4321feebdaed#something"} - }, - "$defs": { - "bar": { - "$anchor": "something", - "type": "string" - } - } - }, - "tests": [ - { - "description": "a string is valid", - "data": {"foo": "bar"}, - "valid": true - }, - { - "description": "a non-string is invalid", - "data": {"foo": 12}, - "valid": false - } - ] - }, - { - "description": "URN ref with nested pointer ref", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$ref": "urn:uuid:deadbeef-4321-ffff-ffff-1234feebdaed", - "$defs": { - "foo": { - "$id": "urn:uuid:deadbeef-4321-ffff-ffff-1234feebdaed", - "$defs": {"bar": {"type": "string"}}, - "$ref": "#/$defs/bar" - } - } - }, - "tests": [ - { - "description": "a string is valid", - "data": "bar", - "valid": true - }, - { - "description": "a non-string is invalid", - "data": 12, - "valid": false - } - ] - }, - { - "description": "ref to if", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$ref": "http://example.com/ref/if", - "if": { - "$id": "http://example.com/ref/if", - "type": "integer" - } - }, - "tests": [ - { - "description": "a non-integer is invalid due to the $ref", - "data": "foo", - "valid": false - }, - { - "description": "an integer is valid", - "data": 12, - "valid": true - } - ] - }, - { - "description": "ref to then", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$ref": "http://example.com/ref/then", - "then": { - "$id": "http://example.com/ref/then", - "type": "integer" - } - }, - "tests": [ - { - "description": "a non-integer is invalid due to the $ref", - "data": "foo", - "valid": false - }, - { - "description": "an integer is valid", - "data": 12, - "valid": true - } - ] - }, - { - "description": "ref to else", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$ref": "http://example.com/ref/else", - "else": { - "$id": "http://example.com/ref/else", - "type": "integer" - } - }, - "tests": [ - { - "description": "a non-integer is invalid due to the $ref", - "data": "foo", - "valid": false - }, - { - "description": "an integer is valid", - "data": 12, - "valid": true - } - ] - }, - { - "description": "ref with absolute-path-reference", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "http://example.com/ref/absref.json", - "$defs": { - "a": { - "$id": "http://example.com/ref/absref/foobar.json", - "type": "number" - }, - "b": { - "$id": "http://example.com/absref/foobar.json", - "type": "string" - } - }, - "$ref": "/absref/foobar.json" - }, - "tests": [ - { - "description": "a string is valid", - "data": "foo", - "valid": true - }, - { - "description": "an integer is invalid", - "data": 12, - "valid": false - } - ] - }, - { - "description": "$id with file URI still resolves pointers - *nix", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "file:///folder/file.json", - "$defs": { - "foo": { - "type": "number" - } - }, - "$ref": "#/$defs/foo" - }, - "tests": [ - { - "description": "number is valid", - "data": 1, - "valid": true - }, - { - "description": "non-number is invalid", - "data": "a", - "valid": false - } - ] - }, - { - "description": "$id with file URI still resolves pointers - windows", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "file:///c:/folder/file.json", - "$defs": { - "foo": { - "type": "number" - } - }, - "$ref": "#/$defs/foo" - }, - "tests": [ - { - "description": "number is valid", - "data": 1, - "valid": true - }, - { - "description": "non-number is invalid", - "data": "a", - "valid": false - } - ] - }, - { - "description": "empty tokens in $ref json-pointer", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$defs": { - "": { - "$defs": { - "": { "type": "number" } - } - } - }, - "allOf": [ - { - "$ref": "#/$defs//$defs/" - } - ] - }, - "tests": [ - { - "description": "number is valid", - "data": 1, - "valid": true - }, - { - "description": "non-number is invalid", - "data": "a", - "valid": false - } - ] - } -] diff --git a/jsonschema/testdata/draft2020-12/refRemote.json b/jsonschema/testdata/draft2020-12/refRemote.json deleted file mode 100644 index 047ac74c..00000000 --- a/jsonschema/testdata/draft2020-12/refRemote.json +++ /dev/null @@ -1,342 +0,0 @@ -[ - { - "description": "remote ref", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$ref": "http://localhost:1234/draft2020-12/integer.json" - }, - "tests": [ - { - "description": "remote ref valid", - "data": 1, - "valid": true - }, - { - "description": "remote ref invalid", - "data": "a", - "valid": false - } - ] - }, - { - "description": "fragment within remote ref", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$ref": "http://localhost:1234/draft2020-12/subSchemas.json#/$defs/integer" - }, - "tests": [ - { - "description": "remote fragment valid", - "data": 1, - "valid": true - }, - { - "description": "remote fragment invalid", - "data": "a", - "valid": false - } - ] - }, - { - "description": "anchor within remote ref", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$ref": "http://localhost:1234/draft2020-12/locationIndependentIdentifier.json#foo" - }, - "tests": [ - { - "description": "remote anchor valid", - "data": 1, - "valid": true - }, - { - "description": "remote anchor invalid", - "data": "a", - "valid": false - } - ] - }, - { - "description": "ref within remote ref", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$ref": "http://localhost:1234/draft2020-12/subSchemas.json#/$defs/refToInteger" - }, - "tests": [ - { - "description": "ref within ref valid", - "data": 1, - "valid": true - }, - { - "description": "ref within ref invalid", - "data": "a", - "valid": false - } - ] - }, - { - "description": "base URI change", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "http://localhost:1234/draft2020-12/", - "items": { - "$id": "baseUriChange/", - "items": {"$ref": "folderInteger.json"} - } - }, - "tests": [ - { - "description": "base URI change ref valid", - "data": [[1]], - "valid": true - }, - { - "description": "base URI change ref invalid", - "data": [["a"]], - "valid": false - } - ] - }, - { - "description": "base URI change - change folder", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "http://localhost:1234/draft2020-12/scope_change_defs1.json", - "type" : "object", - "properties": {"list": {"$ref": "baseUriChangeFolder/"}}, - "$defs": { - "baz": { - "$id": "baseUriChangeFolder/", - "type": "array", - "items": {"$ref": "folderInteger.json"} - } - } - }, - "tests": [ - { - "description": "number is valid", - "data": {"list": [1]}, - "valid": true - }, - { - "description": "string is invalid", - "data": {"list": ["a"]}, - "valid": false - } - ] - }, - { - "description": "base URI change - change folder in subschema", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "http://localhost:1234/draft2020-12/scope_change_defs2.json", - "type" : "object", - "properties": {"list": {"$ref": "baseUriChangeFolderInSubschema/#/$defs/bar"}}, - "$defs": { - "baz": { - "$id": "baseUriChangeFolderInSubschema/", - "$defs": { - "bar": { - "type": "array", - "items": {"$ref": "folderInteger.json"} - } - } - } - } - }, - "tests": [ - { - "description": "number is valid", - "data": {"list": [1]}, - "valid": true - }, - { - "description": "string is invalid", - "data": {"list": ["a"]}, - "valid": false - } - ] - }, - { - "description": "root ref in remote ref", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "http://localhost:1234/draft2020-12/object", - "type": "object", - "properties": { - "name": {"$ref": "name-defs.json#/$defs/orNull"} - } - }, - "tests": [ - { - "description": "string is valid", - "data": { - "name": "foo" - }, - "valid": true - }, - { - "description": "null is valid", - "data": { - "name": null - }, - "valid": true - }, - { - "description": "object is invalid", - "data": { - "name": { - "name": null - } - }, - "valid": false - } - ] - }, - { - "description": "remote ref with ref to defs", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "http://localhost:1234/draft2020-12/schema-remote-ref-ref-defs1.json", - "$ref": "ref-and-defs.json" - }, - "tests": [ - { - "description": "invalid", - "data": { - "bar": 1 - }, - "valid": false - }, - { - "description": "valid", - "data": { - "bar": "a" - }, - "valid": true - } - ] - }, - { - "description": "Location-independent identifier in remote ref", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$ref": "http://localhost:1234/draft2020-12/locationIndependentIdentifier.json#/$defs/refToInteger" - }, - "tests": [ - { - "description": "integer is valid", - "data": 1, - "valid": true - }, - { - "description": "string is invalid", - "data": "foo", - "valid": false - } - ] - }, - { - "description": "retrieved nested refs resolve relative to their URI not $id", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "http://localhost:1234/draft2020-12/some-id", - "properties": { - "name": {"$ref": "nested/foo-ref-string.json"} - } - }, - "tests": [ - { - "description": "number is invalid", - "data": { - "name": {"foo": 1} - }, - "valid": false - }, - { - "description": "string is valid", - "data": { - "name": {"foo": "a"} - }, - "valid": true - } - ] - }, - { - "description": "remote HTTP ref with different $id", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$ref": "http://localhost:1234/different-id-ref-string.json" - }, - "tests": [ - { - "description": "number is invalid", - "data": 1, - "valid": false - }, - { - "description": "string is valid", - "data": "foo", - "valid": true - } - ] - }, - { - "description": "remote HTTP ref with different URN $id", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$ref": "http://localhost:1234/urn-ref-string.json" - }, - "tests": [ - { - "description": "number is invalid", - "data": 1, - "valid": false - }, - { - "description": "string is valid", - "data": "foo", - "valid": true - } - ] - }, - { - "description": "remote HTTP ref with nested absolute ref", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$ref": "http://localhost:1234/nested-absolute-ref-to-string.json" - }, - "tests": [ - { - "description": "number is invalid", - "data": 1, - "valid": false - }, - { - "description": "string is valid", - "data": "foo", - "valid": true - } - ] - }, - { - "description": "$ref to $ref finds detached $anchor", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$ref": "http://localhost:1234/draft2020-12/detached-ref.json#/$defs/foo" - }, - "tests": [ - { - "description": "number is valid", - "data": 1, - "valid": true - }, - { - "description": "non-number is invalid", - "data": "a", - "valid": false - } - ] - } -] diff --git a/jsonschema/testdata/draft2020-12/required.json b/jsonschema/testdata/draft2020-12/required.json deleted file mode 100644 index e66f29f2..00000000 --- a/jsonschema/testdata/draft2020-12/required.json +++ /dev/null @@ -1,158 +0,0 @@ -[ - { - "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 - } - ] - } -] diff --git a/jsonschema/testdata/draft2020-12/type.json b/jsonschema/testdata/draft2020-12/type.json deleted file mode 100644 index 2123c408..00000000 --- a/jsonschema/testdata/draft2020-12/type.json +++ /dev/null @@ -1,501 +0,0 @@ -[ - { - "description": "integer type matches integers", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "integer" - }, - "tests": [ - { - "description": "an integer is an integer", - "data": 1, - "valid": true - }, - { - "description": "a float with zero fractional part is an integer", - "data": 1.0, - "valid": true - }, - { - "description": "a float is not an integer", - "data": 1.1, - "valid": false - }, - { - "description": "a string is not an integer", - "data": "foo", - "valid": false - }, - { - "description": "a string is still not an integer, even if it looks like one", - "data": "1", - "valid": false - }, - { - "description": "an object is not an integer", - "data": {}, - "valid": false - }, - { - "description": "an array is not an integer", - "data": [], - "valid": false - }, - { - "description": "a boolean is not an integer", - "data": true, - "valid": false - }, - { - "description": "null is not an integer", - "data": null, - "valid": false - } - ] - }, - { - "description": "number type matches numbers", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "number" - }, - "tests": [ - { - "description": "an integer is a number", - "data": 1, - "valid": true - }, - { - "description": "a float with zero fractional part is a number (and an integer)", - "data": 1.0, - "valid": true - }, - { - "description": "a float is a number", - "data": 1.1, - "valid": true - }, - { - "description": "a string is not a number", - "data": "foo", - "valid": false - }, - { - "description": "a string is still not a number, even if it looks like one", - "data": "1", - "valid": false - }, - { - "description": "an object is not a number", - "data": {}, - "valid": false - }, - { - "description": "an array is not a number", - "data": [], - "valid": false - }, - { - "description": "a boolean is not a number", - "data": true, - "valid": false - }, - { - "description": "null is not a number", - "data": null, - "valid": false - } - ] - }, - { - "description": "string type matches strings", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "string" - }, - "tests": [ - { - "description": "1 is not a string", - "data": 1, - "valid": false - }, - { - "description": "a float is not a string", - "data": 1.1, - "valid": false - }, - { - "description": "a string is a string", - "data": "foo", - "valid": true - }, - { - "description": "a string is still a string, even if it looks like a number", - "data": "1", - "valid": true - }, - { - "description": "an empty string is still a string", - "data": "", - "valid": true - }, - { - "description": "an object is not a string", - "data": {}, - "valid": false - }, - { - "description": "an array is not a string", - "data": [], - "valid": false - }, - { - "description": "a boolean is not a string", - "data": true, - "valid": false - }, - { - "description": "null is not a string", - "data": null, - "valid": false - } - ] - }, - { - "description": "object type matches objects", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "object" - }, - "tests": [ - { - "description": "an integer is not an object", - "data": 1, - "valid": false - }, - { - "description": "a float is not an object", - "data": 1.1, - "valid": false - }, - { - "description": "a string is not an object", - "data": "foo", - "valid": false - }, - { - "description": "an object is an object", - "data": {}, - "valid": true - }, - { - "description": "an array is not an object", - "data": [], - "valid": false - }, - { - "description": "a boolean is not an object", - "data": true, - "valid": false - }, - { - "description": "null is not an object", - "data": null, - "valid": false - } - ] - }, - { - "description": "array type matches arrays", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "array" - }, - "tests": [ - { - "description": "an integer is not an array", - "data": 1, - "valid": false - }, - { - "description": "a float is not an array", - "data": 1.1, - "valid": false - }, - { - "description": "a string is not an array", - "data": "foo", - "valid": false - }, - { - "description": "an object is not an array", - "data": {}, - "valid": false - }, - { - "description": "an array is an array", - "data": [], - "valid": true - }, - { - "description": "a boolean is not an array", - "data": true, - "valid": false - }, - { - "description": "null is not an array", - "data": null, - "valid": false - } - ] - }, - { - "description": "boolean type matches booleans", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "boolean" - }, - "tests": [ - { - "description": "an integer is not a boolean", - "data": 1, - "valid": false - }, - { - "description": "zero is not a boolean", - "data": 0, - "valid": false - }, - { - "description": "a float is not a boolean", - "data": 1.1, - "valid": false - }, - { - "description": "a string is not a boolean", - "data": "foo", - "valid": false - }, - { - "description": "an empty string is not a boolean", - "data": "", - "valid": false - }, - { - "description": "an object is not a boolean", - "data": {}, - "valid": false - }, - { - "description": "an array is not a boolean", - "data": [], - "valid": false - }, - { - "description": "true is a boolean", - "data": true, - "valid": true - }, - { - "description": "false is a boolean", - "data": false, - "valid": true - }, - { - "description": "null is not a boolean", - "data": null, - "valid": false - } - ] - }, - { - "description": "null type matches only the null object", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "null" - }, - "tests": [ - { - "description": "an integer is not null", - "data": 1, - "valid": false - }, - { - "description": "a float is not null", - "data": 1.1, - "valid": false - }, - { - "description": "zero is not null", - "data": 0, - "valid": false - }, - { - "description": "a string is not null", - "data": "foo", - "valid": false - }, - { - "description": "an empty string is not null", - "data": "", - "valid": false - }, - { - "description": "an object is not null", - "data": {}, - "valid": false - }, - { - "description": "an array is not null", - "data": [], - "valid": false - }, - { - "description": "true is not null", - "data": true, - "valid": false - }, - { - "description": "false is not null", - "data": false, - "valid": false - }, - { - "description": "null is null", - "data": null, - "valid": true - } - ] - }, - { - "description": "multiple types can be specified in an array", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": ["integer", "string"] - }, - "tests": [ - { - "description": "an integer is valid", - "data": 1, - "valid": true - }, - { - "description": "a string is valid", - "data": "foo", - "valid": true - }, - { - "description": "a float is invalid", - "data": 1.1, - "valid": false - }, - { - "description": "an object is invalid", - "data": {}, - "valid": false - }, - { - "description": "an array is invalid", - "data": [], - "valid": false - }, - { - "description": "a boolean is invalid", - "data": true, - "valid": false - }, - { - "description": "null is invalid", - "data": null, - "valid": false - } - ] - }, - { - "description": "type as array with one item", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": ["string"] - }, - "tests": [ - { - "description": "string is valid", - "data": "foo", - "valid": true - }, - { - "description": "number is invalid", - "data": 123, - "valid": false - } - ] - }, - { - "description": "type: array or object", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": ["array", "object"] - }, - "tests": [ - { - "description": "array is valid", - "data": [1,2,3], - "valid": true - }, - { - "description": "object is valid", - "data": {"foo": 123}, - "valid": true - }, - { - "description": "number is invalid", - "data": 123, - "valid": false - }, - { - "description": "string is invalid", - "data": "foo", - "valid": false - }, - { - "description": "null is invalid", - "data": null, - "valid": false - } - ] - }, - { - "description": "type: array, object or null", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": ["array", "object", "null"] - }, - "tests": [ - { - "description": "array is valid", - "data": [1,2,3], - "valid": true - }, - { - "description": "object is valid", - "data": {"foo": 123}, - "valid": true - }, - { - "description": "null is valid", - "data": null, - "valid": true - }, - { - "description": "number is invalid", - "data": 123, - "valid": false - }, - { - "description": "string is invalid", - "data": "foo", - "valid": false - } - ] - } -] diff --git a/jsonschema/testdata/draft2020-12/unevaluatedItems.json b/jsonschema/testdata/draft2020-12/unevaluatedItems.json deleted file mode 100644 index f861cefa..00000000 --- a/jsonschema/testdata/draft2020-12/unevaluatedItems.json +++ /dev/null @@ -1,798 +0,0 @@ -[ - { - "description": "unevaluatedItems true", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "unevaluatedItems": true - }, - "tests": [ - { - "description": "with no unevaluated items", - "data": [], - "valid": true - }, - { - "description": "with unevaluated items", - "data": ["foo"], - "valid": true - } - ] - }, - { - "description": "unevaluatedItems false", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "unevaluatedItems": false - }, - "tests": [ - { - "description": "with no unevaluated items", - "data": [], - "valid": true - }, - { - "description": "with unevaluated items", - "data": ["foo"], - "valid": false - } - ] - }, - { - "description": "unevaluatedItems as schema", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "unevaluatedItems": { "type": "string" } - }, - "tests": [ - { - "description": "with no unevaluated items", - "data": [], - "valid": true - }, - { - "description": "with valid unevaluated items", - "data": ["foo"], - "valid": true - }, - { - "description": "with invalid unevaluated items", - "data": [42], - "valid": false - } - ] - }, - { - "description": "unevaluatedItems with uniform items", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "items": { "type": "string" }, - "unevaluatedItems": false - }, - "tests": [ - { - "description": "unevaluatedItems doesn't apply", - "data": ["foo", "bar"], - "valid": true - } - ] - }, - { - "description": "unevaluatedItems with tuple", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "prefixItems": [ - { "type": "string" } - ], - "unevaluatedItems": false - }, - "tests": [ - { - "description": "with no unevaluated items", - "data": ["foo"], - "valid": true - }, - { - "description": "with unevaluated items", - "data": ["foo", "bar"], - "valid": false - } - ] - }, - { - "description": "unevaluatedItems with items and prefixItems", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "prefixItems": [ - { "type": "string" } - ], - "items": true, - "unevaluatedItems": false - }, - "tests": [ - { - "description": "unevaluatedItems doesn't apply", - "data": ["foo", 42], - "valid": true - } - ] - }, - { - "description": "unevaluatedItems with items", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "items": {"type": "number"}, - "unevaluatedItems": {"type": "string"} - }, - "tests": [ - { - "description": "valid under items", - "comment": "no elements are considered by unevaluatedItems", - "data": [5, 6, 7, 8], - "valid": true - }, - { - "description": "invalid under items", - "data": ["foo", "bar", "baz"], - "valid": false - } - ] - }, - { - "description": "unevaluatedItems with nested tuple", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "prefixItems": [ - { "type": "string" } - ], - "allOf": [ - { - "prefixItems": [ - true, - { "type": "number" } - ] - } - ], - "unevaluatedItems": false - }, - "tests": [ - { - "description": "with no unevaluated items", - "data": ["foo", 42], - "valid": true - }, - { - "description": "with unevaluated items", - "data": ["foo", 42, true], - "valid": false - } - ] - }, - { - "description": "unevaluatedItems with nested items", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "unevaluatedItems": {"type": "boolean"}, - "anyOf": [ - { "items": {"type": "string"} }, - true - ] - }, - "tests": [ - { - "description": "with only (valid) additional items", - "data": [true, false], - "valid": true - }, - { - "description": "with no additional items", - "data": ["yes", "no"], - "valid": true - }, - { - "description": "with invalid additional item", - "data": ["yes", false], - "valid": false - } - ] - }, - { - "description": "unevaluatedItems with nested prefixItems and items", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "allOf": [ - { - "prefixItems": [ - { "type": "string" } - ], - "items": true - } - ], - "unevaluatedItems": false - }, - "tests": [ - { - "description": "with no additional items", - "data": ["foo"], - "valid": true - }, - { - "description": "with additional items", - "data": ["foo", 42, true], - "valid": true - } - ] - }, - { - "description": "unevaluatedItems with nested unevaluatedItems", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "allOf": [ - { - "prefixItems": [ - { "type": "string" } - ] - }, - { "unevaluatedItems": true } - ], - "unevaluatedItems": false - }, - "tests": [ - { - "description": "with no additional items", - "data": ["foo"], - "valid": true - }, - { - "description": "with additional items", - "data": ["foo", 42, true], - "valid": true - } - ] - }, - { - "description": "unevaluatedItems with anyOf", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "prefixItems": [ - { "const": "foo" } - ], - "anyOf": [ - { - "prefixItems": [ - true, - { "const": "bar" } - ] - }, - { - "prefixItems": [ - true, - true, - { "const": "baz" } - ] - } - ], - "unevaluatedItems": false - }, - "tests": [ - { - "description": "when one schema matches and has no unevaluated items", - "data": ["foo", "bar"], - "valid": true - }, - { - "description": "when one schema matches and has unevaluated items", - "data": ["foo", "bar", 42], - "valid": false - }, - { - "description": "when two schemas match and has no unevaluated items", - "data": ["foo", "bar", "baz"], - "valid": true - }, - { - "description": "when two schemas match and has unevaluated items", - "data": ["foo", "bar", "baz", 42], - "valid": false - } - ] - }, - { - "description": "unevaluatedItems with oneOf", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "prefixItems": [ - { "const": "foo" } - ], - "oneOf": [ - { - "prefixItems": [ - true, - { "const": "bar" } - ] - }, - { - "prefixItems": [ - true, - { "const": "baz" } - ] - } - ], - "unevaluatedItems": false - }, - "tests": [ - { - "description": "with no unevaluated items", - "data": ["foo", "bar"], - "valid": true - }, - { - "description": "with unevaluated items", - "data": ["foo", "bar", 42], - "valid": false - } - ] - }, - { - "description": "unevaluatedItems with not", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "prefixItems": [ - { "const": "foo" } - ], - "not": { - "not": { - "prefixItems": [ - true, - { "const": "bar" } - ] - } - }, - "unevaluatedItems": false - }, - "tests": [ - { - "description": "with unevaluated items", - "data": ["foo", "bar"], - "valid": false - } - ] - }, - { - "description": "unevaluatedItems with if/then/else", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "prefixItems": [ - { "const": "foo" } - ], - "if": { - "prefixItems": [ - true, - { "const": "bar" } - ] - }, - "then": { - "prefixItems": [ - true, - true, - { "const": "then" } - ] - }, - "else": { - "prefixItems": [ - true, - true, - true, - { "const": "else" } - ] - }, - "unevaluatedItems": false - }, - "tests": [ - { - "description": "when if matches and it has no unevaluated items", - "data": ["foo", "bar", "then"], - "valid": true - }, - { - "description": "when if matches and it has unevaluated items", - "data": ["foo", "bar", "then", "else"], - "valid": false - }, - { - "description": "when if doesn't match and it has no unevaluated items", - "data": ["foo", 42, 42, "else"], - "valid": true - }, - { - "description": "when if doesn't match and it has unevaluated items", - "data": ["foo", 42, 42, "else", 42], - "valid": false - } - ] - }, - { - "description": "unevaluatedItems with boolean schemas", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "allOf": [true], - "unevaluatedItems": false - }, - "tests": [ - { - "description": "with no unevaluated items", - "data": [], - "valid": true - }, - { - "description": "with unevaluated items", - "data": ["foo"], - "valid": false - } - ] - }, - { - "description": "unevaluatedItems with $ref", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$ref": "#/$defs/bar", - "prefixItems": [ - { "type": "string" } - ], - "unevaluatedItems": false, - "$defs": { - "bar": { - "prefixItems": [ - true, - { "type": "string" } - ] - } - } - }, - "tests": [ - { - "description": "with no unevaluated items", - "data": ["foo", "bar"], - "valid": true - }, - { - "description": "with unevaluated items", - "data": ["foo", "bar", "baz"], - "valid": false - } - ] - }, - { - "description": "unevaluatedItems before $ref", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "unevaluatedItems": false, - "prefixItems": [ - { "type": "string" } - ], - "$ref": "#/$defs/bar", - "$defs": { - "bar": { - "prefixItems": [ - true, - { "type": "string" } - ] - } - } - }, - "tests": [ - { - "description": "with no unevaluated items", - "data": ["foo", "bar"], - "valid": true - }, - { - "description": "with unevaluated items", - "data": ["foo", "bar", "baz"], - "valid": false - } - ] - }, - { - "description": "unevaluatedItems with $dynamicRef", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://example.com/unevaluated-items-with-dynamic-ref/derived", - - "$ref": "./baseSchema", - - "$defs": { - "derived": { - "$dynamicAnchor": "addons", - "prefixItems": [ - true, - { "type": "string" } - ] - }, - "baseSchema": { - "$id": "./baseSchema", - - "$comment": "unevaluatedItems comes first so it's more likely to catch bugs with implementations that are sensitive to keyword ordering", - "unevaluatedItems": false, - "type": "array", - "prefixItems": [ - { "type": "string" } - ], - "$dynamicRef": "#addons", - - "$defs": { - "defaultAddons": { - "$comment": "Needed to satisfy the bookending requirement", - "$dynamicAnchor": "addons" - } - } - } - } - }, - "tests": [ - { - "description": "with no unevaluated items", - "data": ["foo", "bar"], - "valid": true - }, - { - "description": "with unevaluated items", - "data": ["foo", "bar", "baz"], - "valid": false - } - ] - }, - { - "description": "unevaluatedItems can't see inside cousins", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "allOf": [ - { - "prefixItems": [ true ] - }, - { "unevaluatedItems": false } - ] - }, - "tests": [ - { - "description": "always fails", - "data": [ 1 ], - "valid": false - } - ] - }, - { - "description": "item is evaluated in an uncle schema to unevaluatedItems", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "properties": { - "foo": { - "prefixItems": [ - { "type": "string" } - ], - "unevaluatedItems": false - } - }, - "anyOf": [ - { - "properties": { - "foo": { - "prefixItems": [ - true, - { "type": "string" } - ] - } - } - } - ] - }, - "tests": [ - { - "description": "no extra items", - "data": { - "foo": [ - "test" - ] - }, - "valid": true - }, - { - "description": "uncle keyword evaluation is not significant", - "data": { - "foo": [ - "test", - "test" - ] - }, - "valid": false - } - ] - }, - { - "description": "unevaluatedItems depends on adjacent contains", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "prefixItems": [true], - "contains": {"type": "string"}, - "unevaluatedItems": false - }, - "tests": [ - { - "description": "second item is evaluated by contains", - "data": [ 1, "foo" ], - "valid": true - }, - { - "description": "contains fails, second item is not evaluated", - "data": [ 1, 2 ], - "valid": false - }, - { - "description": "contains passes, second item is not evaluated", - "data": [ 1, 2, "foo" ], - "valid": false - } - ] - }, - { - "description": "unevaluatedItems depends on multiple nested contains", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "allOf": [ - { "contains": { "multipleOf": 2 } }, - { "contains": { "multipleOf": 3 } } - ], - "unevaluatedItems": { "multipleOf": 5 } - }, - "tests": [ - { - "description": "5 not evaluated, passes unevaluatedItems", - "data": [ 2, 3, 4, 5, 6 ], - "valid": true - }, - { - "description": "7 not evaluated, fails unevaluatedItems", - "data": [ 2, 3, 4, 7, 8 ], - "valid": false - } - ] - }, - { - "description": "unevaluatedItems and contains interact to control item dependency relationship", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "if": { - "contains": {"const": "a"} - }, - "then": { - "if": { - "contains": {"const": "b"} - }, - "then": { - "if": { - "contains": {"const": "c"} - } - } - }, - "unevaluatedItems": false - }, - "tests": [ - { - "description": "empty array is valid", - "data": [], - "valid": true - }, - { - "description": "only a's are valid", - "data": [ "a", "a" ], - "valid": true - }, - { - "description": "a's and b's are valid", - "data": [ "a", "b", "a", "b", "a" ], - "valid": true - }, - { - "description": "a's, b's and c's are valid", - "data": [ "c", "a", "c", "c", "b", "a" ], - "valid": true - }, - { - "description": "only b's are invalid", - "data": [ "b", "b" ], - "valid": false - }, - { - "description": "only c's are invalid", - "data": [ "c", "c" ], - "valid": false - }, - { - "description": "only b's and c's are invalid", - "data": [ "c", "b", "c", "b", "c" ], - "valid": false - }, - { - "description": "only a's and c's are invalid", - "data": [ "c", "a", "c", "a", "c" ], - "valid": false - } - ] - }, - { - "description": "non-array instances are valid", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "unevaluatedItems": false - }, - "tests": [ - { - "description": "ignores booleans", - "data": true, - "valid": true - }, - { - "description": "ignores integers", - "data": 123, - "valid": true - }, - { - "description": "ignores floats", - "data": 1.0, - "valid": true - }, - { - "description": "ignores objects", - "data": {}, - "valid": true - }, - { - "description": "ignores strings", - "data": "foo", - "valid": true - }, - { - "description": "ignores null", - "data": null, - "valid": true - } - ] - }, - { - "description": "unevaluatedItems with null instance elements", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "unevaluatedItems": { - "type": "null" - } - }, - "tests": [ - { - "description": "allows null elements", - "data": [ null ], - "valid": true - } - ] - }, - { - "description": "unevaluatedItems can see annotations from if without then and else", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "if": { - "prefixItems": [{"const": "a"}] - }, - "unevaluatedItems": false - }, - "tests": [ - { - "description": "valid in case if is evaluated", - "data": [ "a" ], - "valid": true - }, - { - "description": "invalid in case if is evaluated", - "data": [ "b" ], - "valid": false - } - ] - } -] diff --git a/jsonschema/testdata/draft2020-12/unevaluatedProperties.json b/jsonschema/testdata/draft2020-12/unevaluatedProperties.json deleted file mode 100644 index ae29c9eb..00000000 --- a/jsonschema/testdata/draft2020-12/unevaluatedProperties.json +++ /dev/null @@ -1,1601 +0,0 @@ -[ - { - "description": "unevaluatedProperties true", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "object", - "unevaluatedProperties": true - }, - "tests": [ - { - "description": "with no unevaluated properties", - "data": {}, - "valid": true - }, - { - "description": "with unevaluated properties", - "data": { - "foo": "foo" - }, - "valid": true - } - ] - }, - { - "description": "unevaluatedProperties schema", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "object", - "unevaluatedProperties": { - "type": "string", - "minLength": 3 - } - }, - "tests": [ - { - "description": "with no unevaluated properties", - "data": {}, - "valid": true - }, - { - "description": "with valid unevaluated properties", - "data": { - "foo": "foo" - }, - "valid": true - }, - { - "description": "with invalid unevaluated properties", - "data": { - "foo": "fo" - }, - "valid": false - } - ] - }, - { - "description": "unevaluatedProperties false", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "object", - "unevaluatedProperties": false - }, - "tests": [ - { - "description": "with no unevaluated properties", - "data": {}, - "valid": true - }, - { - "description": "with unevaluated properties", - "data": { - "foo": "foo" - }, - "valid": false - } - ] - }, - { - "description": "unevaluatedProperties with adjacent properties", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "object", - "properties": { - "foo": { "type": "string" } - }, - "unevaluatedProperties": false - }, - "tests": [ - { - "description": "with no unevaluated properties", - "data": { - "foo": "foo" - }, - "valid": true - }, - { - "description": "with unevaluated properties", - "data": { - "foo": "foo", - "bar": "bar" - }, - "valid": false - } - ] - }, - { - "description": "unevaluatedProperties with adjacent patternProperties", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "object", - "patternProperties": { - "^foo": { "type": "string" } - }, - "unevaluatedProperties": false - }, - "tests": [ - { - "description": "with no unevaluated properties", - "data": { - "foo": "foo" - }, - "valid": true - }, - { - "description": "with unevaluated properties", - "data": { - "foo": "foo", - "bar": "bar" - }, - "valid": false - } - ] - }, - { - "description": "unevaluatedProperties with adjacent additionalProperties", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "object", - "properties": { - "foo": { "type": "string" } - }, - "additionalProperties": true, - "unevaluatedProperties": false - }, - "tests": [ - { - "description": "with no additional properties", - "data": { - "foo": "foo" - }, - "valid": true - }, - { - "description": "with additional properties", - "data": { - "foo": "foo", - "bar": "bar" - }, - "valid": true - } - ] - }, - { - "description": "unevaluatedProperties with nested properties", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "object", - "properties": { - "foo": { "type": "string" } - }, - "allOf": [ - { - "properties": { - "bar": { "type": "string" } - } - } - ], - "unevaluatedProperties": false - }, - "tests": [ - { - "description": "with no additional properties", - "data": { - "foo": "foo", - "bar": "bar" - }, - "valid": true - }, - { - "description": "with additional properties", - "data": { - "foo": "foo", - "bar": "bar", - "baz": "baz" - }, - "valid": false - } - ] - }, - { - "description": "unevaluatedProperties with nested patternProperties", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "object", - "properties": { - "foo": { "type": "string" } - }, - "allOf": [ - { - "patternProperties": { - "^bar": { "type": "string" } - } - } - ], - "unevaluatedProperties": false - }, - "tests": [ - { - "description": "with no additional properties", - "data": { - "foo": "foo", - "bar": "bar" - }, - "valid": true - }, - { - "description": "with additional properties", - "data": { - "foo": "foo", - "bar": "bar", - "baz": "baz" - }, - "valid": false - } - ] - }, - { - "description": "unevaluatedProperties with nested additionalProperties", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "object", - "properties": { - "foo": { "type": "string" } - }, - "allOf": [ - { - "additionalProperties": true - } - ], - "unevaluatedProperties": false - }, - "tests": [ - { - "description": "with no additional properties", - "data": { - "foo": "foo" - }, - "valid": true - }, - { - "description": "with additional properties", - "data": { - "foo": "foo", - "bar": "bar" - }, - "valid": true - } - ] - }, - { - "description": "unevaluatedProperties with nested unevaluatedProperties", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "object", - "properties": { - "foo": { "type": "string" } - }, - "allOf": [ - { - "unevaluatedProperties": true - } - ], - "unevaluatedProperties": { - "type": "string", - "maxLength": 2 - } - }, - "tests": [ - { - "description": "with no nested unevaluated properties", - "data": { - "foo": "foo" - }, - "valid": true - }, - { - "description": "with nested unevaluated properties", - "data": { - "foo": "foo", - "bar": "bar" - }, - "valid": true - } - ] - }, - { - "description": "unevaluatedProperties with anyOf", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "object", - "properties": { - "foo": { "type": "string" } - }, - "anyOf": [ - { - "properties": { - "bar": { "const": "bar" } - }, - "required": ["bar"] - }, - { - "properties": { - "baz": { "const": "baz" } - }, - "required": ["baz"] - }, - { - "properties": { - "quux": { "const": "quux" } - }, - "required": ["quux"] - } - ], - "unevaluatedProperties": false - }, - "tests": [ - { - "description": "when one matches and has no unevaluated properties", - "data": { - "foo": "foo", - "bar": "bar" - }, - "valid": true - }, - { - "description": "when one matches and has unevaluated properties", - "data": { - "foo": "foo", - "bar": "bar", - "baz": "not-baz" - }, - "valid": false - }, - { - "description": "when two match and has no unevaluated properties", - "data": { - "foo": "foo", - "bar": "bar", - "baz": "baz" - }, - "valid": true - }, - { - "description": "when two match and has unevaluated properties", - "data": { - "foo": "foo", - "bar": "bar", - "baz": "baz", - "quux": "not-quux" - }, - "valid": false - } - ] - }, - { - "description": "unevaluatedProperties with oneOf", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "object", - "properties": { - "foo": { "type": "string" } - }, - "oneOf": [ - { - "properties": { - "bar": { "const": "bar" } - }, - "required": ["bar"] - }, - { - "properties": { - "baz": { "const": "baz" } - }, - "required": ["baz"] - } - ], - "unevaluatedProperties": false - }, - "tests": [ - { - "description": "with no unevaluated properties", - "data": { - "foo": "foo", - "bar": "bar" - }, - "valid": true - }, - { - "description": "with unevaluated properties", - "data": { - "foo": "foo", - "bar": "bar", - "quux": "quux" - }, - "valid": false - } - ] - }, - { - "description": "unevaluatedProperties with not", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "object", - "properties": { - "foo": { "type": "string" } - }, - "not": { - "not": { - "properties": { - "bar": { "const": "bar" } - }, - "required": ["bar"] - } - }, - "unevaluatedProperties": false - }, - "tests": [ - { - "description": "with unevaluated properties", - "data": { - "foo": "foo", - "bar": "bar" - }, - "valid": false - } - ] - }, - { - "description": "unevaluatedProperties with if/then/else", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "object", - "if": { - "properties": { - "foo": { "const": "then" } - }, - "required": ["foo"] - }, - "then": { - "properties": { - "bar": { "type": "string" } - }, - "required": ["bar"] - }, - "else": { - "properties": { - "baz": { "type": "string" } - }, - "required": ["baz"] - }, - "unevaluatedProperties": false - }, - "tests": [ - { - "description": "when if is true and has no unevaluated properties", - "data": { - "foo": "then", - "bar": "bar" - }, - "valid": true - }, - { - "description": "when if is true and has unevaluated properties", - "data": { - "foo": "then", - "bar": "bar", - "baz": "baz" - }, - "valid": false - }, - { - "description": "when if is false and has no unevaluated properties", - "data": { - "baz": "baz" - }, - "valid": true - }, - { - "description": "when if is false and has unevaluated properties", - "data": { - "foo": "else", - "baz": "baz" - }, - "valid": false - } - ] - }, - { - "description": "unevaluatedProperties with if/then/else, then not defined", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "object", - "if": { - "properties": { - "foo": { "const": "then" } - }, - "required": ["foo"] - }, - "else": { - "properties": { - "baz": { "type": "string" } - }, - "required": ["baz"] - }, - "unevaluatedProperties": false - }, - "tests": [ - { - "description": "when if is true and has no unevaluated properties", - "data": { - "foo": "then", - "bar": "bar" - }, - "valid": false - }, - { - "description": "when if is true and has unevaluated properties", - "data": { - "foo": "then", - "bar": "bar", - "baz": "baz" - }, - "valid": false - }, - { - "description": "when if is false and has no unevaluated properties", - "data": { - "baz": "baz" - }, - "valid": true - }, - { - "description": "when if is false and has unevaluated properties", - "data": { - "foo": "else", - "baz": "baz" - }, - "valid": false - } - ] - }, - { - "description": "unevaluatedProperties with if/then/else, else not defined", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "object", - "if": { - "properties": { - "foo": { "const": "then" } - }, - "required": ["foo"] - }, - "then": { - "properties": { - "bar": { "type": "string" } - }, - "required": ["bar"] - }, - "unevaluatedProperties": false - }, - "tests": [ - { - "description": "when if is true and has no unevaluated properties", - "data": { - "foo": "then", - "bar": "bar" - }, - "valid": true - }, - { - "description": "when if is true and has unevaluated properties", - "data": { - "foo": "then", - "bar": "bar", - "baz": "baz" - }, - "valid": false - }, - { - "description": "when if is false and has no unevaluated properties", - "data": { - "baz": "baz" - }, - "valid": false - }, - { - "description": "when if is false and has unevaluated properties", - "data": { - "foo": "else", - "baz": "baz" - }, - "valid": false - } - ] - }, - { - "description": "unevaluatedProperties with dependentSchemas", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "object", - "properties": { - "foo": { "type": "string" } - }, - "dependentSchemas": { - "foo": { - "properties": { - "bar": { "const": "bar" } - }, - "required": ["bar"] - } - }, - "unevaluatedProperties": false - }, - "tests": [ - { - "description": "with no unevaluated properties", - "data": { - "foo": "foo", - "bar": "bar" - }, - "valid": true - }, - { - "description": "with unevaluated properties", - "data": { - "bar": "bar" - }, - "valid": false - } - ] - }, - { - "description": "unevaluatedProperties with boolean schemas", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "object", - "properties": { - "foo": { "type": "string" } - }, - "allOf": [true], - "unevaluatedProperties": false - }, - "tests": [ - { - "description": "with no unevaluated properties", - "data": { - "foo": "foo" - }, - "valid": true - }, - { - "description": "with unevaluated properties", - "data": { - "bar": "bar" - }, - "valid": false - } - ] - }, - { - "description": "unevaluatedProperties with $ref", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "object", - "$ref": "#/$defs/bar", - "properties": { - "foo": { "type": "string" } - }, - "unevaluatedProperties": false, - "$defs": { - "bar": { - "properties": { - "bar": { "type": "string" } - } - } - } - }, - "tests": [ - { - "description": "with no unevaluated properties", - "data": { - "foo": "foo", - "bar": "bar" - }, - "valid": true - }, - { - "description": "with unevaluated properties", - "data": { - "foo": "foo", - "bar": "bar", - "baz": "baz" - }, - "valid": false - } - ] - }, - { - "description": "unevaluatedProperties before $ref", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "object", - "unevaluatedProperties": false, - "properties": { - "foo": { "type": "string" } - }, - "$ref": "#/$defs/bar", - "$defs": { - "bar": { - "properties": { - "bar": { "type": "string" } - } - } - } - }, - "tests": [ - { - "description": "with no unevaluated properties", - "data": { - "foo": "foo", - "bar": "bar" - }, - "valid": true - }, - { - "description": "with unevaluated properties", - "data": { - "foo": "foo", - "bar": "bar", - "baz": "baz" - }, - "valid": false - } - ] - }, - { - "description": "unevaluatedProperties with $dynamicRef", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://example.com/unevaluated-properties-with-dynamic-ref/derived", - - "$ref": "./baseSchema", - - "$defs": { - "derived": { - "$dynamicAnchor": "addons", - "properties": { - "bar": { "type": "string" } - } - }, - "baseSchema": { - "$id": "./baseSchema", - - "$comment": "unevaluatedProperties comes first so it's more likely to catch bugs with implementations that are sensitive to keyword ordering", - "unevaluatedProperties": false, - "type": "object", - "properties": { - "foo": { "type": "string" } - }, - "$dynamicRef": "#addons", - - "$defs": { - "defaultAddons": { - "$comment": "Needed to satisfy the bookending requirement", - "$dynamicAnchor": "addons" - } - } - } - } - }, - "tests": [ - { - "description": "with no unevaluated properties", - "data": { - "foo": "foo", - "bar": "bar" - }, - "valid": true - }, - { - "description": "with unevaluated properties", - "data": { - "foo": "foo", - "bar": "bar", - "baz": "baz" - }, - "valid": false - } - ] - }, - { - "description": "unevaluatedProperties can't see inside cousins", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "allOf": [ - { - "properties": { - "foo": true - } - }, - { - "unevaluatedProperties": false - } - ] - }, - "tests": [ - { - "description": "always fails", - "data": { - "foo": 1 - }, - "valid": false - } - ] - }, - { - "description": "unevaluatedProperties can't see inside cousins (reverse order)", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "allOf": [ - { - "unevaluatedProperties": false - }, - { - "properties": { - "foo": true - } - } - ] - }, - "tests": [ - { - "description": "always fails", - "data": { - "foo": 1 - }, - "valid": false - } - ] - }, - { - "description": "nested unevaluatedProperties, outer false, inner true, properties outside", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "object", - "properties": { - "foo": { "type": "string" } - }, - "allOf": [ - { - "unevaluatedProperties": true - } - ], - "unevaluatedProperties": false - }, - "tests": [ - { - "description": "with no nested unevaluated properties", - "data": { - "foo": "foo" - }, - "valid": true - }, - { - "description": "with nested unevaluated properties", - "data": { - "foo": "foo", - "bar": "bar" - }, - "valid": true - } - ] - }, - { - "description": "nested unevaluatedProperties, outer false, inner true, properties inside", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "object", - "allOf": [ - { - "properties": { - "foo": { "type": "string" } - }, - "unevaluatedProperties": true - } - ], - "unevaluatedProperties": false - }, - "tests": [ - { - "description": "with no nested unevaluated properties", - "data": { - "foo": "foo" - }, - "valid": true - }, - { - "description": "with nested unevaluated properties", - "data": { - "foo": "foo", - "bar": "bar" - }, - "valid": true - } - ] - }, - { - "description": "nested unevaluatedProperties, outer true, inner false, properties outside", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "object", - "properties": { - "foo": { "type": "string" } - }, - "allOf": [ - { - "unevaluatedProperties": false - } - ], - "unevaluatedProperties": true - }, - "tests": [ - { - "description": "with no nested unevaluated properties", - "data": { - "foo": "foo" - }, - "valid": false - }, - { - "description": "with nested unevaluated properties", - "data": { - "foo": "foo", - "bar": "bar" - }, - "valid": false - } - ] - }, - { - "description": "nested unevaluatedProperties, outer true, inner false, properties inside", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "object", - "allOf": [ - { - "properties": { - "foo": { "type": "string" } - }, - "unevaluatedProperties": false - } - ], - "unevaluatedProperties": true - }, - "tests": [ - { - "description": "with no nested unevaluated properties", - "data": { - "foo": "foo" - }, - "valid": true - }, - { - "description": "with nested unevaluated properties", - "data": { - "foo": "foo", - "bar": "bar" - }, - "valid": false - } - ] - }, - { - "description": "cousin unevaluatedProperties, true and false, true with properties", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "object", - "allOf": [ - { - "properties": { - "foo": { "type": "string" } - }, - "unevaluatedProperties": true - }, - { - "unevaluatedProperties": false - } - ] - }, - "tests": [ - { - "description": "with no nested unevaluated properties", - "data": { - "foo": "foo" - }, - "valid": false - }, - { - "description": "with nested unevaluated properties", - "data": { - "foo": "foo", - "bar": "bar" - }, - "valid": false - } - ] - }, - { - "description": "cousin unevaluatedProperties, true and false, false with properties", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "object", - "allOf": [ - { - "unevaluatedProperties": true - }, - { - "properties": { - "foo": { "type": "string" } - }, - "unevaluatedProperties": false - } - ] - }, - "tests": [ - { - "description": "with no nested unevaluated properties", - "data": { - "foo": "foo" - }, - "valid": true - }, - { - "description": "with nested unevaluated properties", - "data": { - "foo": "foo", - "bar": "bar" - }, - "valid": false - } - ] - }, - { - "description": "property is evaluated in an uncle schema to unevaluatedProperties", - "comment": "see https://stackoverflow.com/questions/66936884/deeply-nested-unevaluatedproperties-and-their-expectations", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "object", - "properties": { - "foo": { - "type": "object", - "properties": { - "bar": { - "type": "string" - } - }, - "unevaluatedProperties": false - } - }, - "anyOf": [ - { - "properties": { - "foo": { - "properties": { - "faz": { - "type": "string" - } - } - } - } - } - ] - }, - "tests": [ - { - "description": "no extra properties", - "data": { - "foo": { - "bar": "test" - } - }, - "valid": true - }, - { - "description": "uncle keyword evaluation is not significant", - "data": { - "foo": { - "bar": "test", - "faz": "test" - } - }, - "valid": false - } - ] - }, - { - "description": "in-place applicator siblings, allOf has unevaluated", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "object", - "allOf": [ - { - "properties": { - "foo": true - }, - "unevaluatedProperties": false - } - ], - "anyOf": [ - { - "properties": { - "bar": true - } - } - ] - }, - "tests": [ - { - "description": "base case: both properties present", - "data": { - "foo": 1, - "bar": 1 - }, - "valid": false - }, - { - "description": "in place applicator siblings, bar is missing", - "data": { - "foo": 1 - }, - "valid": true - }, - { - "description": "in place applicator siblings, foo is missing", - "data": { - "bar": 1 - }, - "valid": false - } - ] - }, - { - "description": "in-place applicator siblings, anyOf has unevaluated", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "object", - "allOf": [ - { - "properties": { - "foo": true - } - } - ], - "anyOf": [ - { - "properties": { - "bar": true - }, - "unevaluatedProperties": false - } - ] - }, - "tests": [ - { - "description": "base case: both properties present", - "data": { - "foo": 1, - "bar": 1 - }, - "valid": false - }, - { - "description": "in place applicator siblings, bar is missing", - "data": { - "foo": 1 - }, - "valid": false - }, - { - "description": "in place applicator siblings, foo is missing", - "data": { - "bar": 1 - }, - "valid": true - } - ] - }, - { - "description": "unevaluatedProperties + single cyclic ref", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "object", - "properties": { - "x": { "$ref": "#" } - }, - "unevaluatedProperties": false - }, - "tests": [ - { - "description": "Empty is valid", - "data": {}, - "valid": true - }, - { - "description": "Single is valid", - "data": { "x": {} }, - "valid": true - }, - { - "description": "Unevaluated on 1st level is invalid", - "data": { "x": {}, "y": {} }, - "valid": false - }, - { - "description": "Nested is valid", - "data": { "x": { "x": {} } }, - "valid": true - }, - { - "description": "Unevaluated on 2nd level is invalid", - "data": { "x": { "x": {}, "y": {} } }, - "valid": false - }, - { - "description": "Deep nested is valid", - "data": { "x": { "x": { "x": {} } } }, - "valid": true - }, - { - "description": "Unevaluated on 3rd level is invalid", - "data": { "x": { "x": { "x": {}, "y": {} } } }, - "valid": false - } - ] - }, - { - "description": "unevaluatedProperties + ref inside allOf / oneOf", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$defs": { - "one": { - "properties": { "a": true } - }, - "two": { - "required": ["x"], - "properties": { "x": true } - } - }, - "allOf": [ - { "$ref": "#/$defs/one" }, - { "properties": { "b": true } }, - { - "oneOf": [ - { "$ref": "#/$defs/two" }, - { - "required": ["y"], - "properties": { "y": true } - } - ] - } - ], - "unevaluatedProperties": false - }, - "tests": [ - { - "description": "Empty is invalid (no x or y)", - "data": {}, - "valid": false - }, - { - "description": "a and b are invalid (no x or y)", - "data": { "a": 1, "b": 1 }, - "valid": false - }, - { - "description": "x and y are invalid", - "data": { "x": 1, "y": 1 }, - "valid": false - }, - { - "description": "a and x are valid", - "data": { "a": 1, "x": 1 }, - "valid": true - }, - { - "description": "a and y are valid", - "data": { "a": 1, "y": 1 }, - "valid": true - }, - { - "description": "a and b and x are valid", - "data": { "a": 1, "b": 1, "x": 1 }, - "valid": true - }, - { - "description": "a and b and y are valid", - "data": { "a": 1, "b": 1, "y": 1 }, - "valid": true - }, - { - "description": "a and b and x and y are invalid", - "data": { "a": 1, "b": 1, "x": 1, "y": 1 }, - "valid": false - } - ] - }, - { - "description": "dynamic evalation inside nested refs", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$defs": { - "one": { - "oneOf": [ - { "$ref": "#/$defs/two" }, - { "required": ["b"], "properties": { "b": true } }, - { "required": ["xx"], "patternProperties": { "x": true } }, - { "required": ["all"], "unevaluatedProperties": true } - ] - }, - "two": { - "oneOf": [ - { "required": ["c"], "properties": { "c": true } }, - { "required": ["d"], "properties": { "d": true } } - ] - } - }, - "oneOf": [ - { "$ref": "#/$defs/one" }, - { "required": ["a"], "properties": { "a": true } } - ], - "unevaluatedProperties": false - }, - "tests": [ - { - "description": "Empty is invalid", - "data": {}, - "valid": false - }, - { - "description": "a is valid", - "data": { "a": 1 }, - "valid": true - }, - { - "description": "b is valid", - "data": { "b": 1 }, - "valid": true - }, - { - "description": "c is valid", - "data": { "c": 1 }, - "valid": true - }, - { - "description": "d is valid", - "data": { "d": 1 }, - "valid": true - }, - { - "description": "a + b is invalid", - "data": { "a": 1, "b": 1 }, - "valid": false - }, - { - "description": "a + c is invalid", - "data": { "a": 1, "c": 1 }, - "valid": false - }, - { - "description": "a + d is invalid", - "data": { "a": 1, "d": 1 }, - "valid": false - }, - { - "description": "b + c is invalid", - "data": { "b": 1, "c": 1 }, - "valid": false - }, - { - "description": "b + d is invalid", - "data": { "b": 1, "d": 1 }, - "valid": false - }, - { - "description": "c + d is invalid", - "data": { "c": 1, "d": 1 }, - "valid": false - }, - { - "description": "xx is valid", - "data": { "xx": 1 }, - "valid": true - }, - { - "description": "xx + foox is valid", - "data": { "xx": 1, "foox": 1 }, - "valid": true - }, - { - "description": "xx + foo is invalid", - "data": { "xx": 1, "foo": 1 }, - "valid": false - }, - { - "description": "xx + a is invalid", - "data": { "xx": 1, "a": 1 }, - "valid": false - }, - { - "description": "xx + b is invalid", - "data": { "xx": 1, "b": 1 }, - "valid": false - }, - { - "description": "xx + c is invalid", - "data": { "xx": 1, "c": 1 }, - "valid": false - }, - { - "description": "xx + d is invalid", - "data": { "xx": 1, "d": 1 }, - "valid": false - }, - { - "description": "all is valid", - "data": { "all": 1 }, - "valid": true - }, - { - "description": "all + foo is valid", - "data": { "all": 1, "foo": 1 }, - "valid": true - }, - { - "description": "all + a is invalid", - "data": { "all": 1, "a": 1 }, - "valid": false - } - ] - }, - { - "description": "non-object instances are valid", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "unevaluatedProperties": false - }, - "tests": [ - { - "description": "ignores booleans", - "data": true, - "valid": true - }, - { - "description": "ignores integers", - "data": 123, - "valid": true - }, - { - "description": "ignores floats", - "data": 1.0, - "valid": true - }, - { - "description": "ignores arrays", - "data": [], - "valid": true - }, - { - "description": "ignores strings", - "data": "foo", - "valid": true - }, - { - "description": "ignores null", - "data": null, - "valid": true - } - ] - }, - { - "description": "unevaluatedProperties with null valued instance properties", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "unevaluatedProperties": { - "type": "null" - } - }, - "tests": [ - { - "description": "allows null valued properties", - "data": {"foo": null}, - "valid": true - } - ] - }, - { - "description": "unevaluatedProperties not affected by propertyNames", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "propertyNames": {"maxLength": 1}, - "unevaluatedProperties": { - "type": "number" - } - }, - "tests": [ - { - "description": "allows only number properties", - "data": {"a": 1}, - "valid": true - }, - { - "description": "string property is invalid", - "data": {"a": "b"}, - "valid": false - } - ] - }, - { - "description": "unevaluatedProperties can see annotations from if without then and else", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "if": { - "patternProperties": { - "foo": { - "type": "string" - } - } - }, - "unevaluatedProperties": false - }, - "tests": [ - { - "description": "valid in case if is evaluated", - "data": { - "foo": "a" - }, - "valid": true - }, - { - "description": "invalid in case if is evaluated", - "data": { - "bar": "a" - }, - "valid": false - } - ] - }, - { - "description": "dependentSchemas with unevaluatedProperties", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "properties": {"foo2": {}}, - "dependentSchemas": { - "foo" : {}, - "foo2": { - "properties": { - "bar":{} - } - } - }, - "unevaluatedProperties": false - }, - "tests": [ - { - "description": "unevaluatedProperties doesn't consider dependentSchemas", - "data": {"foo": ""}, - "valid": false - }, - { - "description": "unevaluatedProperties doesn't see bar when foo2 is absent", - "data": {"bar": ""}, - "valid": false - }, - { - "description": "unevaluatedProperties sees bar when foo2 is present", - "data": { "foo2": "", "bar": ""}, - "valid": true - } - ] - } -] diff --git a/jsonschema/testdata/draft2020-12/uniqueItems.json b/jsonschema/testdata/draft2020-12/uniqueItems.json deleted file mode 100644 index 4ea3bf98..00000000 --- a/jsonschema/testdata/draft2020-12/uniqueItems.json +++ /dev/null @@ -1,419 +0,0 @@ -[ - { - "description": "uniqueItems validation", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "uniqueItems": true - }, - "tests": [ - { - "description": "unique array of integers is valid", - "data": [1, 2], - "valid": true - }, - { - "description": "non-unique array of integers is invalid", - "data": [1, 1], - "valid": false - }, - { - "description": "non-unique array of more than two integers is invalid", - "data": [1, 2, 1], - "valid": false - }, - { - "description": "numbers are unique if mathematically unequal", - "data": [1.0, 1.00, 1], - "valid": false - }, - { - "description": "false is not equal to zero", - "data": [0, false], - "valid": true - }, - { - "description": "true is not equal to one", - "data": [1, true], - "valid": true - }, - { - "description": "unique array of strings is valid", - "data": ["foo", "bar", "baz"], - "valid": true - }, - { - "description": "non-unique array of strings is invalid", - "data": ["foo", "bar", "foo"], - "valid": false - }, - { - "description": "unique array of objects is valid", - "data": [{"foo": "bar"}, {"foo": "baz"}], - "valid": true - }, - { - "description": "non-unique array of objects is invalid", - "data": [{"foo": "bar"}, {"foo": "bar"}], - "valid": false - }, - { - "description": "property order of array of objects is ignored", - "data": [{"foo": "bar", "bar": "foo"}, {"bar": "foo", "foo": "bar"}], - "valid": false - }, - { - "description": "unique array of nested objects is valid", - "data": [ - {"foo": {"bar" : {"baz" : true}}}, - {"foo": {"bar" : {"baz" : false}}} - ], - "valid": true - }, - { - "description": "non-unique array of nested objects is invalid", - "data": [ - {"foo": {"bar" : {"baz" : true}}}, - {"foo": {"bar" : {"baz" : true}}} - ], - "valid": false - }, - { - "description": "unique array of arrays is valid", - "data": [["foo"], ["bar"]], - "valid": true - }, - { - "description": "non-unique array of arrays is invalid", - "data": [["foo"], ["foo"]], - "valid": false - }, - { - "description": "non-unique array of more than two arrays is invalid", - "data": [["foo"], ["bar"], ["foo"]], - "valid": false - }, - { - "description": "1 and true are unique", - "data": [1, true], - "valid": true - }, - { - "description": "0 and false are unique", - "data": [0, false], - "valid": true - }, - { - "description": "[1] and [true] are unique", - "data": [[1], [true]], - "valid": true - }, - { - "description": "[0] and [false] are unique", - "data": [[0], [false]], - "valid": true - }, - { - "description": "nested [1] and [true] are unique", - "data": [[[1], "foo"], [[true], "foo"]], - "valid": true - }, - { - "description": "nested [0] and [false] are unique", - "data": [[[0], "foo"], [[false], "foo"]], - "valid": true - }, - { - "description": "unique heterogeneous types are valid", - "data": [{}, [1], true, null, 1, "{}"], - "valid": true - }, - { - "description": "non-unique heterogeneous types are invalid", - "data": [{}, [1], true, null, {}, 1], - "valid": false - }, - { - "description": "different objects are unique", - "data": [{"a": 1, "b": 2}, {"a": 2, "b": 1}], - "valid": true - }, - { - "description": "objects are non-unique despite key order", - "data": [{"a": 1, "b": 2}, {"b": 2, "a": 1}], - "valid": false - }, - { - "description": "{\"a\": false} and {\"a\": 0} are unique", - "data": [{"a": false}, {"a": 0}], - "valid": true - }, - { - "description": "{\"a\": true} and {\"a\": 1} are unique", - "data": [{"a": true}, {"a": 1}], - "valid": true - } - ] - }, - { - "description": "uniqueItems with an array of items", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "prefixItems": [{"type": "boolean"}, {"type": "boolean"}], - "uniqueItems": true - }, - "tests": [ - { - "description": "[false, true] from items array is valid", - "data": [false, true], - "valid": true - }, - { - "description": "[true, false] from items array is valid", - "data": [true, false], - "valid": true - }, - { - "description": "[false, false] from items array is not valid", - "data": [false, false], - "valid": false - }, - { - "description": "[true, true] from items array is not valid", - "data": [true, true], - "valid": false - }, - { - "description": "unique array extended from [false, true] is valid", - "data": [false, true, "foo", "bar"], - "valid": true - }, - { - "description": "unique array extended from [true, false] is valid", - "data": [true, false, "foo", "bar"], - "valid": true - }, - { - "description": "non-unique array extended from [false, true] is not valid", - "data": [false, true, "foo", "foo"], - "valid": false - }, - { - "description": "non-unique array extended from [true, false] is not valid", - "data": [true, false, "foo", "foo"], - "valid": false - } - ] - }, - { - "description": "uniqueItems with an array of items and additionalItems=false", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "prefixItems": [{"type": "boolean"}, {"type": "boolean"}], - "uniqueItems": true, - "items": false - }, - "tests": [ - { - "description": "[false, true] from items array is valid", - "data": [false, true], - "valid": true - }, - { - "description": "[true, false] from items array is valid", - "data": [true, false], - "valid": true - }, - { - "description": "[false, false] from items array is not valid", - "data": [false, false], - "valid": false - }, - { - "description": "[true, true] from items array is not valid", - "data": [true, true], - "valid": false - }, - { - "description": "extra items are invalid even if unique", - "data": [false, true, null], - "valid": false - } - ] - }, - { - "description": "uniqueItems=false validation", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "uniqueItems": false - }, - "tests": [ - { - "description": "unique array of integers is valid", - "data": [1, 2], - "valid": true - }, - { - "description": "non-unique array of integers is valid", - "data": [1, 1], - "valid": true - }, - { - "description": "numbers are unique if mathematically unequal", - "data": [1.0, 1.00, 1], - "valid": true - }, - { - "description": "false is not equal to zero", - "data": [0, false], - "valid": true - }, - { - "description": "true is not equal to one", - "data": [1, true], - "valid": true - }, - { - "description": "unique array of objects is valid", - "data": [{"foo": "bar"}, {"foo": "baz"}], - "valid": true - }, - { - "description": "non-unique array of objects is valid", - "data": [{"foo": "bar"}, {"foo": "bar"}], - "valid": true - }, - { - "description": "unique array of nested objects is valid", - "data": [ - {"foo": {"bar" : {"baz" : true}}}, - {"foo": {"bar" : {"baz" : false}}} - ], - "valid": true - }, - { - "description": "non-unique array of nested objects is valid", - "data": [ - {"foo": {"bar" : {"baz" : true}}}, - {"foo": {"bar" : {"baz" : true}}} - ], - "valid": true - }, - { - "description": "unique array of arrays is valid", - "data": [["foo"], ["bar"]], - "valid": true - }, - { - "description": "non-unique array of arrays is valid", - "data": [["foo"], ["foo"]], - "valid": true - }, - { - "description": "1 and true are unique", - "data": [1, true], - "valid": true - }, - { - "description": "0 and false are unique", - "data": [0, false], - "valid": true - }, - { - "description": "unique heterogeneous types are valid", - "data": [{}, [1], true, null, 1], - "valid": true - }, - { - "description": "non-unique heterogeneous types are valid", - "data": [{}, [1], true, null, {}, 1], - "valid": true - } - ] - }, - { - "description": "uniqueItems=false with an array of items", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "prefixItems": [{"type": "boolean"}, {"type": "boolean"}], - "uniqueItems": false - }, - "tests": [ - { - "description": "[false, true] from items array is valid", - "data": [false, true], - "valid": true - }, - { - "description": "[true, false] from items array is valid", - "data": [true, false], - "valid": true - }, - { - "description": "[false, false] from items array is valid", - "data": [false, false], - "valid": true - }, - { - "description": "[true, true] from items array is valid", - "data": [true, true], - "valid": true - }, - { - "description": "unique array extended from [false, true] is valid", - "data": [false, true, "foo", "bar"], - "valid": true - }, - { - "description": "unique array extended from [true, false] is valid", - "data": [true, false, "foo", "bar"], - "valid": true - }, - { - "description": "non-unique array extended from [false, true] is valid", - "data": [false, true, "foo", "foo"], - "valid": true - }, - { - "description": "non-unique array extended from [true, false] is valid", - "data": [true, false, "foo", "foo"], - "valid": true - } - ] - }, - { - "description": "uniqueItems=false with an array of items and additionalItems=false", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "prefixItems": [{"type": "boolean"}, {"type": "boolean"}], - "uniqueItems": false, - "items": false - }, - "tests": [ - { - "description": "[false, true] from items array is valid", - "data": [false, true], - "valid": true - }, - { - "description": "[true, false] from items array is valid", - "data": [true, false], - "valid": true - }, - { - "description": "[false, false] from items array is valid", - "data": [false, false], - "valid": true - }, - { - "description": "[true, true] from items array is valid", - "data": [true, true], - "valid": true - }, - { - "description": "extra items are invalid even if unique", - "data": [false, true, null], - "valid": false - } - ] - } -] diff --git a/jsonschema/testdata/remotes/README.md b/jsonschema/testdata/remotes/README.md deleted file mode 100644 index 8a641dbd..00000000 --- a/jsonschema/testdata/remotes/README.md +++ /dev/null @@ -1,4 +0,0 @@ -# JSON Schema test suite: remote references - -These files were copied from -https://github.com/json-schema-org/JSON-Schema-Test-Suite/tree/83e866b46c9f9e7082fd51e83a61c5f2145a1ab7/remotes. diff --git a/jsonschema/testdata/remotes/different-id-ref-string.json b/jsonschema/testdata/remotes/different-id-ref-string.json deleted file mode 100644 index 7f888609..00000000 --- a/jsonschema/testdata/remotes/different-id-ref-string.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "$id": "http://localhost:1234/real-id-ref-string.json", - "$defs": {"bar": {"type": "string"}}, - "$ref": "#/$defs/bar" -} diff --git a/jsonschema/testdata/remotes/draft2020-12/baseUriChange/folderInteger.json b/jsonschema/testdata/remotes/draft2020-12/baseUriChange/folderInteger.json deleted file mode 100644 index 1f44a631..00000000 --- a/jsonschema/testdata/remotes/draft2020-12/baseUriChange/folderInteger.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "integer" -} diff --git a/jsonschema/testdata/remotes/draft2020-12/baseUriChangeFolder/folderInteger.json b/jsonschema/testdata/remotes/draft2020-12/baseUriChangeFolder/folderInteger.json deleted file mode 100644 index 1f44a631..00000000 --- a/jsonschema/testdata/remotes/draft2020-12/baseUriChangeFolder/folderInteger.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "integer" -} diff --git a/jsonschema/testdata/remotes/draft2020-12/baseUriChangeFolderInSubschema/folderInteger.json b/jsonschema/testdata/remotes/draft2020-12/baseUriChangeFolderInSubschema/folderInteger.json deleted file mode 100644 index 1f44a631..00000000 --- a/jsonschema/testdata/remotes/draft2020-12/baseUriChangeFolderInSubschema/folderInteger.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "integer" -} diff --git a/jsonschema/testdata/remotes/draft2020-12/detached-dynamicref.json b/jsonschema/testdata/remotes/draft2020-12/detached-dynamicref.json deleted file mode 100644 index 07cce1da..00000000 --- a/jsonschema/testdata/remotes/draft2020-12/detached-dynamicref.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "$id": "http://localhost:1234/draft2020-12/detached-dynamicref.json", - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$defs": { - "foo": { - "$dynamicRef": "#detached" - }, - "detached": { - "$dynamicAnchor": "detached", - "type": "integer" - } - } -} \ No newline at end of file diff --git a/jsonschema/testdata/remotes/draft2020-12/detached-ref.json b/jsonschema/testdata/remotes/draft2020-12/detached-ref.json deleted file mode 100644 index 9c2dca93..00000000 --- a/jsonschema/testdata/remotes/draft2020-12/detached-ref.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "$id": "http://localhost:1234/draft2020-12/detached-ref.json", - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$defs": { - "foo": { - "$ref": "#detached" - }, - "detached": { - "$anchor": "detached", - "type": "integer" - } - } -} \ No newline at end of file diff --git a/jsonschema/testdata/remotes/draft2020-12/extendible-dynamic-ref.json b/jsonschema/testdata/remotes/draft2020-12/extendible-dynamic-ref.json deleted file mode 100644 index 65bc0c21..00000000 --- a/jsonschema/testdata/remotes/draft2020-12/extendible-dynamic-ref.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "description": "extendible array", - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "http://localhost:1234/draft2020-12/extendible-dynamic-ref.json", - "type": "object", - "properties": { - "elements": { - "type": "array", - "items": { - "$dynamicRef": "#elements" - } - } - }, - "required": ["elements"], - "additionalProperties": false, - "$defs": { - "elements": { - "$dynamicAnchor": "elements" - } - } -} diff --git a/jsonschema/testdata/remotes/draft2020-12/format-assertion-false.json b/jsonschema/testdata/remotes/draft2020-12/format-assertion-false.json deleted file mode 100644 index 43a711c9..00000000 --- a/jsonschema/testdata/remotes/draft2020-12/format-assertion-false.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "$id": "http://localhost:1234/draft2020-12/format-assertion-false.json", - "$schema": "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/format-assertion": false - }, - "$dynamicAnchor": "meta", - "allOf": [ - { "$ref": "https://json-schema.org/draft/2020-12/meta/core" }, - { "$ref": "https://json-schema.org/draft/2020-12/meta/format-assertion" } - ] -} diff --git a/jsonschema/testdata/remotes/draft2020-12/format-assertion-true.json b/jsonschema/testdata/remotes/draft2020-12/format-assertion-true.json deleted file mode 100644 index 39c6b0ab..00000000 --- a/jsonschema/testdata/remotes/draft2020-12/format-assertion-true.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "$id": "http://localhost:1234/draft2020-12/format-assertion-true.json", - "$schema": "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/format-assertion": true - }, - "$dynamicAnchor": "meta", - "allOf": [ - { "$ref": "https://json-schema.org/draft/2020-12/meta/core" }, - { "$ref": "https://json-schema.org/draft/2020-12/meta/format-assertion" } - ] -} diff --git a/jsonschema/testdata/remotes/draft2020-12/integer.json b/jsonschema/testdata/remotes/draft2020-12/integer.json deleted file mode 100644 index 1f44a631..00000000 --- a/jsonschema/testdata/remotes/draft2020-12/integer.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "integer" -} diff --git a/jsonschema/testdata/remotes/draft2020-12/locationIndependentIdentifier.json b/jsonschema/testdata/remotes/draft2020-12/locationIndependentIdentifier.json deleted file mode 100644 index 6565a1ee..00000000 --- a/jsonschema/testdata/remotes/draft2020-12/locationIndependentIdentifier.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$defs": { - "refToInteger": { - "$ref": "#foo" - }, - "A": { - "$anchor": "foo", - "type": "integer" - } - } -} diff --git a/jsonschema/testdata/remotes/draft2020-12/metaschema-no-validation.json b/jsonschema/testdata/remotes/draft2020-12/metaschema-no-validation.json deleted file mode 100644 index 71be8b5d..00000000 --- a/jsonschema/testdata/remotes/draft2020-12/metaschema-no-validation.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "http://localhost:1234/draft2020-12/metaschema-no-validation.json", - "$vocabulary": { - "https://json-schema.org/draft/2020-12/vocab/applicator": true, - "https://json-schema.org/draft/2020-12/vocab/core": true - }, - "$dynamicAnchor": "meta", - "allOf": [ - { "$ref": "https://json-schema.org/draft/2020-12/meta/applicator" }, - { "$ref": "https://json-schema.org/draft/2020-12/meta/core" } - ] -} diff --git a/jsonschema/testdata/remotes/draft2020-12/metaschema-optional-vocabulary.json b/jsonschema/testdata/remotes/draft2020-12/metaschema-optional-vocabulary.json deleted file mode 100644 index a6963e54..00000000 --- a/jsonschema/testdata/remotes/draft2020-12/metaschema-optional-vocabulary.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "http://localhost:1234/draft2020-12/metaschema-optional-vocabulary.json", - "$vocabulary": { - "https://json-schema.org/draft/2020-12/vocab/validation": true, - "https://json-schema.org/draft/2020-12/vocab/core": true, - "http://localhost:1234/draft/2020-12/vocab/custom": false - }, - "$dynamicAnchor": "meta", - "allOf": [ - { "$ref": "https://json-schema.org/draft/2020-12/meta/validation" }, - { "$ref": "https://json-schema.org/draft/2020-12/meta/core" } - ] -} diff --git a/jsonschema/testdata/remotes/draft2020-12/name-defs.json b/jsonschema/testdata/remotes/draft2020-12/name-defs.json deleted file mode 100644 index 67bc33c5..00000000 --- a/jsonschema/testdata/remotes/draft2020-12/name-defs.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$defs": { - "orNull": { - "anyOf": [ - { - "type": "null" - }, - { - "$ref": "#" - } - ] - } - }, - "type": "string" -} diff --git a/jsonschema/testdata/remotes/draft2020-12/nested/foo-ref-string.json b/jsonschema/testdata/remotes/draft2020-12/nested/foo-ref-string.json deleted file mode 100644 index 29661ff9..00000000 --- a/jsonschema/testdata/remotes/draft2020-12/nested/foo-ref-string.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "object", - "properties": { - "foo": {"$ref": "string.json"} - } -} diff --git a/jsonschema/testdata/remotes/draft2020-12/nested/string.json b/jsonschema/testdata/remotes/draft2020-12/nested/string.json deleted file mode 100644 index 6607ac53..00000000 --- a/jsonschema/testdata/remotes/draft2020-12/nested/string.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "string" -} diff --git a/jsonschema/testdata/remotes/draft2020-12/prefixItems.json b/jsonschema/testdata/remotes/draft2020-12/prefixItems.json deleted file mode 100644 index acd8293c..00000000 --- a/jsonschema/testdata/remotes/draft2020-12/prefixItems.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "$id": "http://localhost:1234/draft2020-12/prefixItems.json", - "$schema": "https://json-schema.org/draft/2020-12/schema", - "prefixItems": [ - {"type": "string"} - ] -} diff --git a/jsonschema/testdata/remotes/draft2020-12/ref-and-defs.json b/jsonschema/testdata/remotes/draft2020-12/ref-and-defs.json deleted file mode 100644 index 16d30fa3..00000000 --- a/jsonschema/testdata/remotes/draft2020-12/ref-and-defs.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "http://localhost:1234/draft2020-12/ref-and-defs.json", - "$defs": { - "inner": { - "properties": { - "bar": { "type": "string" } - } - } - }, - "$ref": "#/$defs/inner" -} diff --git a/jsonschema/testdata/remotes/draft2020-12/subSchemas.json b/jsonschema/testdata/remotes/draft2020-12/subSchemas.json deleted file mode 100644 index 1bb4846d..00000000 --- a/jsonschema/testdata/remotes/draft2020-12/subSchemas.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$defs": { - "integer": { - "type": "integer" - }, - "refToInteger": { - "$ref": "#/$defs/integer" - } - } -} diff --git a/jsonschema/testdata/remotes/draft2020-12/tree.json b/jsonschema/testdata/remotes/draft2020-12/tree.json deleted file mode 100644 index b07555fb..00000000 --- a/jsonschema/testdata/remotes/draft2020-12/tree.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "description": "tree schema, extensible", - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "http://localhost:1234/draft2020-12/tree.json", - "$dynamicAnchor": "node", - - "type": "object", - "properties": { - "data": true, - "children": { - "type": "array", - "items": { - "$dynamicRef": "#node" - } - } - } -} diff --git a/jsonschema/testdata/remotes/nested-absolute-ref-to-string.json b/jsonschema/testdata/remotes/nested-absolute-ref-to-string.json deleted file mode 100644 index f46c7616..00000000 --- a/jsonschema/testdata/remotes/nested-absolute-ref-to-string.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "$defs": { - "bar": { - "$id": "http://localhost:1234/the-nested-id.json", - "type": "string" - } - }, - "$ref": "http://localhost:1234/the-nested-id.json" -} diff --git a/jsonschema/testdata/remotes/urn-ref-string.json b/jsonschema/testdata/remotes/urn-ref-string.json deleted file mode 100644 index aca2211b..00000000 --- a/jsonschema/testdata/remotes/urn-ref-string.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "$id": "urn:uuid:feebdaed-ffff-0000-ffff-0000deadbeef", - "$defs": {"bar": {"type": "string"}}, - "$ref": "#/$defs/bar" -} diff --git a/jsonschema/util.go b/jsonschema/util.go deleted file mode 100644 index 71c34439..00000000 --- a/jsonschema/util.go +++ /dev/null @@ -1,420 +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 ( - "bytes" - "cmp" - "encoding/binary" - "encoding/json" - "fmt" - "hash/maphash" - "math" - "math/big" - "reflect" - "slices" - "sync" - - "github.com/modelcontextprotocol/go-sdk/internal/util" -) - -// Equal reports whether two Go values representing JSON values are equal according -// to the JSON Schema spec. -// The values must not contain cycles. -// See https://json-schema.org/draft/2020-12/json-schema-core#section-4.2.2. -// It behaves like reflect.DeepEqual, except that numbers are compared according -// to mathematical equality. -func Equal(x, y any) bool { - return equalValue(reflect.ValueOf(x), reflect.ValueOf(y)) -} - -func equalValue(x, y reflect.Value) bool { - // Copied from src/reflect/deepequal.go, omitting the visited check (because JSON - // values are trees). - if !x.IsValid() || !y.IsValid() { - return x.IsValid() == y.IsValid() - } - - // Treat numbers specially. - rx, ok1 := jsonNumber(x) - ry, ok2 := jsonNumber(y) - if ok1 && ok2 { - return rx.Cmp(ry) == 0 - } - if x.Kind() != y.Kind() { - return false - } - switch x.Kind() { - case reflect.Array: - if x.Len() != y.Len() { - return false - } - for i := range x.Len() { - if !equalValue(x.Index(i), y.Index(i)) { - return false - } - } - return true - case reflect.Slice: - if x.IsNil() != y.IsNil() { - return false - } - if x.Len() != y.Len() { - return false - } - if x.UnsafePointer() == y.UnsafePointer() { - return true - } - // Special case for []byte, which is common. - if x.Type().Elem().Kind() == reflect.Uint8 && x.Type() == y.Type() { - return bytes.Equal(x.Bytes(), y.Bytes()) - } - for i := range x.Len() { - if !equalValue(x.Index(i), y.Index(i)) { - return false - } - } - return true - case reflect.Interface: - if x.IsNil() || y.IsNil() { - return x.IsNil() == y.IsNil() - } - return equalValue(x.Elem(), y.Elem()) - case reflect.Pointer: - if x.UnsafePointer() == y.UnsafePointer() { - return true - } - return equalValue(x.Elem(), y.Elem()) - case reflect.Struct: - t := x.Type() - if t != y.Type() { - return false - } - for i := range t.NumField() { - sf := t.Field(i) - if !sf.IsExported() { - continue - } - if !equalValue(x.FieldByIndex(sf.Index), y.FieldByIndex(sf.Index)) { - return false - } - } - return true - case reflect.Map: - if x.IsNil() != y.IsNil() { - return false - } - if x.Len() != y.Len() { - return false - } - if x.UnsafePointer() == y.UnsafePointer() { - return true - } - iter := x.MapRange() - for iter.Next() { - vx := iter.Value() - vy := y.MapIndex(iter.Key()) - if !vy.IsValid() || !equalValue(vx, vy) { - return false - } - } - return true - case reflect.Func: - if x.Type() != y.Type() { - return false - } - if x.IsNil() && y.IsNil() { - return true - } - panic("cannot compare functions") - case reflect.String: - return x.String() == y.String() - case reflect.Bool: - return x.Bool() == y.Bool() - // Ints, uints and floats handled in jsonNumber, at top of function. - default: - panic(fmt.Sprintf("unsupported kind: %s", x.Kind())) - } -} - -// hashValue adds v to the data hashed by h. v must not have cycles. -// hashValue panics if the value contains functions or channels, or maps whose -// key type is not string. -// It ignores unexported fields of structs. -// Calls to hashValue with the equal values (in the sense -// of [Equal]) result in the same sequence of values written to the hash. -func hashValue(h *maphash.Hash, v reflect.Value) { - // TODO: replace writes of basic types with WriteComparable in 1.24. - - writeUint := func(u uint64) { - var buf [8]byte - binary.BigEndian.PutUint64(buf[:], u) - h.Write(buf[:]) - } - - var write func(reflect.Value) - write = func(v reflect.Value) { - if r, ok := jsonNumber(v); ok { - // We want 1.0 and 1 to hash the same. - // big.Rats are always normalized, so they will be. - // We could do this more efficiently by handling the int and float cases - // separately, but that's premature. - writeUint(uint64(r.Sign() + 1)) - h.Write(r.Num().Bytes()) - h.Write(r.Denom().Bytes()) - return - } - switch v.Kind() { - case reflect.Invalid: - h.WriteByte(0) - case reflect.String: - h.WriteString(v.String()) - case reflect.Bool: - if v.Bool() { - h.WriteByte(1) - } else { - h.WriteByte(0) - } - case reflect.Complex64, reflect.Complex128: - c := v.Complex() - writeUint(math.Float64bits(real(c))) - writeUint(math.Float64bits(imag(c))) - case reflect.Array, reflect.Slice: - // Although we could treat []byte more efficiently, - // JSON values are unlikely to contain them. - writeUint(uint64(v.Len())) - for i := range v.Len() { - write(v.Index(i)) - } - case reflect.Interface, reflect.Pointer: - write(v.Elem()) - case reflect.Struct: - t := v.Type() - for i := range t.NumField() { - if sf := t.Field(i); sf.IsExported() { - write(v.FieldByIndex(sf.Index)) - } - } - case reflect.Map: - if v.Type().Key().Kind() != reflect.String { - panic("map with non-string key") - } - // Sort the keys so the hash is deterministic. - keys := v.MapKeys() - // Write the length. That distinguishes between, say, two consecutive - // maps with disjoint keys from one map that has the items of both. - writeUint(uint64(len(keys))) - slices.SortFunc(keys, func(x, y reflect.Value) int { return cmp.Compare(x.String(), y.String()) }) - for _, k := range keys { - write(k) - write(v.MapIndex(k)) - } - // Ints, uints and floats handled in jsonNumber, at top of function. - default: - panic(fmt.Sprintf("unsupported kind: %s", v.Kind())) - } - } - - write(v) -} - -// jsonNumber converts a numeric value or a json.Number to a [big.Rat]. -// If v is not a number, it returns nil, false. -func jsonNumber(v reflect.Value) (*big.Rat, bool) { - r := new(big.Rat) - switch { - case !v.IsValid(): - return nil, false - case v.CanInt(): - r.SetInt64(v.Int()) - case v.CanUint(): - r.SetUint64(v.Uint()) - case v.CanFloat(): - r.SetFloat64(v.Float()) - default: - jn, ok := v.Interface().(json.Number) - if !ok { - return nil, false - } - if _, ok := r.SetString(jn.String()); !ok { - // This can fail in rare cases; for example, "1e9999999". - // That is a valid JSON number, since the spec puts no limit on the size - // of the exponent. - return nil, false - } - } - return r, true -} - -// jsonType returns a string describing the type of the JSON value, -// as described in the JSON Schema specification: -// https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-01#section-6.1.1. -// It returns "", false if the value is not valid JSON. -func jsonType(v reflect.Value) (string, bool) { - if !v.IsValid() { - // Not v.IsNil(): a nil []any is still a JSON array. - return "null", true - } - if v.CanInt() || v.CanUint() { - return "integer", true - } - if v.CanFloat() { - if _, f := math.Modf(v.Float()); f == 0 { - return "integer", true - } - return "number", true - } - switch v.Kind() { - case reflect.Bool: - return "boolean", true - case reflect.String: - return "string", true - case reflect.Slice, reflect.Array: - return "array", true - case reflect.Map, reflect.Struct: - return "object", true - default: - return "", false - } -} - -func assert(cond bool, msg string) { - if !cond { - panic("assertion failed: " + msg) - } -} - -// marshalStructWithMap marshals its first argument to JSON, treating the field named -// mapField as an embedded map. The first argument must be a pointer to -// a struct. The underlying type of mapField must be a map[string]any, and it must have -// a "-" json tag, meaning it will not be marshaled. -// -// For example, given this struct: -// -// type S struct { -// A int -// Extra map[string] any `json:"-"` -// } -// -// and this value: -// -// s := S{A: 1, Extra: map[string]any{"B": 2}} -// -// the call marshalJSONWithMap(s, "Extra") would return -// -// {"A": 1, "B": 2} -// -// It is an error if the map contains the same key as another struct field's -// JSON name. -// -// marshalStructWithMap calls json.Marshal on a value of type T, so T must not -// have a MarshalJSON method that calls this function, on pain of infinite regress. -// -// Note that there is a similar function in mcp/util.go, but they are not the same. -// Here the function requires `-` json tag, does not clear the mapField map, -// and handles embedded struct due to the implementation of jsonNames in this package. -// -// TODO: avoid this restriction on T by forcing it to marshal in a default way. -// See https://go.dev/play/p/EgXKJHxEx_R. -func marshalStructWithMap[T any](s *T, mapField string) ([]byte, error) { - // Marshal the struct and the map separately, and concatenate the bytes. - // This strategy is dramatically less complicated than - // constructing a synthetic struct or map with the combined keys. - if s == nil { - return []byte("null"), nil - } - s2 := *s - vMapField := reflect.ValueOf(&s2).Elem().FieldByName(mapField) - mapVal := vMapField.Interface().(map[string]any) - - // Check for duplicates. - names := jsonNames(reflect.TypeFor[T]()) - for key := range mapVal { - if names[key] { - return nil, fmt.Errorf("map key %q duplicates struct field", key) - } - } - - structBytes, err := json.Marshal(s2) - if err != nil { - return nil, fmt.Errorf("marshalStructWithMap(%+v): %w", s, err) - } - if len(mapVal) == 0 { - return structBytes, nil - } - mapBytes, err := json.Marshal(mapVal) - if err != nil { - return nil, err - } - if len(structBytes) == 2 { // must be "{}" - return mapBytes, nil - } - // "{X}" + "{Y}" => "{X,Y}" - res := append(structBytes[:len(structBytes)-1], ',') - res = append(res, mapBytes[1:]...) - return res, nil -} - -// unmarshalStructWithMap is the inverse of marshalStructWithMap. -// T has the same restrictions as in that function. -// -// Note that there is a similar function in mcp/util.go, but they are not the same. -// Here jsonNames also returns fields from embedded structs, hence this function -// handles embedded structs as well. -func unmarshalStructWithMap[T any](data []byte, v *T, mapField string) error { - // Unmarshal into the struct, ignoring unknown fields. - if err := json.Unmarshal(data, v); err != nil { - return err - } - // Unmarshal into the map. - m := map[string]any{} - if err := json.Unmarshal(data, &m); err != nil { - return err - } - // Delete from the map the fields of the struct. - for n := range jsonNames(reflect.TypeFor[T]()) { - delete(m, n) - } - if len(m) != 0 { - reflect.ValueOf(v).Elem().FieldByName(mapField).Set(reflect.ValueOf(m)) - } - return nil -} - -var jsonNamesMap sync.Map // from reflect.Type to map[string]bool - -// jsonNames returns the set of JSON object keys that t will marshal into, -// including fields from embedded structs in t. -// t must be a struct type. -// -// Note that there is a similar function in mcp/util.go, but they are not the same -// Here the function recurses over embedded structs and includes fields from them. -func jsonNames(t reflect.Type) map[string]bool { - // Lock not necessary: at worst we'll duplicate work. - if val, ok := jsonNamesMap.Load(t); ok { - return val.(map[string]bool) - } - m := map[string]bool{} - for i := range t.NumField() { - field := t.Field(i) - // handle embedded structs - if field.Anonymous { - fieldType := field.Type - if fieldType.Kind() == reflect.Ptr { - fieldType = fieldType.Elem() - } - for n := range jsonNames(fieldType) { - m[n] = true - } - continue - } - info := util.FieldJSONInfo(field) - if !info.Omit { - m[info.Name] = true - } - } - jsonNamesMap.Store(t, m) - return m -} diff --git a/jsonschema/util_test.go b/jsonschema/util_test.go deleted file mode 100644 index 03ccb4d7..00000000 --- a/jsonschema/util_test.go +++ /dev/null @@ -1,186 +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 ( - "encoding/json" - "hash/maphash" - "reflect" - "strings" - "testing" - - "github.com/google/go-cmp/cmp" - "github.com/google/go-cmp/cmp/cmpopts" -) - -func TestEqual(t *testing.T) { - for _, tt := range []struct { - x1, x2 any - want bool - }{ - {0, 1, false}, - {1, 1.0, true}, - {nil, 0, false}, - {"0", 0, false}, - {2.5, 2.5, true}, - {[]int{1, 2}, []float64{1.0, 2.0}, true}, - {[]int(nil), []int{}, false}, - {[]map[string]any(nil), []map[string]any{}, false}, - { - map[string]any{"a": 1, "b": 2.0}, - map[string]any{"a": 1.0, "b": 2}, - true, - }, - } { - check := func(x1, x2 any, want bool) { - t.Helper() - if got := Equal(x1, x2); got != want { - t.Errorf("jsonEqual(%#v, %#v) = %t, want %t", x1, x2, got, want) - } - } - check(tt.x1, tt.x1, true) - check(tt.x2, tt.x2, true) - check(tt.x1, tt.x2, tt.want) - check(tt.x2, tt.x1, tt.want) - } -} - -func TestJSONType(t *testing.T) { - for _, tt := range []struct { - val string - want string - }{ - {`null`, "null"}, - {`0`, "integer"}, - {`0.0`, "integer"}, - {`1e2`, "integer"}, - {`0.1`, "number"}, - {`""`, "string"}, - {`true`, "boolean"}, - {`[]`, "array"}, - {`{}`, "object"}, - } { - var val any - if err := json.Unmarshal([]byte(tt.val), &val); err != nil { - t.Fatal(err) - } - got, ok := jsonType(reflect.ValueOf(val)) - if !ok { - t.Fatalf("jsonType failed on %q", tt.val) - } - if got != tt.want { - t.Errorf("%s: got %q, want %q", tt.val, got, tt.want) - } - - } -} - -func TestHash(t *testing.T) { - x := map[string]any{ - "s": []any{1, "foo", nil, true}, - "f": 2.5, - "m": map[string]any{ - "n": json.Number("123.456"), - "schema": &Schema{Type: "integer", UniqueItems: true}, - }, - "c": 1.2 + 3.4i, - "n": nil, - } - - seed := maphash.MakeSeed() - - hash := func(x any) uint64 { - var h maphash.Hash - h.SetSeed(seed) - hashValue(&h, reflect.ValueOf(x)) - return h.Sum64() - } - - want := hash(x) - // Run several times to verify consistency. - for range 10 { - if got := hash(x); got != want { - t.Errorf("hash values differ: %d vs. %d", got, want) - } - } - - // Check mathematically equal values. - nums := []any{ - 5, - uint(5), - 5.0, - json.Number("5"), - json.Number("5.00"), - } - for i, n := range nums { - if i == 0 { - want = hash(n) - } else if got := hash(n); got != want { - t.Errorf("hashes differ between %v (%[1]T) and %v (%[2]T)", nums[0], n) - } - } - - // Check that a bare JSON `null` is OK. - var null any - if err := json.Unmarshal([]byte(`null`), &null); err != nil { - t.Fatal(err) - } - _ = hash(null) -} - -func TestMarshalStructWithMap(t *testing.T) { - type S struct { - A int - B string `json:"b,omitempty"` - u bool - M map[string]any `json:"-"` - } - t.Run("basic", func(t *testing.T) { - s := S{A: 1, B: "two", M: map[string]any{"!@#": true}} - got, err := marshalStructWithMap(&s, "M") - if err != nil { - t.Fatal(err) - } - want := `{"A":1,"b":"two","!@#":true}` - if g := string(got); g != want { - t.Errorf("\ngot %s\nwant %s", g, want) - } - - var un S - if err := unmarshalStructWithMap(got, &un, "M"); err != nil { - t.Fatal(err) - } - if diff := cmp.Diff(s, un, cmpopts.IgnoreUnexported(S{})); diff != "" { - t.Errorf("mismatch (-want, +got):\n%s", diff) - } - }) - t.Run("duplicate", func(t *testing.T) { - s := S{A: 1, B: "two", M: map[string]any{"b": "dup"}} - _, err := marshalStructWithMap(&s, "M") - if err == nil || !strings.Contains(err.Error(), "duplicate") { - t.Errorf("got %v, want error with 'duplicate'", err) - } - }) - t.Run("embedded", func(t *testing.T) { - type Embedded struct { - A int - B int - Extra map[string]any `json:"-"` - } - type S struct { - C int - Embedded - } - s := S{C: 1, Embedded: Embedded{A: 2, B: 3, Extra: map[string]any{"d": 4, "e": 5}}} - got, err := marshalStructWithMap(&s, "Extra") - if err != nil { - t.Fatal(err) - } - want := `{"C":1,"A":2,"B":3,"d":4,"e":5}` - if g := string(got); g != want { - t.Errorf("got %v, want %v", g, want) - } - }) -} diff --git a/jsonschema/validate.go b/jsonschema/validate.go deleted file mode 100644 index 3b864107..00000000 --- a/jsonschema/validate.go +++ /dev/null @@ -1,760 +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 ( - "encoding/json" - "fmt" - "hash/maphash" - "iter" - "math" - "math/big" - "reflect" - "slices" - "strings" - "sync" - "unicode/utf8" - - "github.com/modelcontextprotocol/go-sdk/internal/util" -) - -// The value of the "$schema" keyword for the version that we can validate. -const draft202012 = "https://json-schema.org/draft/2020-12/schema" - -// Validate validates the instance, which must be a JSON value, against the schema. -// It returns nil if validation is successful or an error if it is not. -// If the schema type is "object", instance can be a map[string]any or a struct. -func (rs *Resolved) Validate(instance any) error { - if s := rs.root.Schema; s != "" && s != draft202012 { - return fmt.Errorf("cannot validate version %s, only %s", s, draft202012) - } - st := &state{rs: rs} - return st.validate(reflect.ValueOf(instance), st.rs.root, nil) -} - -// validateDefaults walks the schema tree. If it finds a default, it validates it -// against the schema containing it. -// -// TODO(jba): account for dynamic refs. This algorithm simple-mindedly -// treats each schema with a default as its own root. -func (rs *Resolved) validateDefaults() error { - if s := rs.root.Schema; s != "" && s != draft202012 { - return fmt.Errorf("cannot validate version %s, only %s", s, draft202012) - } - st := &state{rs: rs} - for s := range rs.root.all() { - // We checked for nil schemas in [Schema.Resolve]. - assert(s != nil, "nil schema") - if s.DynamicRef != "" { - return fmt.Errorf("jsonschema: %s: validateDefaults does not support dynamic refs", rs.schemaString(s)) - } - if s.Default != nil { - var d any - if err := json.Unmarshal(s.Default, &d); err != nil { - return fmt.Errorf("unmarshaling default value of schema %s: %w", rs.schemaString(s), err) - } - if err := st.validate(reflect.ValueOf(d), s, nil); err != nil { - return err - } - } - } - return nil -} - -// state is the state of single call to ResolvedSchema.Validate. -type state struct { - rs *Resolved - // stack holds the schemas from recursive calls to validate. - // These are the "dynamic scopes" used to resolve dynamic references. - // https://json-schema.org/draft/2020-12/json-schema-core#scopes - stack []*Schema -} - -// validate validates the reflected value of the instance. -func (st *state) validate(instance reflect.Value, schema *Schema, callerAnns *annotations) (err error) { - defer util.Wrapf(&err, "validating %s", st.rs.schemaString(schema)) - - // Maintain a stack for dynamic schema resolution. - st.stack = append(st.stack, schema) // push - defer func() { - st.stack = st.stack[:len(st.stack)-1] // pop - }() - - // We checked for nil schemas in [Schema.Resolve]. - assert(schema != nil, "nil schema") - - // Step through interfaces and pointers. - for instance.Kind() == reflect.Pointer || instance.Kind() == reflect.Interface { - instance = instance.Elem() - } - - schemaInfo := st.rs.resolvedInfos[schema] - - // type: https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-01#section-6.1.1 - if schema.Type != "" || schema.Types != nil { - gotType, ok := jsonType(instance) - if !ok { - return fmt.Errorf("type: %v of type %[1]T is not a valid JSON value", instance) - } - if schema.Type != "" { - // "number" subsumes integers - if !(gotType == schema.Type || - gotType == "integer" && schema.Type == "number") { - 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: %v has type %q, want one of %q", - instance, gotType, strings.Join(schema.Types, ", ")) - } - } - } - // enum: https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-01#section-6.1.2 - if schema.Enum != nil { - ok := false - for _, e := range schema.Enum { - if equalValue(reflect.ValueOf(e), instance) { - ok = true - break - } - } - if !ok { - return fmt.Errorf("enum: %v does not equal any of: %v", instance, schema.Enum) - } - } - - // const: https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-01#section-6.1.3 - if schema.Const != nil { - if !equalValue(reflect.ValueOf(*schema.Const), instance) { - return fmt.Errorf("const: %v does not equal %v", instance, *schema.Const) - } - } - - // numbers: https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-01#section-6.2 - if schema.MultipleOf != nil || schema.Minimum != nil || schema.Maximum != nil || schema.ExclusiveMinimum != nil || schema.ExclusiveMaximum != nil { - n, ok := jsonNumber(instance) - if ok { // these keywords don't apply to non-numbers - if schema.MultipleOf != nil { - // TODO: validate MultipleOf as non-zero. - // The test suite assumes floats. - nf, _ := n.Float64() // don't care if it's exact or not - if _, f := math.Modf(nf / *schema.MultipleOf); f != 0 { - return fmt.Errorf("multipleOf: %s is not a multiple of %f", n, *schema.MultipleOf) - } - } - - m := new(big.Rat) // reuse for all of the following - cmp := func(f float64) int { return n.Cmp(m.SetFloat64(f)) } - - if schema.Minimum != nil && cmp(*schema.Minimum) < 0 { - return fmt.Errorf("minimum: %s is less than %f", n, *schema.Minimum) - } - if schema.Maximum != nil && cmp(*schema.Maximum) > 0 { - return fmt.Errorf("maximum: %s is greater than %f", n, *schema.Maximum) - } - if schema.ExclusiveMinimum != nil && cmp(*schema.ExclusiveMinimum) <= 0 { - return fmt.Errorf("exclusiveMinimum: %s is less than or equal to %f", n, *schema.ExclusiveMinimum) - } - if schema.ExclusiveMaximum != nil && cmp(*schema.ExclusiveMaximum) >= 0 { - return fmt.Errorf("exclusiveMaximum: %s is greater than or equal to %f", n, *schema.ExclusiveMaximum) - } - } - } - - // strings: https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-01#section-6.3 - if instance.Kind() == reflect.String && (schema.MinLength != nil || schema.MaxLength != nil || schema.Pattern != "") { - str := instance.String() - n := utf8.RuneCountInString(str) - if schema.MinLength != nil { - if m := *schema.MinLength; n < m { - return fmt.Errorf("minLength: %q contains %d Unicode code points, fewer than %d", str, n, m) - } - } - if schema.MaxLength != nil { - if m := *schema.MaxLength; n > m { - return fmt.Errorf("maxLength: %q contains %d Unicode code points, more than %d", str, n, m) - } - } - - if schema.Pattern != "" && !schemaInfo.pattern.MatchString(str) { - return fmt.Errorf("pattern: %q does not match regular expression %q", str, schema.Pattern) - } - } - - var anns annotations // all the annotations for this call and child calls - - // $ref: https://json-schema.org/draft/2020-12/json-schema-core#section-8.2.3.1 - if schema.Ref != "" { - if err := st.validate(instance, schemaInfo.resolvedRef, &anns); err != nil { - return err - } - } - - // $dynamicRef: https://json-schema.org/draft/2020-12/json-schema-core#section-8.2.3.2 - if schema.DynamicRef != "" { - // The ref behaves lexically or dynamically, but not both. - assert((schemaInfo.resolvedDynamicRef == nil) != (schemaInfo.dynamicRefAnchor == ""), - "DynamicRef not resolved properly") - if schemaInfo.resolvedDynamicRef != nil { - // Same as $ref. - if err := st.validate(instance, schemaInfo.resolvedDynamicRef, &anns); err != nil { - return err - } - } else { - // Dynamic behavior. - // Look for the base of the outermost schema on the stack with this dynamic - // anchor. (Yes, outermost: the one farthest from here. This the opposite - // of how ordinary dynamic variables behave.) - // Why the base of the schema being validated and not the schema itself? - // Because the base is the scope for anchors. In fact it's possible to - // refer to a schema that is not on the stack, but a child of some base - // on the stack. - // For an example, search for "detached" in testdata/draft2020-12/dynamicRef.json. - var dynamicSchema *Schema - for _, s := range st.stack { - base := st.rs.resolvedInfos[s].base - info, ok := st.rs.resolvedInfos[base].anchors[schemaInfo.dynamicRefAnchor] - if ok && info.dynamic { - dynamicSchema = info.schema - break - } - } - if dynamicSchema == nil { - return fmt.Errorf("missing dynamic anchor %q", schemaInfo.dynamicRefAnchor) - } - if err := st.validate(instance, dynamicSchema, &anns); err != nil { - return err - } - } - } - - // logic - // https://json-schema.org/draft/2020-12/json-schema-core#section-10.2 - // These must happen before arrays and objects because if they evaluate an item or property, - // then the unevaluatedItems/Properties schemas don't apply to it. - // See https://json-schema.org/draft/2020-12/json-schema-core#section-11.2, paragraph 4. - // - // If any of these fail, then validation fails, even if there is an unevaluatedXXX - // keyword in the schema. The spec is unclear about this, but that is the intention. - - valid := func(s *Schema, anns *annotations) bool { return st.validate(instance, s, anns) == nil } - - if schema.AllOf != nil { - for _, ss := range schema.AllOf { - if err := st.validate(instance, ss, &anns); err != nil { - return err - } - } - } - if schema.AnyOf != nil { - // We must visit them all, to collect annotations. - ok := false - for _, ss := range schema.AnyOf { - if valid(ss, &anns) { - ok = true - } - } - if !ok { - return fmt.Errorf("anyOf: did not validate against any of %v", schema.AnyOf) - } - } - if schema.OneOf != nil { - // Exactly one. - var okSchema *Schema - for _, ss := range schema.OneOf { - if valid(ss, &anns) { - if okSchema != nil { - return fmt.Errorf("oneOf: validated against both %v and %v", okSchema, ss) - } - okSchema = ss - } - } - if okSchema == nil { - return fmt.Errorf("oneOf: did not validate against any of %v", schema.OneOf) - } - } - if schema.Not != nil { - // Ignore annotations from "not". - if valid(schema.Not, nil) { - return fmt.Errorf("not: validated against %v", schema.Not) - } - } - if schema.If != nil { - var ss *Schema - if valid(schema.If, &anns) { - ss = schema.Then - } else { - ss = schema.Else - } - if ss != nil { - if err := st.validate(instance, ss, &anns); err != nil { - return err - } - } - } - - // arrays - // TODO(jba): consider arrays of structs. - if instance.Kind() == reflect.Array || instance.Kind() == reflect.Slice { - // https://json-schema.org/draft/2020-12/json-schema-core#section-10.3.1 - // This validate call doesn't collect annotations for the items of the instance; they are separate - // instances in their own right. - // TODO(jba): if the test suite doesn't cover this case, add a test. For example, nested arrays. - for i, ischema := range schema.PrefixItems { - if i >= instance.Len() { - break // shorter is OK - } - if err := st.validate(instance.Index(i), ischema, nil); err != nil { - return err - } - } - anns.noteEndIndex(min(len(schema.PrefixItems), instance.Len())) - - if schema.Items != nil { - for i := len(schema.PrefixItems); i < instance.Len(); i++ { - if err := st.validate(instance.Index(i), schema.Items, nil); err != nil { - return err - } - } - // Note that all the items in this array have been validated. - anns.allItems = true - } - - nContains := 0 - if schema.Contains != nil { - for i := range instance.Len() { - if err := st.validate(instance.Index(i), schema.Contains, nil); err == nil { - nContains++ - anns.noteIndex(i) - } - } - if nContains == 0 && (schema.MinContains == nil || *schema.MinContains > 0) { - return fmt.Errorf("contains: %s does not have an item matching %s", instance, schema.Contains) - } - } - - // https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-01#section-6.4 - // TODO(jba): check that these next four keywords' values are integers. - if schema.MinContains != nil && schema.Contains != nil { - if m := *schema.MinContains; nContains < m { - return fmt.Errorf("minContains: contains validated %d items, less than %d", nContains, m) - } - } - if schema.MaxContains != nil && schema.Contains != nil { - if m := *schema.MaxContains; nContains > m { - return fmt.Errorf("maxContains: contains validated %d items, greater than %d", nContains, m) - } - } - if schema.MinItems != nil { - if m := *schema.MinItems; instance.Len() < m { - return fmt.Errorf("minItems: array length %d is less than %d", instance.Len(), m) - } - } - if schema.MaxItems != nil { - if m := *schema.MaxItems; instance.Len() > m { - return fmt.Errorf("maxItems: array length %d is greater than %d", instance.Len(), m) - } - } - if schema.UniqueItems { - if instance.Len() > 1 { - // Hash each item and compare the hashes. - // If two hashes differ, the items differ. - // If two hashes are the same, compare the collisions for equality. - // (The same logic as hash table lookup.) - // TODO(jba): Use container/hash.Map when it becomes available (https://go.dev/issue/69559), - hashes := map[uint64][]int{} // from hash to indices - seed := maphash.MakeSeed() - for i := range instance.Len() { - item := instance.Index(i) - var h maphash.Hash - h.SetSeed(seed) - hashValue(&h, item) - hv := h.Sum64() - if sames := hashes[hv]; len(sames) > 0 { - for _, j := range sames { - if equalValue(item, instance.Index(j)) { - return fmt.Errorf("uniqueItems: array items %d and %d are equal", i, j) - } - } - } - hashes[hv] = append(hashes[hv], i) - } - } - } - - // https://json-schema.org/draft/2020-12/json-schema-core#section-11.2 - if schema.UnevaluatedItems != nil && !anns.allItems { - // Apply this subschema to all items in the array that haven't been successfully validated. - // That includes validations by subschemas on the same instance, like allOf. - for i := anns.endIndex; i < instance.Len(); i++ { - if !anns.evaluatedIndexes[i] { - if err := st.validate(instance.Index(i), schema.UnevaluatedItems, nil); err != nil { - return err - } - } - } - anns.allItems = true - } - } - - // objects - // https://json-schema.org/draft/2020-12/json-schema-core#section-10.3.2 - if instance.Kind() == reflect.Map || instance.Kind() == reflect.Struct { - 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, subschema := range schema.Properties { - val := property(instance, prop) - if !val.IsValid() { - // It's OK if the instance doesn't have the property. - continue - } - // If the instance is a struct and an optional property has the zero - // value, then we could interpret it as present or missing. Be generous: - // assume it's missing, and thus always validates successfully. - if instance.Kind() == reflect.Struct && val.IsZero() && !schemaInfo.isRequired[prop] { - continue - } - if err := st.validate(val, subschema, nil); err != nil { - return err - } - evalProps[prop] = true - } - if len(schema.PatternProperties) > 0 { - for prop, val := range properties(instance) { - // Check every matching pattern. - for re, schema := range schemaInfo.patternProperties { - if re.MatchString(prop) { - if err := st.validate(val, schema, nil); err != nil { - return err - } - evalProps[prop] = true - } - } - } - } - if schema.AdditionalProperties != nil { - // Apply to all properties not handled above. - for prop, val := range properties(instance) { - if !evalProps[prop] { - if err := st.validate(val, schema.AdditionalProperties, nil); err != nil { - return err - } - evalProps[prop] = true - } - } - } - anns.noteProperties(evalProps) - if schema.PropertyNames != nil { - // Note: properties unnecessarily fetches each value. We could define a propertyNames function - // if performance ever matters. - for prop := range properties(instance) { - if err := st.validate(reflect.ValueOf(prop), schema.PropertyNames, nil); err != nil { - return err - } - } - } - - // https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-01#section-6.5 - var min, max int - if schema.MinProperties != nil || schema.MaxProperties != nil { - min, max = numPropertiesBounds(instance, schemaInfo.isRequired) - } - if schema.MinProperties != nil { - if n, m := max, *schema.MinProperties; n < m { - return fmt.Errorf("minProperties: object has %d properties, less than %d", n, m) - } - } - if schema.MaxProperties != nil { - if n, m := min, *schema.MaxProperties; n > m { - return fmt.Errorf("maxProperties: object has %d properties, greater than %d", n, m) - } - } - - hasProperty := func(prop string) bool { - return property(instance, 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) - 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 prop, val := range properties(instance) { - if !anns.evaluatedProperties[prop] { - if err := st.validate(val, schema.UnevaluatedProperties, nil); 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) - } - return nil -} - -// resolveDynamicRef returns the schema referred to by the argument schema's -// $dynamicRef value. -// It returns an error if the dynamic reference has no referent. -// If there is no $dynamicRef, resolveDynamicRef returns nil, nil. -// See https://json-schema.org/draft/2020-12/json-schema-core#section-8.2.3.2. -func (st *state) resolveDynamicRef(schema *Schema) (*Schema, error) { - if schema.DynamicRef == "" { - return nil, nil - } - info := st.rs.resolvedInfos[schema] - // The ref behaves lexically or dynamically, but not both. - assert((info.resolvedDynamicRef == nil) != (info.dynamicRefAnchor == ""), - "DynamicRef not statically resolved properly") - if r := info.resolvedDynamicRef; r != nil { - // Same as $ref. - return r, nil - } - // Dynamic behavior. - // Look for the base of the outermost schema on the stack with this dynamic - // anchor. (Yes, outermost: the one farthest from here. This the opposite - // of how ordinary dynamic variables behave.) - // Why the base of the schema being validated and not the schema itself? - // Because the base is the scope for anchors. In fact it's possible to - // refer to a schema that is not on the stack, but a child of some base - // on the stack. - // For an example, search for "detached" in testdata/draft2020-12/dynamicRef.json. - for _, s := range st.stack { - base := st.rs.resolvedInfos[s].base - info, ok := st.rs.resolvedInfos[base].anchors[info.dynamicRefAnchor] - if ok && info.dynamic { - return info.schema, nil - } - } - return nil, fmt.Errorf("missing dynamic anchor %q", info.dynamicRefAnchor) -} - -// ApplyDefaults modifies an instance by applying the schema's defaults to it. If -// a schema or sub-schema has a default, then a corresponding zero instance value -// is set to the default. -// -// The JSON Schema specification does not describe how defaults should be interpreted. -// This method honors defaults only on properties, and only those that are not required. -// If the instance is a map and the property is missing, the property is added to -// the map with the default. -// If the instance is a struct, the field corresponding to the property exists, and -// its value is zero, the field is set to the default. -// ApplyDefaults can panic if a default cannot be assigned to a field. -// -// The argument must be a pointer to the instance. -// (In case we decide that top-level defaults are meaningful.) -// -// It is recommended to first call Resolve with a ValidateDefaults option of true, -// then call this method, and lastly call Validate. -// -// TODO(jba): consider what defaults on top-level or array instances might mean. -// TODO(jba): follow $ref and $dynamicRef -// TODO(jba): apply defaults on sub-schemas to corresponding sub-instances. -func (rs *Resolved) ApplyDefaults(instancep any) error { - st := &state{rs: rs} - return st.applyDefaults(reflect.ValueOf(instancep), rs.root) -} - -// Leave this as a potentially recursive helper function, because we'll surely want -// to apply defaults on sub-schemas someday. -func (st *state) applyDefaults(instancep reflect.Value, schema *Schema) (err error) { - defer util.Wrapf(&err, "applyDefaults: schema %s, instance %v", st.rs.schemaString(schema), instancep) - - schemaInfo := st.rs.resolvedInfos[schema] - instance := instancep.Elem() - if instance.Kind() == reflect.Map || instance.Kind() == reflect.Struct { - 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) - } - } - for prop, subschema := range schema.Properties { - // Ignore defaults on required properties. (A required property shouldn't have a default.) - if schemaInfo.isRequired[prop] { - continue - } - val := property(instance, prop) - switch instance.Kind() { - case reflect.Map: - // If there is a default for this property, and the map key is missing, - // set the map value to the default. - if subschema.Default != nil && !val.IsValid() { - // Create an lvalue, since map values aren't addressable. - lvalue := reflect.New(instance.Type().Elem()) - if err := json.Unmarshal(subschema.Default, lvalue.Interface()); err != nil { - return err - } - instance.SetMapIndex(reflect.ValueOf(prop), lvalue.Elem()) - } - case reflect.Struct: - // If there is a default for this property, and the field exists but is zero, - // set the field to the default. - if subschema.Default != nil && val.IsValid() && val.IsZero() { - if err := json.Unmarshal(subschema.Default, val.Addr().Interface()); err != nil { - return err - } - } - default: - panic(fmt.Sprintf("applyDefaults: property %s: bad value %s of kind %s", - prop, instance, instance.Kind())) - } - } - } - return nil -} - -// property returns the value of the property of v with the given name, or the invalid -// reflect.Value if there is none. -// If v is a map, the property is the value of the map whose key is name. -// If v is a struct, the property is the value of the field with the given name according -// to the encoding/json package (see [jsonName]). -// If v is anything else, property panics. -func property(v reflect.Value, name string) reflect.Value { - switch v.Kind() { - case reflect.Map: - return v.MapIndex(reflect.ValueOf(name)) - case reflect.Struct: - props := structPropertiesOf(v.Type()) - // Ignore nonexistent properties. - if sf, ok := props[name]; ok { - return v.FieldByIndex(sf.Index) - } - return reflect.Value{} - default: - panic(fmt.Sprintf("property(%q): bad value %s of kind %s", name, v, v.Kind())) - } -} - -// properties returns an iterator over the names and values of all properties -// in v, which must be a map or a struct. -// If a struct, zero-valued properties that are marked omitempty or omitzero -// are excluded. -func properties(v reflect.Value) iter.Seq2[string, reflect.Value] { - return func(yield func(string, reflect.Value) bool) { - switch v.Kind() { - case reflect.Map: - for k, e := range v.Seq2() { - if !yield(k.String(), e) { - return - } - } - case reflect.Struct: - for name, sf := range structPropertiesOf(v.Type()) { - val := v.FieldByIndex(sf.Index) - if val.IsZero() { - info := util.FieldJSONInfo(sf) - if info.Settings["omitempty"] || info.Settings["omitzero"] { - continue - } - } - if !yield(name, val) { - return - } - } - default: - panic(fmt.Sprintf("bad value %s of kind %s", v, v.Kind())) - } - } -} - -// numPropertiesBounds returns bounds on the number of v's properties. -// v must be a map or a struct. -// If v is a map, both bounds are the map's size. -// If v is a struct, the max is the number of struct properties. -// But since we don't know whether a zero value indicates a missing optional property -// or not, be generous and use the number of non-zero properties as the min. -func numPropertiesBounds(v reflect.Value, isRequired map[string]bool) (int, int) { - switch v.Kind() { - case reflect.Map: - return v.Len(), v.Len() - case reflect.Struct: - sp := structPropertiesOf(v.Type()) - min := 0 - for prop, sf := range sp { - if !v.FieldByIndex(sf.Index).IsZero() || isRequired[prop] { - min++ - } - } - return min, len(sp) - default: - panic(fmt.Sprintf("properties: bad value: %s of kind %s", v, v.Kind())) - } -} - -// A propertyMap is a map from property name to struct field index. -type propertyMap = map[string]reflect.StructField - -var structProperties sync.Map // from reflect.Type to propertyMap - -// structPropertiesOf returns the JSON Schema properties for the struct type t. -// The caller must not mutate the result. -func structPropertiesOf(t reflect.Type) propertyMap { - // Mutex not necessary: at worst we'll recompute the same value. - if props, ok := structProperties.Load(t); ok { - return props.(propertyMap) - } - props := map[string]reflect.StructField{} - for _, sf := range reflect.VisibleFields(t) { - info := util.FieldJSONInfo(sf) - if !info.Omit { - props[info.Name] = sf - } - } - structProperties.Store(t, props) - return props -} diff --git a/jsonschema/validate_test.go b/jsonschema/validate_test.go deleted file mode 100644 index 7fb52d18..00000000 --- a/jsonschema/validate_test.go +++ /dev/null @@ -1,294 +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 ( - "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 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 -} diff --git a/mcp/client.go b/mcp/client.go index 9e3f3935..d3139f54 100644 --- a/mcp/client.go +++ b/mcp/client.go @@ -134,8 +134,8 @@ func (c *Client) Connect(ctx context.Context, t Transport) (cs *ClientSession, e return nil, unsupportedProtocolVersionError{res.ProtocolVersion} } cs.initializeResult = res - if hc, ok := cs.mcpConn.(httpConnection); ok { - hc.setProtocolVersion(res.ProtocolVersion) + if hc, ok := cs.mcpConn.(clientConnection); ok { + hc.initialized(res) } if err := handleNotify(ctx, cs, notificationInitialized, &InitializedParams{}); err != nil { _ = cs.Close() @@ -153,8 +153,8 @@ func (c *Client) Connect(ctx context.Context, t Transport) (cs *ClientSession, e // methods can be used to send requests or notifications to the server. Create // a session by calling [Client.Connect]. // -// Call [ClientSession.Close] to close the connection, or await client -// termination with [ServerSession.Wait]. +// Call [ClientSession.Close] to close the connection, or await server +// termination with [ClientSession.Wait]. type ClientSession struct { conn *jsonrpc2.Connection client *Client @@ -250,7 +250,7 @@ func (c *Client) listRoots(_ context.Context, _ *ClientSession, _ *ListRootsPara func (c *Client) createMessage(ctx context.Context, cs *ClientSession, params *CreateMessageParams) (*CreateMessageResult, error) { if c.opts.CreateMessageHandler == nil { // TODO: wrap or annotate this error? Pick a standard code? - return nil, &jsonrpc2.WireError{Code: CodeUnsupportedMethod, Message: "client does not support CreateMessage"} + return nil, jsonrpc2.NewError(CodeUnsupportedMethod, "client does not support CreateMessage") } return c.opts.CreateMessageHandler(ctx, cs, params) } diff --git a/mcp/client_list_test.go b/mcp/client_list_test.go index 497a9cd0..5b13a4c8 100644 --- a/mcp/client_list_test.go +++ b/mcp/client_list_test.go @@ -11,7 +11,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" - "github.com/modelcontextprotocol/go-sdk/jsonschema" + "github.com/google/jsonschema-go/jsonschema" "github.com/modelcontextprotocol/go-sdk/mcp" ) diff --git a/mcp/client_test.go b/mcp/client_test.go index 73fe09e6..7920c55c 100644 --- a/mcp/client_test.go +++ b/mcp/client_test.go @@ -11,7 +11,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" - "github.com/modelcontextprotocol/go-sdk/jsonschema" + "github.com/google/jsonschema-go/jsonschema" ) type Item struct { diff --git a/mcp/cmd_test.go b/mcp/cmd_test.go index bc149f4c..82a35a80 100644 --- a/mcp/cmd_test.go +++ b/mcp/cmd_test.go @@ -10,7 +10,9 @@ import ( "log" "os" "os/exec" + "os/signal" "runtime" + "syscall" "testing" "time" @@ -21,14 +23,27 @@ import ( const runAsServer = "_MCP_RUN_AS_SERVER" func TestMain(m *testing.M) { - if os.Getenv(runAsServer) != "" { + // If the runAsServer variable is set, execute the relevant serverFunc + // instead of running tests (aka the fork and exec trick). + if name := os.Getenv(runAsServer); name != "" { + run := serverFuncs[name] + if run == nil { + log.Fatalf("Unknown server %q", name) + } os.Unsetenv(runAsServer) - runServer() + run() return } os.Exit(m.Run()) } +// serverFuncs defines server functions that may be run as subprocesses via +// [TestMain]. +var serverFuncs = map[string]func(){ + "default": runServer, + "cancelContext": runCancelContextServer, +} + func runServer() { ctx := context.Background() @@ -39,6 +54,16 @@ func runServer() { } } +func runCancelContextServer() { + ctx, done := signal.NotifyContext(context.Background(), syscall.SIGINT) + defer done() + + server := mcp.NewServer(testImpl, nil) + if err := server.Run(ctx, mcp.NewStdioTransport()); err != nil { + log.Fatal(err) + } +} + func TestServerRunContextCancel(t *testing.T) { server := mcp.NewServer(&mcp.Implementation{Name: "greeter", Version: "v0.0.1"}, nil) mcp.AddTool(server, &mcp.Tool{Name: "greet", Description: "say hi"}, SayHi) @@ -80,15 +105,18 @@ func TestServerRunContextCancel(t *testing.T) { } func TestServerInterrupt(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("requires POSIX signals") + } requireExec(t) ctx, cancel := context.WithCancel(context.Background()) defer cancel() - cmd := createServerCommand(t) + cmd := createServerCommand(t, "default") client := mcp.NewClient(testImpl, nil) - session, err := client.Connect(ctx, mcp.NewCommandTransport(cmd)) + _, err := client.Connect(ctx, mcp.NewCommandTransport(cmd)) if err != nil { t.Fatal(err) } @@ -101,19 +129,54 @@ func TestServerInterrupt(t *testing.T) { }() // send a signal to the server process to terminate it + cmd.Process.Signal(os.Interrupt) + + // wait for the server to exit + // TODO: use synctest when available + select { + case <-time.After(5 * time.Second): + t.Fatal("server did not exit after SIGINT") + case <-onExit: + } +} + +func TestStdioContextCancellation(t *testing.T) { if runtime.GOOS == "windows" { - // Windows does not support os.Interrupt - session.Close() - } else { - cmd.Process.Signal(os.Interrupt) + t.Skip("requires POSIX signals") } + requireExec(t) + + // This test is a variant of TestServerInterrupt reproducing the conditions + // of #224, where interrupt failed to shut down the server because reads of + // Stdin were not unblocked. + + cmd := createServerCommand(t, "cancelContext") + // Creating a stdin pipe causes os.Stdin.Close to not immediately unblock + // pending reads. + _, _ = cmd.StdinPipe() + + // Just Start the command, rather than connecting to the server, because we + // don't want the client connection to indirectly flush stdin through writes. + if err := cmd.Start(); err != nil { + t.Fatalf("starting command: %v", err) + } + + // Sleep to make it more likely that the server is blocked in the read loop. + time.Sleep(100 * time.Millisecond) + + onExit := make(chan struct{}) + go func() { + cmd.Process.Wait() + close(onExit) + }() + + cmd.Process.Signal(os.Interrupt) - // wait for the server to exit - // TODO: use synctest when availble select { case <-time.After(5 * time.Second): - t.Fatal("server did not exit after SIGTERM") + t.Fatal("server did not exit after SIGINT") case <-onExit: + t.Logf("done.") } } @@ -123,7 +186,7 @@ func TestCmdTransport(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() - cmd := createServerCommand(t) + cmd := createServerCommand(t, "default") client := mcp.NewClient(&mcp.Implementation{Name: "client", Version: "v0.0.1"}, nil) session, err := client.Connect(ctx, mcp.NewCommandTransport(cmd)) @@ -150,7 +213,11 @@ func TestCmdTransport(t *testing.T) { } } -func createServerCommand(t *testing.T) *exec.Cmd { +// createServerCommand creates a command to fork and exec the test binary as an +// MCP server. +// +// serverName must refer to an entry in the [serverFuncs] map. +func createServerCommand(t *testing.T, serverName string) *exec.Cmd { t.Helper() exe, err := os.Executable() @@ -158,7 +225,7 @@ func createServerCommand(t *testing.T) *exec.Cmd { t.Fatal(err) } cmd := exec.Command(exe) - cmd.Env = append(os.Environ(), runAsServer+"=true") + cmd.Env = append(os.Environ(), runAsServer+"="+serverName) return cmd } diff --git a/mcp/conformance_test.go b/mcp/conformance_test.go index 883d8a89..8e6ea1be 100644 --- a/mcp/conformance_test.go +++ b/mcp/conformance_test.go @@ -135,7 +135,7 @@ func runServerTest(t *testing.T, test *conformanceTest) { // Connect the server, and connect the client stream, // but don't connect an actual client. cTransport, sTransport := NewInMemoryTransports() - ss, err := s.Connect(ctx, sTransport) + ss, err := s.Connect(ctx, sTransport, nil) if err != nil { t.Fatal(err) } diff --git a/mcp/example_middleware_test.go b/mcp/example_middleware_test.go index 1328473a..05b07d8a 100644 --- a/mcp/example_middleware_test.go +++ b/mcp/example_middleware_test.go @@ -11,7 +11,7 @@ import ( "os" "time" - "github.com/modelcontextprotocol/go-sdk/jsonschema" + "github.com/google/jsonschema-go/jsonschema" "github.com/modelcontextprotocol/go-sdk/mcp" ) @@ -114,7 +114,7 @@ func Example_loggingMiddleware() { ctx := context.Background() // Connect server and client - serverSession, _ := server.Connect(ctx, serverTransport) + serverSession, _ := server.Connect(ctx, serverTransport, nil) defer serverSession.Close() clientSession, _ := client.Connect(ctx, clientTransport) diff --git a/mcp/features_test.go b/mcp/features_test.go index e0165ecb..1c22ecd3 100644 --- a/mcp/features_test.go +++ b/mcp/features_test.go @@ -11,7 +11,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" - "github.com/modelcontextprotocol/go-sdk/jsonschema" + "github.com/google/jsonschema-go/jsonschema" ) type SayHiParams struct { diff --git a/mcp/logging.go b/mcp/logging.go index 4880e179..e09e000e 100644 --- a/mcp/logging.go +++ b/mcp/logging.go @@ -112,14 +112,12 @@ func NewLoggingHandler(ss *ServerSession, opts *LoggingHandlerOptions) *LoggingH return lh } -// Enabled implements [slog.Handler.Enabled] by comparing level to the [ServerSession]'s level. +// Enabled implements [slog.Handler.Enabled]. func (h *LoggingHandler) Enabled(ctx context.Context, level slog.Level) bool { - // This is also checked in ServerSession.LoggingMessage, so checking it here - // is just an optimization that skips building the JSON. - h.ss.mu.Lock() - mcpLevel := h.ss.logLevel - h.ss.mu.Unlock() - return level >= mcpLevelToSlog(mcpLevel) + // This is already checked in ServerSession.LoggingMessage. Checking it here + // would be an optimization that skips building the JSON, but it would + // end up loading the SessionState twice, so don't do it. + return true } // WithAttrs implements [slog.Handler.WithAttrs]. diff --git a/mcp/mcp_test.go b/mcp/mcp_test.go index da53465c..abc5b398 100644 --- a/mcp/mcp_test.go +++ b/mcp/mcp_test.go @@ -21,8 +21,8 @@ import ( "time" "github.com/google/go-cmp/cmp" + "github.com/google/jsonschema-go/jsonschema" "github.com/modelcontextprotocol/go-sdk/internal/jsonrpc2" - "github.com/modelcontextprotocol/go-sdk/jsonschema" ) type hiParams struct { @@ -104,7 +104,7 @@ func TestEndToEnd(t *testing.T) { s.AddResource(resource2, readHandler) // Connect the server. - ss, err := s.Connect(ctx, st) + ss, err := s.Connect(ctx, st, nil) if err != nil { t.Fatal(err) } @@ -549,7 +549,7 @@ func basicConnection(t *testing.T, config func(*Server)) (*ServerSession, *Clien if config != nil { config(s) } - ss, err := s.Connect(ctx, st) + ss, err := s.Connect(ctx, st, nil) if err != nil { t.Fatal(err) } @@ -598,7 +598,7 @@ func TestBatching(t *testing.T) { ct, st := NewInMemoryTransports() s := NewServer(testImpl, nil) - _, err := s.Connect(ctx, st) + _, err := s.Connect(ctx, st, nil) if err != nil { t.Fatal(err) } @@ -668,7 +668,7 @@ func TestMiddleware(t *testing.T) { ct, st := NewInMemoryTransports() s := NewServer(testImpl, nil) - ss, err := s.Connect(ctx, st) + ss, err := s.Connect(ctx, st, nil) if err != nil { t.Fatal(err) } @@ -777,7 +777,7 @@ func TestNoJSONNull(t *testing.T) { ct = NewLoggingTransport(ct, &logbuf) s := NewServer(testImpl, nil) - ss, err := s.Connect(ctx, st) + ss, err := s.Connect(ctx, st, nil) if err != nil { t.Fatal(err) } @@ -845,7 +845,7 @@ func TestKeepAlive(t *testing.T) { s := NewServer(testImpl, serverOpts) AddTool(s, greetTool(), sayHi) - ss, err := s.Connect(ctx, st) + ss, err := s.Connect(ctx, st, nil) if err != nil { t.Fatal(err) } @@ -889,7 +889,7 @@ func TestKeepAliveFailure(t *testing.T) { // Server without keepalive (to test one-sided keepalive) s := NewServer(testImpl, nil) AddTool(s, greetTool(), sayHi) - ss, err := s.Connect(ctx, st) + ss, err := s.Connect(ctx, st, nil) if err != nil { t.Fatal(err) } diff --git a/mcp/protocol.go b/mcp/protocol.go index 3ca6cb5e..d2d343b8 100644 --- a/mcp/protocol.go +++ b/mcp/protocol.go @@ -14,7 +14,7 @@ import ( "encoding/json" "fmt" - "github.com/modelcontextprotocol/go-sdk/jsonschema" + "github.com/google/jsonschema-go/jsonschema" ) // Optional annotations for the client. The client can use annotations to inform @@ -78,6 +78,8 @@ type CallToolResultFor[Out any] struct { IsError bool `json:"isError,omitempty"` } +func (*CallToolResultFor[Out]) isResult() {} + // UnmarshalJSON handles the unmarshalling of content into the Content // interface. func (x *CallToolResultFor[Out]) UnmarshalJSON(data []byte) error { @@ -97,6 +99,7 @@ func (x *CallToolResultFor[Out]) UnmarshalJSON(data []byte) error { return nil } +func (x *CallToolParamsFor[Out]) isParams() {} func (x *CallToolParamsFor[Out]) GetProgressToken() any { return getProgressToken(x) } func (x *CallToolParamsFor[Out]) SetProgressToken(t any) { setProgressToken(x, t) } @@ -114,6 +117,7 @@ type CancelledParams struct { RequestID any `json:"requestId"` } +func (x *CancelledParams) isParams() {} func (x *CancelledParams) GetProgressToken() any { return getProgressToken(x) } func (x *CancelledParams) SetProgressToken(t any) { setProgressToken(x, t) } @@ -207,6 +211,8 @@ type CompleteParams struct { Ref *CompleteReference `json:"ref"` } +func (*CompleteParams) isParams() {} + type CompletionResultDetails struct { HasMore bool `json:"hasMore,omitempty"` Total int `json:"total,omitempty"` @@ -221,6 +227,8 @@ type CompleteResult struct { Completion CompletionResultDetails `json:"completion"` } +func (*CompleteResult) isResult() {} + type CreateMessageParams struct { // This property is reserved by the protocol to allow clients and servers to // attach additional metadata to their responses. @@ -245,6 +253,7 @@ type CreateMessageParams struct { Temperature float64 `json:"temperature,omitempty"` } +func (x *CreateMessageParams) isParams() {} func (x *CreateMessageParams) GetProgressToken() any { return getProgressToken(x) } func (x *CreateMessageParams) SetProgressToken(t any) { setProgressToken(x, t) } @@ -264,6 +273,7 @@ type CreateMessageResult struct { StopReason string `json:"stopReason,omitempty"` } +func (*CreateMessageResult) isResult() {} func (r *CreateMessageResult) UnmarshalJSON(data []byte) error { type result CreateMessageResult // avoid recursion var wire struct { @@ -291,6 +301,7 @@ type GetPromptParams struct { Name string `json:"name"` } +func (x *GetPromptParams) isParams() {} func (x *GetPromptParams) GetProgressToken() any { return getProgressToken(x) } func (x *GetPromptParams) SetProgressToken(t any) { setProgressToken(x, t) } @@ -304,6 +315,8 @@ type GetPromptResult struct { Messages []*PromptMessage `json:"messages"` } +func (*GetPromptResult) isResult() {} + type InitializeParams struct { // This property is reserved by the protocol to allow clients and servers to // attach additional metadata to their responses. @@ -315,6 +328,7 @@ type InitializeParams struct { ProtocolVersion string `json:"protocolVersion"` } +func (x *InitializeParams) isParams() {} func (x *InitializeParams) GetProgressToken() any { return getProgressToken(x) } func (x *InitializeParams) SetProgressToken(t any) { setProgressToken(x, t) } @@ -338,12 +352,15 @@ type InitializeResult struct { ServerInfo *Implementation `json:"serverInfo"` } +func (*InitializeResult) isResult() {} + type InitializedParams struct { // This property is reserved by the protocol to allow clients and servers to // attach additional metadata to their responses. Meta `json:"_meta,omitempty"` } +func (x *InitializedParams) isParams() {} func (x *InitializedParams) GetProgressToken() any { return getProgressToken(x) } func (x *InitializedParams) SetProgressToken(t any) { setProgressToken(x, t) } @@ -356,6 +373,7 @@ type ListPromptsParams struct { Cursor string `json:"cursor,omitempty"` } +func (x *ListPromptsParams) isParams() {} func (x *ListPromptsParams) GetProgressToken() any { return getProgressToken(x) } func (x *ListPromptsParams) SetProgressToken(t any) { setProgressToken(x, t) } func (x *ListPromptsParams) cursorPtr() *string { return &x.Cursor } @@ -371,6 +389,7 @@ type ListPromptsResult struct { Prompts []*Prompt `json:"prompts"` } +func (x *ListPromptsResult) isResult() {} func (x *ListPromptsResult) nextCursorPtr() *string { return &x.NextCursor } type ListResourceTemplatesParams struct { @@ -382,6 +401,7 @@ type ListResourceTemplatesParams struct { Cursor string `json:"cursor,omitempty"` } +func (x *ListResourceTemplatesParams) isParams() {} func (x *ListResourceTemplatesParams) GetProgressToken() any { return getProgressToken(x) } func (x *ListResourceTemplatesParams) SetProgressToken(t any) { setProgressToken(x, t) } func (x *ListResourceTemplatesParams) cursorPtr() *string { return &x.Cursor } @@ -397,6 +417,7 @@ type ListResourceTemplatesResult struct { ResourceTemplates []*ResourceTemplate `json:"resourceTemplates"` } +func (x *ListResourceTemplatesResult) isResult() {} func (x *ListResourceTemplatesResult) nextCursorPtr() *string { return &x.NextCursor } type ListResourcesParams struct { @@ -408,6 +429,7 @@ type ListResourcesParams struct { Cursor string `json:"cursor,omitempty"` } +func (x *ListResourcesParams) isParams() {} func (x *ListResourcesParams) GetProgressToken() any { return getProgressToken(x) } func (x *ListResourcesParams) SetProgressToken(t any) { setProgressToken(x, t) } func (x *ListResourcesParams) cursorPtr() *string { return &x.Cursor } @@ -423,6 +445,7 @@ type ListResourcesResult struct { Resources []*Resource `json:"resources"` } +func (x *ListResourcesResult) isResult() {} func (x *ListResourcesResult) nextCursorPtr() *string { return &x.NextCursor } type ListRootsParams struct { @@ -431,6 +454,7 @@ type ListRootsParams struct { Meta `json:"_meta,omitempty"` } +func (x *ListRootsParams) isParams() {} func (x *ListRootsParams) GetProgressToken() any { return getProgressToken(x) } func (x *ListRootsParams) SetProgressToken(t any) { setProgressToken(x, t) } @@ -444,6 +468,8 @@ type ListRootsResult struct { Roots []*Root `json:"roots"` } +func (*ListRootsResult) isResult() {} + type ListToolsParams struct { // This property is reserved by the protocol to allow clients and servers to // attach additional metadata to their responses. @@ -453,6 +479,7 @@ type ListToolsParams struct { Cursor string `json:"cursor,omitempty"` } +func (x *ListToolsParams) isParams() {} func (x *ListToolsParams) GetProgressToken() any { return getProgressToken(x) } func (x *ListToolsParams) SetProgressToken(t any) { setProgressToken(x, t) } func (x *ListToolsParams) cursorPtr() *string { return &x.Cursor } @@ -468,6 +495,7 @@ type ListToolsResult struct { Tools []*Tool `json:"tools"` } +func (x *ListToolsResult) isResult() {} func (x *ListToolsResult) nextCursorPtr() *string { return &x.NextCursor } // The severity of a log message. @@ -489,6 +517,7 @@ type LoggingMessageParams struct { Logger string `json:"logger,omitempty"` } +func (x *LoggingMessageParams) isParams() {} func (x *LoggingMessageParams) GetProgressToken() any { return getProgressToken(x) } func (x *LoggingMessageParams) SetProgressToken(t any) { setProgressToken(x, t) } @@ -550,6 +579,7 @@ type PingParams struct { Meta `json:"_meta,omitempty"` } +func (x *PingParams) isParams() {} func (x *PingParams) GetProgressToken() any { return getProgressToken(x) } func (x *PingParams) SetProgressToken(t any) { setProgressToken(x, t) } @@ -569,6 +599,8 @@ type ProgressNotificationParams struct { Total float64 `json:"total,omitempty"` } +func (*ProgressNotificationParams) isParams() {} + // A prompt or prompt template that the server offers. type Prompt struct { // See [specification/2025-06-18/basic/index#general-fields] for notes on _meta @@ -606,6 +638,7 @@ type PromptListChangedParams struct { Meta `json:"_meta,omitempty"` } +func (x *PromptListChangedParams) isParams() {} func (x *PromptListChangedParams) GetProgressToken() any { return getProgressToken(x) } func (x *PromptListChangedParams) SetProgressToken(t any) { setProgressToken(x, t) } @@ -646,6 +679,7 @@ type ReadResourceParams struct { URI string `json:"uri"` } +func (x *ReadResourceParams) isParams() {} func (x *ReadResourceParams) GetProgressToken() any { return getProgressToken(x) } func (x *ReadResourceParams) SetProgressToken(t any) { setProgressToken(x, t) } @@ -657,6 +691,8 @@ type ReadResourceResult struct { Contents []*ResourceContents `json:"contents"` } +func (*ReadResourceResult) isResult() {} + // A known resource that the server is capable of reading. type Resource struct { // See [specification/2025-06-18/basic/index#general-fields] for notes on _meta @@ -697,6 +733,7 @@ type ResourceListChangedParams struct { Meta `json:"_meta,omitempty"` } +func (x *ResourceListChangedParams) isParams() {} func (x *ResourceListChangedParams) GetProgressToken() any { return getProgressToken(x) } func (x *ResourceListChangedParams) SetProgressToken(t any) { setProgressToken(x, t) } @@ -754,6 +791,7 @@ type RootsListChangedParams struct { Meta `json:"_meta,omitempty"` } +func (x *RootsListChangedParams) isParams() {} func (x *RootsListChangedParams) GetProgressToken() any { return getProgressToken(x) } func (x *RootsListChangedParams) SetProgressToken(t any) { setProgressToken(x, t) } @@ -798,6 +836,7 @@ type SetLevelParams struct { Level LoggingLevel `json:"level"` } +func (x *SetLevelParams) isParams() {} func (x *SetLevelParams) GetProgressToken() any { return getProgressToken(x) } func (x *SetLevelParams) SetProgressToken(t any) { setProgressToken(x, t) } @@ -873,6 +912,7 @@ type ToolListChangedParams struct { Meta `json:"_meta,omitempty"` } +func (x *ToolListChangedParams) isParams() {} func (x *ToolListChangedParams) GetProgressToken() any { return getProgressToken(x) } func (x *ToolListChangedParams) SetProgressToken(t any) { setProgressToken(x, t) } @@ -886,6 +926,8 @@ type SubscribeParams struct { URI string `json:"uri"` } +func (*SubscribeParams) isParams() {} + // Sent from the client to request cancellation of resources/updated // notifications from the server. This should follow a previous // resources/subscribe request. @@ -897,6 +939,8 @@ type UnsubscribeParams struct { URI string `json:"uri"` } +func (*UnsubscribeParams) isParams() {} + // A notification from the server to the client, informing it that a resource // has changed and may need to be read again. This should only be sent if the // client previously sent a resources/subscribe request. @@ -908,6 +952,8 @@ type ResourceUpdatedNotificationParams struct { URI string `json:"uri"` } +func (*ResourceUpdatedNotificationParams) isParams() {} + // TODO(jba): add CompleteRequest and related types. // TODO(jba): add ElicitRequest and related types. diff --git a/mcp/server.go b/mcp/server.go index c8878da3..68d8b3f9 100644 --- a/mcp/server.go +++ b/mcp/server.go @@ -10,6 +10,7 @@ import ( "encoding/base64" "encoding/gob" "encoding/json" + "errors" "fmt" "iter" "maps" @@ -514,7 +515,8 @@ func (s *Server) unsubscribe(ctx context.Context, ss *ServerSession, params *Uns // If no tools have been added, the server will not have the tool capability. // The same goes for other features like prompts and resources. func (s *Server) Run(ctx context.Context, t Transport) error { - ss, err := s.Connect(ctx, t) + // TODO: provide a way to pass ServerSessionOptions? + ss, err := s.Connect(ctx, t, nil) if err != nil { return err } @@ -557,21 +559,72 @@ func (s *Server) disconnect(cc *ServerSession) { } } +type SessionOptions struct { + // SessionID is the session's unique ID. + SessionID string + // SessionState is the current state of the session. The default is the initial + // state. + SessionState *SessionState + // SessionStore stores SessionStates. By default it is a MemorySessionStore. + SessionStore SessionStore +} + // Connect connects the MCP server over the given transport and starts handling // messages. +// It returns a [ServerSession] for interacting with a [Client]. // -// It returns a connection object that may be used to terminate the connection -// (with [Connection.Close]), or await client termination (with -// [Connection.Wait]). -func (s *Server) Connect(ctx context.Context, t Transport) (*ServerSession, error) { - return connect(ctx, t, s) +// [SessionOptions.SessionStore] should be nil only for single-session transports, +// like [StdioTransport]. Multi-session transports, like [StreamableServerTransport], +// must provide a [SessionStore]. +func (s *Server) Connect(ctx context.Context, t Transport, opts *SessionOptions) (*ServerSession, error) { + if opts != nil && opts.SessionState == nil && opts.SessionStore != nil { + return nil, errors.New("ServerSessionOptions has store but no state") + } + ss, err := connect(ctx, t, s) + if err != nil { + return nil, err + } + var state *SessionState + ss.mu.Lock() + if opts != nil { + ss.sessionID = opts.SessionID + ss.store = opts.SessionStore + state = opts.SessionState + } + if ss.store == nil { + ss.store = NewMemorySessionStore() + } + ss.mu.Unlock() + if state == nil { + state = &SessionState{} + } + // TODO(jba): This store is redundant with the one in StreamableHTTPHandler.ServeHTTP; dedup. + if err := ss.storeState(ctx, state); err != nil { + return nil, err + } + return ss, nil } -func (s *Server) callInitializedHandler(ctx context.Context, ss *ServerSession, params *InitializedParams) (Result, error) { - if s.opts.KeepAlive > 0 { - ss.startKeepalive(s.opts.KeepAlive) +func (ss *ServerSession) initialized(ctx context.Context, params *InitializedParams) (Result, error) { + if ss.server.opts.KeepAlive > 0 { + ss.startKeepalive(ss.server.opts.KeepAlive) } - return callNotificationHandler(ctx, s.opts.InitializedHandler, ss, params) + // TODO(jba): optimistic locking + state, err := ss.loadState(ctx) + if err != nil { + return nil, err + } + if state.InitializeParams == nil { + return nil, fmt.Errorf("%q before %q", notificationInitialized, methodInitialize) + } + if state.Initialized { + return nil, fmt.Errorf("duplicate %q received", notificationInitialized) + } + state.Initialized = true + if err := ss.storeState(ctx, state); err != nil { + return nil, err + } + return callNotificationHandler(ctx, ss.server.opts.InitializedHandler, ss, params) } func (s *Server) callRootsListChangedHandler(ctx context.Context, ss *ServerSession, params *RootsListChangedParams) (Result, error) { @@ -597,25 +650,32 @@ func (ss *ServerSession) NotifyProgress(ctx context.Context, params *ProgressNot // Call [ServerSession.Close] to close the connection, or await client // termination with [ServerSession.Wait]. type ServerSession struct { - server *Server - conn *jsonrpc2.Connection - mcpConn Connection - mu sync.Mutex - logLevel LoggingLevel - initializeParams *InitializeParams - initialized bool - keepaliveCancel context.CancelFunc + server *Server + conn *jsonrpc2.Connection + mu sync.Mutex + logLevel LoggingLevel + keepaliveCancel context.CancelFunc + sessionID string + store SessionStore } func (ss *ServerSession) setConn(c Connection) { - ss.mcpConn = c } func (ss *ServerSession) ID() string { - if ss.mcpConn == nil { - return "" - } - return ss.mcpConn.SessionID() + return ss.sessionID +} + +func (ss *ServerSession) loadState(ctx context.Context) (*SessionState, error) { + ss.mu.Lock() + defer ss.mu.Unlock() + return ss.store.Load(ctx, ss.sessionID) +} + +func (ss *ServerSession) storeState(ctx context.Context, state *SessionState) error { + ss.mu.Lock() + defer ss.mu.Unlock() + return ss.store.Store(ctx, ss.sessionID, state) } // Ping pings the client. @@ -638,16 +698,19 @@ func (ss *ServerSession) CreateMessage(ctx context.Context, params *CreateMessag // The message is not sent if the client has not called SetLevel, or if its level // is below that of the last SetLevel. func (ss *ServerSession) Log(ctx context.Context, params *LoggingMessageParams) error { - ss.mu.Lock() - logLevel := ss.logLevel - ss.mu.Unlock() - if logLevel == "" { + // TODO: Loading the state on every log message can be expensive. Consider caching it briefly, perhaps for the + // duration of a request. + state, err := ss.loadState(ctx) + if err != nil { + return err + } + if state.LogLevel == "" { // The spec is unclear, but seems to imply that no log messages are sent until the client // sets the level. // TODO(jba): read other SDKs, possibly file an issue. return nil } - if compareLevels(params.Level, logLevel) < 0 { + if compareLevels(params.Level, state.LogLevel) < 0 { return nil } return handleNotify(ctx, ss, notificationLoggingMessage, params) @@ -702,7 +765,7 @@ var serverMethodInfos = map[string]methodInfo{ methodSetLevel: newMethodInfo(sessionMethod((*ServerSession).setLevel), 0), methodSubscribe: newMethodInfo(serverMethod((*Server).subscribe), 0), methodUnsubscribe: newMethodInfo(serverMethod((*Server).unsubscribe), 0), - notificationInitialized: newMethodInfo(serverMethod((*Server).callInitializedHandler), notification|missingParamsOK), + notificationInitialized: newMethodInfo(sessionMethod((*ServerSession).initialized), notification|missingParamsOK), notificationRootsListChanged: newMethodInfo(serverMethod((*Server).callRootsListChangedHandler), notification|missingParamsOK), notificationProgress: newMethodInfo(sessionMethod((*ServerSession).callProgressNotificationHandler), notification), } @@ -728,16 +791,17 @@ func (ss *ServerSession) getConn() *jsonrpc2.Connection { return ss.conn } // handle invokes the method described by the given JSON RPC request. func (ss *ServerSession) handle(ctx context.Context, req *jsonrpc.Request) (any, error) { - ss.mu.Lock() - initialized := ss.initialized - ss.mu.Unlock() + state, err := ss.loadState(ctx) + if err != nil { + return nil, err + } // From the spec: // "The client SHOULD NOT send requests other than pings before the server // has responded to the initialize request." switch req.Method { - case "initialize", "ping": + case methodInitialize, methodPing, notificationInitialized: default: - if !initialized { + if !state.Initialized { return nil, fmt.Errorf("method %q is invalid during session initialization", req.Method) } } @@ -745,6 +809,7 @@ func (ss *ServerSession) handle(ctx context.Context, req *jsonrpc.Request) (any, // server->client calls and notifications to the incoming request from which // they originated. See [idContextKey] for details. ctx = context.WithValue(ctx, idContextKey{}, req.ID) + // TODO(jba): pass the state down so that it doesn't get loaded again. return handleReceive(ctx, ss, req) } @@ -752,20 +817,19 @@ func (ss *ServerSession) initialize(ctx context.Context, params *InitializeParam if params == nil { return nil, fmt.Errorf("%w: \"params\" must be be provided", jsonrpc2.ErrInvalidParams) } - ss.mu.Lock() - ss.initializeParams = params - ss.mu.Unlock() - // Mark the connection as initialized when this method exits. - // TODO: Technically, the server should not be considered initialized until it has - // *responded*, but we don't have adequate visibility into the jsonrpc2 - // connection to implement that easily. In any case, once we've initialized - // here, we can handle requests. - defer func() { - ss.mu.Lock() - ss.initialized = true - ss.mu.Unlock() - }() + // TODO(jba): optimistic locking + state, err := ss.loadState(ctx) + if err != nil { + return nil, err + } + if state.InitializeParams != nil { + return nil, fmt.Errorf("session %s already initialized", ss.sessionID) + } + state.InitializeParams = params + if err := ss.storeState(ctx, state); err != nil { + return nil, fmt.Errorf("storing session state: %w", err) + } // If we support the client's version, reply with it. Otherwise, reply with our // latest version. @@ -788,10 +852,15 @@ func (ss *ServerSession) ping(context.Context, *PingParams) (*emptyResult, error return &emptyResult{}, nil } -func (ss *ServerSession) setLevel(_ context.Context, params *SetLevelParams) (*emptyResult, error) { - ss.mu.Lock() - defer ss.mu.Unlock() - ss.logLevel = params.Level +func (ss *ServerSession) setLevel(ctx context.Context, params *SetLevelParams) (*emptyResult, error) { + state, err := ss.loadState(ctx) + if err != nil { + return nil, err + } + state.LogLevel = params.Level + if err := ss.storeState(ctx, state); err != nil { + return nil, err + } return &emptyResult{}, nil } diff --git a/mcp/server_example_test.go b/mcp/server_example_test.go index 3ab7a2a4..33bde4b9 100644 --- a/mcp/server_example_test.go +++ b/mcp/server_example_test.go @@ -31,7 +31,7 @@ func ExampleServer() { server := mcp.NewServer(&mcp.Implementation{Name: "greeter", Version: "v0.0.1"}, nil) mcp.AddTool(server, &mcp.Tool{Name: "greet", Description: "say hi"}, SayHi) - serverSession, err := server.Connect(ctx, serverTransport) + serverSession, err := server.Connect(ctx, serverTransport, nil) if err != nil { log.Fatal(err) } @@ -62,7 +62,7 @@ func createSessions(ctx context.Context) (*mcp.ClientSession, *mcp.ServerSession server := mcp.NewServer(testImpl, nil) client := mcp.NewClient(testImpl, nil) serverTransport, clientTransport := mcp.NewInMemoryTransports() - serverSession, err := server.Connect(ctx, serverTransport) + serverSession, err := server.Connect(ctx, serverTransport, nil) if err != nil { log.Fatal(err) } diff --git a/mcp/server_test.go b/mcp/server_test.go index 5a161b72..6415decc 100644 --- a/mcp/server_test.go +++ b/mcp/server_test.go @@ -11,7 +11,7 @@ import ( "testing" "github.com/google/go-cmp/cmp" - "github.com/modelcontextprotocol/go-sdk/jsonschema" + "github.com/google/jsonschema-go/jsonschema" ) type testItem struct { diff --git a/mcp/session.go b/mcp/session.go new file mode 100644 index 00000000..255a3619 --- /dev/null +++ b/mcp/session.go @@ -0,0 +1,81 @@ +// 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 mcp + +import ( + "context" + "errors" + "fmt" + "sync" +) + +// SessionState is the state of a session. +type SessionState struct { + // InitializeParams are the parameters from the initialize request. + InitializeParams *InitializeParams `json:"initializeParams"` + // Initialized reports whether the session received an "initialized" notification + // from the client. + Initialized bool + + // LogLevel is the logging level for the session. + LogLevel LoggingLevel `json:"logLevel"` + + // TODO: resource subscriptions +} + +// ErrNotSession indicates that a session is not in a SessionStore. +var ErrNoSession = errors.New("no such session") + +// SessionStore is an interface for storing and retrieving session state. +type SessionStore interface { + // Load retrieves the session state for the given session ID. + // If there is none, it returns nil and an error wrapping ErrNoSession. + Load(ctx context.Context, sessionID string) (*SessionState, error) + // Store saves the session state for the given session ID. + Store(ctx context.Context, sessionID string, state *SessionState) error + // Delete removes the session state for the given session ID. + Delete(ctx context.Context, sessionID string) error +} + +// MemorySessionStore is an in-memory implementation of SessionStore. +// It is safe for concurrent use. +type MemorySessionStore struct { + mu sync.Mutex + store map[string]*SessionState +} + +// NewMemorySessionStore creates a new MemorySessionStore. +func NewMemorySessionStore() *MemorySessionStore { + return &MemorySessionStore{ + store: make(map[string]*SessionState), + } +} + +// Load retrieves the session state for the given session ID. +func (s *MemorySessionStore) Load(ctx context.Context, sessionID string) (*SessionState, error) { + s.mu.Lock() + defer s.mu.Unlock() + state, ok := s.store[sessionID] + if !ok { + return nil, fmt.Errorf("session ID %q: %w", sessionID, ErrNoSession) + } + return state, nil +} + +// Store saves the session state for the given session ID. +func (s *MemorySessionStore) Store(ctx context.Context, sessionID string, state *SessionState) error { + s.mu.Lock() + defer s.mu.Unlock() + s.store[sessionID] = state + return nil +} + +// Delete removes the session state for the given session ID. +func (s *MemorySessionStore) Delete(ctx context.Context, sessionID string) error { + s.mu.Lock() + defer s.mu.Unlock() + delete(s.store, sessionID) + return nil +} diff --git a/mcp/session_test.go b/mcp/session_test.go new file mode 100644 index 00000000..6c8f2fe1 --- /dev/null +++ b/mcp/session_test.go @@ -0,0 +1,48 @@ +// 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 mcp + +import ( + "context" + "errors" + "testing" +) + +func TestMemorySessionStore(t *testing.T) { + ctx := context.Background() + store := NewMemorySessionStore() + + sessionID := "test-session" + state := &SessionState{LogLevel: "debug"} + + // Test Store and Load + if err := store.Store(ctx, sessionID, state); err != nil { + t.Fatalf("Store() error = %v", err) + } + + loadedState, err := store.Load(ctx, sessionID) + if err != nil { + t.Fatalf("Load() error = %v", err) + } + if loadedState == nil { + t.Fatal("Load() returned nil state") + } + if loadedState.LogLevel != state.LogLevel { + t.Errorf("Load() LogLevel = %v, want %v", loadedState.LogLevel, state.LogLevel) + } + + // Test Delete + if err := store.Delete(ctx, sessionID); err != nil { + t.Fatalf("Delete() error = %v", err) + } + + deletedState, err := store.Load(ctx, sessionID) + if !errors.Is(err, ErrNoSession) { + t.Fatalf("Load() after Delete(): got %v, want ErrNoSession", err) + } + if deletedState != nil { + t.Error("Load() after Delete() returned non-nil state") + } +} diff --git a/mcp/shared.go b/mcp/shared.go index 319071f2..e3688641 100644 --- a/mcp/shared.go +++ b/mcp/shared.go @@ -275,6 +275,8 @@ func sessionMethod[S Session, P Params, R Result](f func(S, context.Context, P) // Error codes const ( + // TODO: should these be unexported? + CodeResourceNotFound = -32002 // The error code if the method exists and was called properly, but the peer does not support it. CodeUnsupportedMethod = -31001 @@ -335,6 +337,9 @@ func setProgressToken(p Params, pt any) { // Params is a parameter (input) type for an MCP call or notification. type Params interface { + // isParams discourages implementation of Params outside of this package. + isParams() + // GetMeta returns metadata from a value. GetMeta() map[string]any // SetMeta sets the metadata on a value. @@ -356,6 +361,9 @@ type RequestParams interface { // Result is a result of an MCP call. type Result interface { + // isResult discourages implementation of Result outside of this package. + isResult() + // GetMeta returns metadata from a value. GetMeta() map[string]any // SetMeta sets the metadata on a value. @@ -366,6 +374,7 @@ type Result interface { // Those methods cannot return nil, because jsonrpc2 cannot handle nils. type emptyResult struct{} +func (*emptyResult) isResult() {} func (*emptyResult) GetMeta() map[string]any { panic("should never be called") } func (*emptyResult) SetMeta(map[string]any) { panic("should never be called") } diff --git a/mcp/shared_test.go b/mcp/shared_test.go index f319d80e..0aea1947 100644 --- a/mcp/shared_test.go +++ b/mcp/shared_test.go @@ -7,6 +7,7 @@ package mcp import ( "context" "encoding/json" + "fmt" "strings" "testing" ) @@ -88,3 +89,146 @@ func TestToolValidate(t *testing.T) { }) } } + +// TestNilParamsHandling tests that nil parameters don't cause panic in unmarshalParams. +// This addresses a vulnerability where missing or null parameters could crash the server. +func TestNilParamsHandling(t *testing.T) { + // Define test types for clarity + type TestArgs struct { + Name string `json:"name"` + Value int `json:"value"` + } + type TestParams = *CallToolParamsFor[TestArgs] + type TestResult = *CallToolResultFor[string] + + // Simple test handler + testHandler := func(ctx context.Context, ss *ServerSession, params TestParams) (TestResult, error) { + result := "processed: " + params.Arguments.Name + return &CallToolResultFor[string]{StructuredContent: result}, nil + } + + methodInfo := newMethodInfo(testHandler, missingParamsOK) + + // Helper function to test that unmarshalParams doesn't panic and handles nil gracefully + mustNotPanic := func(t *testing.T, rawMsg json.RawMessage, expectNil bool) Params { + t.Helper() + + defer func() { + if r := recover(); r != nil { + t.Fatalf("unmarshalParams panicked: %v", r) + } + }() + + params, err := methodInfo.unmarshalParams(rawMsg) + if err != nil { + t.Fatalf("unmarshalParams failed: %v", err) + } + + if expectNil { + if params != nil { + t.Fatalf("Expected nil params, got %v", params) + } + return params + } + + if params == nil { + t.Fatal("unmarshalParams returned unexpected nil") + } + + // Verify the result can be used safely + typedParams := params.(TestParams) + _ = typedParams.Name + _ = typedParams.Arguments.Name + _ = typedParams.Arguments.Value + + return params + } + + // Test different nil parameter scenarios - with missingParamsOK flag, nil/null should return nil + t.Run("missing_params", func(t *testing.T) { + mustNotPanic(t, nil, true) // Expect nil with missingParamsOK flag + }) + + t.Run("explicit_null", func(t *testing.T) { + mustNotPanic(t, json.RawMessage(`null`), true) // Expect nil with missingParamsOK flag + }) + + t.Run("empty_object", func(t *testing.T) { + mustNotPanic(t, json.RawMessage(`{}`), false) // Empty object should create valid params + }) + + t.Run("valid_params", func(t *testing.T) { + rawMsg := json.RawMessage(`{"name":"test","arguments":{"name":"hello","value":42}}`) + params := mustNotPanic(t, rawMsg, false) + + // For valid params, also verify the values are parsed correctly + typedParams := params.(TestParams) + if typedParams.Name != "test" { + t.Errorf("Expected name 'test', got %q", typedParams.Name) + } + if typedParams.Arguments.Name != "hello" { + t.Errorf("Expected argument name 'hello', got %q", typedParams.Arguments.Name) + } + if typedParams.Arguments.Value != 42 { + t.Errorf("Expected argument value 42, got %d", typedParams.Arguments.Value) + } + }) +} + +// TestNilParamsEdgeCases tests edge cases to ensure we don't over-fix +func TestNilParamsEdgeCases(t *testing.T) { + type TestArgs struct { + Name string `json:"name"` + Value int `json:"value"` + } + type TestParams = *CallToolParamsFor[TestArgs] + + testHandler := func(ctx context.Context, ss *ServerSession, params TestParams) (*CallToolResultFor[string], error) { + return &CallToolResultFor[string]{StructuredContent: "test"}, nil + } + + methodInfo := newMethodInfo(testHandler, missingParamsOK) + + // These should fail normally, not be treated as nil params + invalidCases := []json.RawMessage{ + json.RawMessage(""), // empty string - should error + json.RawMessage("[]"), // array - should error + json.RawMessage(`"null"`), // string "null" - should error + json.RawMessage("0"), // number - should error + json.RawMessage("false"), // boolean - should error + } + + for i, rawMsg := range invalidCases { + t.Run(fmt.Sprintf("invalid_case_%d", i), func(t *testing.T) { + params, err := methodInfo.unmarshalParams(rawMsg) + if err == nil && params == nil { + t.Error("Should not return nil params without error") + } + }) + } + + // Test that methods without missingParamsOK flag properly reject nil params + t.Run("reject_when_params_required", func(t *testing.T) { + methodInfoStrict := newMethodInfo(testHandler, 0) // No missingParamsOK flag + + testCases := []struct { + name string + params json.RawMessage + }{ + {"nil_params", nil}, + {"null_params", json.RawMessage(`null`)}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + _, err := methodInfoStrict.unmarshalParams(tc.params) + if err == nil { + t.Error("Expected error for required params, got nil") + } + if !strings.Contains(err.Error(), "missing required \"params\"") { + t.Errorf("Expected 'missing required params' error, got: %v", err) + } + }) + } + }) +} diff --git a/mcp/sse.go b/mcp/sse.go index cf44276b..f74a3fb6 100644 --- a/mcp/sse.go +++ b/mcp/sse.go @@ -155,7 +155,7 @@ func (t *SSEServerTransport) Connect(context.Context) (Connection, error) { if err != nil { return nil, err } - return sseServerConn{t}, nil + return &sseServerConn{t: t}, nil } func (h *SSEHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { @@ -221,7 +221,7 @@ func (h *SSEHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { http.Error(w, "no server available", http.StatusBadRequest) return } - ss, err := server.Connect(req.Context(), transport) + ss, err := server.Connect(req.Context(), transport, nil) if err != nil { http.Error(w, "connection failed", http.StatusInternalServerError) return @@ -244,10 +244,10 @@ type sseServerConn struct { } // TODO(jba): get the session ID. (Not urgent because SSE transports have been removed from the spec.) -func (s sseServerConn) SessionID() string { return "" } +func (s *sseServerConn) SessionID() string { return "" } // Read implements jsonrpc2.Reader. -func (s sseServerConn) Read(ctx context.Context) (jsonrpc.Message, error) { +func (s *sseServerConn) Read(ctx context.Context) (jsonrpc.Message, error) { select { case <-ctx.Done(): return nil, ctx.Err() @@ -259,7 +259,7 @@ func (s sseServerConn) Read(ctx context.Context) (jsonrpc.Message, error) { } // Write implements jsonrpc2.Writer. -func (s sseServerConn) Write(ctx context.Context, msg jsonrpc.Message) error { +func (s *sseServerConn) Write(ctx context.Context, msg jsonrpc.Message) error { if ctx.Err() != nil { return ctx.Err() } @@ -288,7 +288,7 @@ func (s sseServerConn) Write(ctx context.Context, msg jsonrpc.Message) error { // It must be safe to call Close more than once, as the close may // asynchronously be initiated by either the server closing its connection, or // by the hanging GET exiting. -func (s sseServerConn) Close() error { +func (s *sseServerConn) Close() error { s.t.mu.Lock() defer s.t.mu.Unlock() if !s.t.closed { diff --git a/mcp/streamable.go b/mcp/streamable.go index 3f53a689..e5e51ac1 100644 --- a/mcp/streamable.go +++ b/mcp/streamable.go @@ -7,9 +7,12 @@ package mcp import ( "bytes" "context" + "encoding/json" "errors" "fmt" "io" + "io/fs" + "iter" "math" "math/rand/v2" "net/http" @@ -33,10 +36,11 @@ const ( // // [MCP spec]: https://modelcontextprotocol.io/2025/03/26/streamable-http-transport.html type StreamableHTTPHandler struct { + opts StreamableHTTPOptions getServer func(*http.Request) *Server - sessionsMu sync.Mutex - sessions map[string]*StreamableServerTransport // keyed by IDs (from Mcp-Session-Id header) + transportMu sync.Mutex + transports map[string]*StreamableServerTransport // keyed by IDs (from Mcp-Session-Id header) } // StreamableHTTPOptions is a placeholder options struct for future @@ -44,6 +48,14 @@ type StreamableHTTPHandler struct { type StreamableHTTPOptions struct { // TODO: support configurable session ID generation (?) // TODO: support session retention (?) + + // transportOptions sets the streamable server transport options to use when + // establishing a new session. + transportOptions *StreamableServerTransportOptions + + // SessionStore is the store for persistent sessions. + // If nil, sessions will be stored in memory. + SessionStore SessionStore } // NewStreamableHTTPHandler returns a new [StreamableHTTPHandler]. @@ -52,10 +64,17 @@ type StreamableHTTPOptions struct { // sessions. It is OK for getServer to return the same server multiple times. // If getServer returns nil, a 400 Bad Request will be served. func NewStreamableHTTPHandler(getServer func(*http.Request) *Server, opts *StreamableHTTPOptions) *StreamableHTTPHandler { - return &StreamableHTTPHandler{ - getServer: getServer, - sessions: make(map[string]*StreamableServerTransport), + h := &StreamableHTTPHandler{ + getServer: getServer, + transports: make(map[string]*StreamableServerTransport), + } + if opts != nil { + h.opts = *opts + } + if h.opts.SessionStore == nil { + h.opts.SessionStore = NewMemorySessionStore() } + return h } // closeAll closes all ongoing sessions. @@ -66,12 +85,12 @@ func NewStreamableHTTPHandler(getServer func(*http.Request) *Server, opts *Strea // Should we allow passing in a session store? That would allow the handler to // be stateless. func (h *StreamableHTTPHandler) closeAll() { - h.sessionsMu.Lock() - defer h.sessionsMu.Unlock() - for _, s := range h.sessions { - s.Close() + h.transportMu.Lock() + defer h.transportMu.Unlock() + for _, t := range h.transports { + t.Close() } - h.sessions = nil + h.transports = nil } func (h *StreamableHTTPHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { @@ -98,29 +117,26 @@ func (h *StreamableHTTPHandler) ServeHTTP(w http.ResponseWriter, req *http.Reque return } - var session *StreamableServerTransport - if id := req.Header.Get(sessionIDHeader); id != "" { - h.sessionsMu.Lock() - session, _ = h.sessions[id] - h.sessionsMu.Unlock() - if session == nil { - http.Error(w, "session not found", http.StatusNotFound) - return - } + var transport *StreamableServerTransport + sessionID := req.Header.Get(sessionIDHeader) + if sessionID != "" { + h.transportMu.Lock() + transport, _ = h.transports[sessionID] + h.transportMu.Unlock() } // TODO(rfindley): simplify the locking so that each request has only one // critical section. if req.Method == http.MethodDelete { - if session == nil { + if transport == nil { // => Mcp-Session-Id was not set; else we'd have returned NotFound above. http.Error(w, "DELETE requires an Mcp-Session-Id header", http.StatusBadRequest) return } - h.sessionsMu.Lock() - delete(h.sessions, session.sessionID) - h.sessionsMu.Unlock() - session.Close() + h.transportMu.Lock() + delete(h.transports, transport.sessionID) + h.transportMu.Unlock() + transport.Close() w.WriteHeader(http.StatusNoContent) return } @@ -133,34 +149,71 @@ func (h *StreamableHTTPHandler) ServeHTTP(w http.ResponseWriter, req *http.Reque return } - if session == nil { - s := NewStreamableServerTransport(randText(), nil) + if transport == nil { + var state *SessionState + var err error + if sessionID != "" { + // The session might be in the store. + state, err = h.opts.SessionStore.Load(req.Context(), sessionID) + if errors.Is(err, fs.ErrNotExist) { + http.Error(w, fmt.Sprintf("no session with ID %s", sessionID), http.StatusNotFound) + return + } else if err != nil { + http.Error(w, fmt.Sprintf("SessionStore.Load(%q): %v", sessionID, err), http.StatusInternalServerError) + return + } + } else { + // New session: store an empty state. + state = &SessionState{} + sessionID = newSessionID() + if err := h.opts.SessionStore.Store(req.Context(), sessionID, state); err != nil { + http.Error(w, fmt.Sprintf("SessionStore.Store, new session: %v", err), http.StatusInternalServerError) + return + } + } + transport = NewStreamableServerTransport(sessionID, nil) server := h.getServer(req) if server == nil { - // The getServer argument to NewStreamableHTTPHandler returned nil. http.Error(w, "no server available", http.StatusBadRequest) return } // Pass req.Context() here, to allow middleware to add context values. // The context is detached in the jsonrpc2 library when handling the // long-running stream. - if _, err := server.Connect(req.Context(), s); err != nil { - http.Error(w, "failed connection", http.StatusInternalServerError) + // TODO: rename SessionOptions to ConnectOptions? + _, err = server.Connect(req.Context(), transport, &SessionOptions{ + SessionID: sessionID, + SessionState: state, + SessionStore: h.opts.SessionStore, + }) + if err != nil { + http.Error(w, fmt.Sprintf("failed connection: %v", err), http.StatusInternalServerError) return } - h.sessionsMu.Lock() - h.sessions[s.sessionID] = s - h.sessionsMu.Unlock() - session = s + h.transportMu.Lock() + // Check in case another request with the same stored session ID got here first. + if _, ok := h.transports[transport.sessionID]; !ok { + h.transports[transport.sessionID] = transport + } + h.transportMu.Unlock() } - session.ServeHTTP(w, req) + transport.ServeHTTP(w, req) } type StreamableServerTransportOptions struct { // Storage for events, to enable stream resumption. // If nil, a [MemoryEventStore] with the default maximum size will be used. EventStore EventStore + + // jsonResponse, if set, tells the server to prefer to respond to requests + // using application/json responses rather than text/event-stream. + // + // Specifically, responses will be application/json whenever incoming POST + // request contain only a single message. In this case, notifications or + // requests made within the context of a server request will be sent to the + // hanging GET request, if any. + jsonResponse bool } // NewStreamableServerTransport returns a new [StreamableServerTransport] with @@ -182,7 +235,11 @@ func NewStreamableServerTransport(sessionID string, opts *StreamableServerTransp streams: make(map[StreamID]*stream), requestStreams: make(map[jsonrpc.ID]StreamID), } - t.streams[0] = newStream(0) + // Stream 0 corresponds to the hanging 'GET'. + // + // It is always text/event-stream, since it must carry arbitrarily many + // messages. + t.streams[0] = newStream(0, false) if opts != nil { t.opts = *opts } @@ -199,7 +256,7 @@ func (t *StreamableServerTransport) SessionID() string { // A StreamableServerTransport implements the [Transport] interface for a // single session. type StreamableServerTransport struct { - nextStreamID atomic.Int64 // incrementing next stream ID + lastStreamID atomic.Int64 // last stream ID used, atomically incremented sessionID string opts StreamableServerTransportOptions @@ -210,11 +267,12 @@ type StreamableServerTransport struct { // Sessions are closed exactly once. isDone bool - // Sessions can have multiple logical connections, corresponding to HTTP - // requests. Additionally, logical sessions may be resumed by subsequent HTTP - // requests, when the session is terminated unexpectedly. + // Sessions can have multiple logical connections (which we call streams), + // corresponding to HTTP requests. Additionally, streams may be resumed by + // subsequent HTTP requests, when the HTTP connection is terminated + // unexpectedly. // - // Therefore, we use a logical connection ID to key the connection state, and + // Therefore, we use a logical stream ID to key the stream state, and // perform the accounting described below when incoming HTTP requests are // handled. @@ -227,10 +285,9 @@ type StreamableServerTransport struct { // requestStreams maps incoming requests to their logical stream ID. // - // Lifecycle: requestStreams persists for the duration of the session. + // Lifecycle: requestStreams persist for the duration of the session. // - // TODO(rfindley): clean up once requests are handled. See the TODO for streams - // above. + // TODO: clean up once requests are handled. See the TODO for streams above. requestStreams map[jsonrpc.ID]StreamID } @@ -245,6 +302,12 @@ type stream struct { // ID 0 is used for messages that don't correlate with an incoming request. id StreamID + // jsonResponse records whether this stream should respond with application/json + // instead of text/event-stream. + // + // See [StreamableServerTransportOptions.jsonResponse]. + jsonResponse bool + // signal is a 1-buffered channel, owned by an incoming HTTP request, that signals // that there are messages available to write into the HTTP response. // In addition, the presence of a channel guarantees that at most one HTTP response @@ -267,16 +330,15 @@ type stream struct { // streamRequests is the set of unanswered incoming RPCs for the stream. // - // Lifecycle: requests values persist until the requests have been - // replied to by the server. Notably, NOT until they are sent to an HTTP - // response, as delivery is not guaranteed. + // Requests persist until their response data has been added to outgoing. requests map[jsonrpc.ID]struct{} } -func newStream(id StreamID) *stream { +func newStream(id StreamID, jsonResponse bool) *stream { return &stream{ - id: id, - requests: make(map[jsonrpc.ID]struct{}), + id: id, + jsonResponse: jsonResponse, + requests: make(map[jsonrpc.ID]struct{}), } } @@ -321,25 +383,23 @@ type idContextKey struct{} // ServeHTTP handles a single HTTP request for the session. func (t *StreamableServerTransport) ServeHTTP(w http.ResponseWriter, req *http.Request) { - status := 0 - message := "" switch req.Method { case http.MethodGet: - status, message = t.serveGET(w, req) + t.serveGET(w, req) case http.MethodPost: - status, message = t.servePOST(w, req) + t.servePOST(w, req) default: // Should not be reached, as this is checked in StreamableHTTPHandler.ServeHTTP. w.Header().Set("Allow", "GET, POST") - status = http.StatusMethodNotAllowed - message = "unsupported method" - } - if status != 0 && status != http.StatusOK { - http.Error(w, message, status) + http.Error(w, "unsupported method", http.StatusMethodNotAllowed) } } -func (t *StreamableServerTransport) serveGET(w http.ResponseWriter, req *http.Request) (int, string) { +// serveGET streams messages to a hanging http GET, with stream ID and last +// message parsed from the Last-Event-ID header. +// +// It returns an HTTP status code and error message. +func (t *StreamableServerTransport) serveGET(w http.ResponseWriter, req *http.Request) { // connID 0 corresponds to the default GET request. id := StreamID(0) // By default, we haven't seen a last index. Since indices start at 0, we represent @@ -351,7 +411,8 @@ func (t *StreamableServerTransport) serveGET(w http.ResponseWriter, req *http.Re var ok bool id, lastIdx, ok = parseEventID(eid) if !ok { - return http.StatusBadRequest, fmt.Sprintf("malformed Last-Event-ID %q", eid) + http.Error(w, fmt.Sprintf("malformed Last-Event-ID %q", eid), http.StatusBadRequest) + return } } @@ -359,31 +420,50 @@ func (t *StreamableServerTransport) serveGET(w http.ResponseWriter, req *http.Re stream, ok := t.streams[id] t.mu.Unlock() if !ok { - return http.StatusBadRequest, "unknown stream" + http.Error(w, "unknown stream", http.StatusBadRequest) + return } if !stream.signal.CompareAndSwap(nil, signalChanPtr()) { // The CAS returned false, meaning that the comparison failed: stream.signal is not nil. - return http.StatusBadRequest, "stream ID conflicts with ongoing stream" + http.Error(w, "stream ID conflicts with ongoing stream", http.StatusConflict) + return } - return t.streamResponse(stream, w, req, lastIdx) + defer stream.signal.Store(nil) + persistent := id == 0 // Only the special stream 0 is a hanging get. + t.respondSSE(stream, w, req, lastIdx, persistent) } -func (t *StreamableServerTransport) servePOST(w http.ResponseWriter, req *http.Request) (int, string) { +// servePOST handles an incoming message, and replies with either an outgoing +// message stream or single response object, depending on whether the +// jsonResponse option is set. +// +// It returns an HTTP status code and error message. +func (t *StreamableServerTransport) servePOST(w http.ResponseWriter, req *http.Request) { if len(req.Header.Values("Last-Event-ID")) > 0 { - return http.StatusBadRequest, "can't send Last-Event-ID for POST request" + http.Error(w, "can't send Last-Event-ID for POST request", http.StatusBadRequest) + return } // Read incoming messages. body, err := io.ReadAll(req.Body) if err != nil { - return http.StatusBadRequest, "failed to read body" + http.Error(w, "failed to read body", http.StatusBadRequest) + return } if len(body) == 0 { - return http.StatusBadRequest, "POST requires a non-empty body" + http.Error(w, "POST requires a non-empty body", http.StatusBadRequest) + return } + // TODO(#21): if the negotiated protocol version is 2025-06-18 or later, + // we should not allow batching here. + // + // This also requires access to the negotiated version, which would either be + // set by the MCP-Protocol-Version header, or would require peeking into the + // session. incoming, _, err := readBatch(body) if err != nil { - return http.StatusBadRequest, fmt.Sprintf("malformed payload: %v", err) + http.Error(w, fmt.Sprintf("malformed payload: %v", err), http.StatusBadRequest) + return } requests := make(map[jsonrpc.ID]struct{}) for _, msg := range incoming { @@ -392,7 +472,8 @@ func (t *StreamableServerTransport) servePOST(w http.ResponseWriter, req *http.R // the HTTP request. If we didn't do this, a request with a bad method or // missing ID could be silently swallowed. if _, err := checkRequest(req, serverMethodInfos); err != nil { - return http.StatusBadRequest, err.Error() + http.Error(w, err.Error(), http.StatusBadRequest) + return } if req.ID.IsValid() { requests[req.ID] = struct{}{} @@ -400,39 +481,84 @@ func (t *StreamableServerTransport) servePOST(w http.ResponseWriter, req *http.R } } - // Update accounting for this request. - stream := newStream(StreamID(t.nextStreamID.Add(1))) - t.mu.Lock() - t.streams[stream.id] = stream + var stream *stream // if non-nil, used to handle requests + + // If we have requests, we need to handle responses along with any + // notifications or server->client requests made in the course of handling. + // Update accounting for this incoming payload. if len(requests) > 0 { - stream.requests = make(map[jsonrpc.ID]struct{}) - } - for reqID := range requests { - t.requestStreams[reqID] = stream.id - stream.requests[reqID] = struct{}{} + stream = newStream(StreamID(t.lastStreamID.Add(1)), t.opts.jsonResponse) + t.mu.Lock() + t.streams[stream.id] = stream + stream.requests = requests + for reqID := range requests { + t.requestStreams[reqID] = stream.id + } + t.mu.Unlock() + stream.signal.Store(signalChanPtr()) } - t.mu.Unlock() - stream.signal.Store(signalChanPtr()) // Publish incoming messages. for _, msg := range incoming { t.incoming <- msg } - // TODO(rfindley): consider optimizing for a single incoming request, by - // responding with application/json when there is only a single message in - // the response. - // (But how would we know there is only a single message? For example, couldn't - // a progress notification be sent before a response on the same context?) - return t.streamResponse(stream, w, req, -1) + if stream == nil { + w.WriteHeader(http.StatusAccepted) + return + } + + if stream.jsonResponse { + t.respondJSON(stream, w, req) + } else { + t.respondSSE(stream, w, req, -1, false) + } } -// lastIndex is the index of the last seen event if resuming, else -1. -func (t *StreamableServerTransport) streamResponse(stream *stream, w http.ResponseWriter, req *http.Request, lastIndex int) (int, string) { - defer stream.signal.Store(nil) +func (t *StreamableServerTransport) respondJSON(stream *stream, w http.ResponseWriter, req *http.Request) { + w.Header().Set("Cache-Control", "no-cache, no-transform") + w.Header().Set("Content-Type", "application/json") + w.Header().Set(sessionIDHeader, t.sessionID) + + var msgs []json.RawMessage + ctx := req.Context() + for msg, ok := range t.messages(ctx, stream, false) { + if !ok { + if ctx.Err() != nil { + w.WriteHeader(http.StatusNoContent) + return + } else { + http.Error(w, http.StatusText(http.StatusGone), http.StatusGone) + return + } + } + msgs = append(msgs, msg) + } + var data []byte + if len(msgs) == 1 { + data = []byte(msgs[0]) + } else { + // TODO: add tests for batch responses, or disallow them entirely. + var err error + data, err = json.Marshal(msgs) + if err != nil { + http.Error(w, fmt.Sprintf("internal error marshalling response: %v", err), http.StatusInternalServerError) + return + } + } + _, _ = w.Write(data) // ignore error: client disconnected +} +// lastIndex is the index of the last seen event if resuming, else -1. +func (t *StreamableServerTransport) respondSSE(stream *stream, w http.ResponseWriter, req *http.Request, lastIndex int, persistent bool) { writes := 0 + // Accept checked in [StreamableHTTPHandler] + w.Header().Set("Cache-Control", "no-cache, no-transform") + w.Header().Set("Content-Type", "text/event-stream") // Accept checked in [StreamableHTTPHandler] + w.Header().Set("Connection", "keep-alive") + w.Header().Set(sessionIDHeader, t.sessionID) + // write one event containing data. write := func(data []byte) bool { lastIndex++ @@ -450,10 +576,13 @@ func (t *StreamableServerTransport) streamResponse(stream *stream, w http.Respon return true } - w.Header().Set(sessionIDHeader, t.sessionID) - w.Header().Set("Content-Type", "text/event-stream") // Accept checked in [StreamableHTTPHandler] - w.Header().Set("Cache-Control", "no-cache, no-transform") - w.Header().Set("Connection", "keep-alive") + errorf := func(code int, format string, args ...any) { + if writes == 0 { + http.Error(w, fmt.Sprintf(format, args...), code) + } else { + // TODO(#170): log when we add server-side logging + } + } if lastIndex >= 0 { // Resume. @@ -466,67 +595,83 @@ func (t *StreamableServerTransport) streamResponse(stream *stream, w http.Respon if errors.Is(err, ErrEventsPurged) { status = http.StatusInsufficientStorage } - return status, err.Error() + errorf(status, "failed to read events: %v", err) + return } // The iterator yields events beginning just after lastIndex, or it would have // yielded an error. if !write(data) { - return 0, "" + return } } } -stream: // Repeatedly collect pending outgoing events and send them. - for { - t.mu.Lock() - outgoing := stream.outgoing - stream.outgoing = nil - t.mu.Unlock() - - for _, data := range outgoing { - if err := t.opts.EventStore.Append(req.Context(), t.SessionID(), stream.id, data); err != nil { - return http.StatusInternalServerError, err.Error() - } - if !write(data) { - return 0, "" + ctx := req.Context() + for msg, ok := range t.messages(ctx, stream, persistent) { + if !ok { + if ctx.Err() != nil && writes == 0 { + // This probably doesn't matter, but respond with NoContent if the client disconnected. + w.WriteHeader(http.StatusNoContent) + } else { + errorf(http.StatusGone, "stream terminated") } + return + } + if err := t.opts.EventStore.Append(req.Context(), t.SessionID(), stream.id, msg); err != nil { + errorf(http.StatusInternalServerError, "storing event: %v", err.Error()) + return + } + if !write(msg) { + return } + } +} - t.mu.Lock() - nOutstanding := len(stream.requests) - t.mu.Unlock() - // If all requests have been handled and replied to, we should terminate this connection. - // "After the JSON-RPC response has been sent, the server SHOULD close the SSE stream." - // §6.4, https://modelcontextprotocol.io/specification/2025-06-18/basic/transports#sending-messages-to-the-server - // We only want to terminate POSTs, and GETs that are replaying. The general-purpose GET - // (stream ID 0) will never have requests, and should remain open indefinitely. - // TODO: implement the GET case. - if req.Method == http.MethodPost && nOutstanding == 0 { - if writes == 0 { - // Spec: If the server accepts the input, the server MUST return HTTP - // status code 202 Accepted with no body. - w.WriteHeader(http.StatusAccepted) +// messages iterates over messages sent to the current stream. +// +// The first iterated value is the received JSON message. The second iterated +// value is an OK value indicating whether the stream terminated normally. +// +// If the stream did not terminate normally, it is either because ctx was +// cancelled, or the connection is closed: check the ctx.Err() to differentiate +// these cases. +func (t *StreamableServerTransport) messages(ctx context.Context, stream *stream, persistent bool) iter.Seq2[json.RawMessage, bool] { + return func(yield func(json.RawMessage, bool) bool) { + for { + t.mu.Lock() + outgoing := stream.outgoing + stream.outgoing = nil + nOutstanding := len(stream.requests) + t.mu.Unlock() + + for _, data := range outgoing { + if !yield(data, true) { + return + } } - return 0, "" - } - select { - case <-*stream.signal.Load(): // there are new outgoing messages - // return to top of loop - case <-t.done: // session is closed - if writes == 0 { - return http.StatusGone, "session terminated" + // If all requests have been handled and replied to, we should terminate this connection. + // "After the JSON-RPC response has been sent, the server SHOULD close the SSE stream." + // §6.4, https://modelcontextprotocol.io/specification/2025-06-18/basic/transports#sending-messages-to-the-server + // We only want to terminate POSTs, and GETs that are replaying. The general-purpose GET + // (stream ID 0) will never have requests, and should remain open indefinitely. + if nOutstanding == 0 && !persistent { + return } - break stream - case <-req.Context().Done(): - if writes == 0 { - w.WriteHeader(http.StatusNoContent) + + select { + case <-*stream.signal.Load(): // there are new outgoing messages + // return to top of loop + case <-t.done: // session is closed + yield(nil, false) + return + case <-ctx.Done(): + yield(nil, false) + return } - break stream } } - return 0, "" } // Event IDs: encode both the logical connection ID and the index, as @@ -586,7 +731,7 @@ func (t *StreamableServerTransport) Write(ctx context.Context, msg jsonrpc.Messa isResponse = true } else { // Otherwise, we check to see if it request was made in the context of an - // ongoing request. This may not be the case if the request way made with + // ongoing request. This may not be the case if the request was made with // an unrelated context. if v := ctx.Value(idContextKey{}); v != nil { forRequest = v.(jsonrpc.ID) @@ -597,10 +742,10 @@ func (t *StreamableServerTransport) Write(ctx context.Context, msg jsonrpc.Messa // // For messages sent outside of a request context, this is the default // connection 0. - var forConn StreamID + var forStream StreamID if forRequest.IsValid() { t.mu.Lock() - forConn = t.requestStreams[forRequest] + forStream = t.requestStreams[forRequest] t.mu.Unlock() } @@ -615,15 +760,19 @@ func (t *StreamableServerTransport) Write(ctx context.Context, msg jsonrpc.Messa return errors.New("session is closed") } - stream := t.streams[forConn] + stream := t.streams[forStream] if stream == nil { - return fmt.Errorf("no stream with ID %d", forConn) + return fmt.Errorf("no stream with ID %d", forStream) } - if len(stream.requests) == 0 && forConn != 0 { - // No outstanding requests for this connection, which means it is logically - // done. This is a sequencing violation from the server, so we should report - // a side-channel error here. Put the message on the general queue to avoid - // dropping messages. + + // Special case a few conditions where we fall back on stream 0 (the hanging GET): + // + // - if forStream is known, but the associated stream is logically complete + // - if the stream is application/json, but the message is not a response + // + // TODO(rfindley): either of these, particularly the first, might be + // considered a bug in the server. Report it through a side-channel? + if len(stream.requests) == 0 && forStream != 0 || stream.jsonResponse && !isResponse { stream = t.streams[0] } @@ -742,164 +891,201 @@ func (t *StreamableClientTransport) Connect(ctx context.Context) (Connection, er ReconnectOptions: reconnOpts, ctx: connCtx, cancel: cancel, + failed: make(chan struct{}), } - // Start the persistent SSE listener right away. - // Section 2.2: The client MAY issue an HTTP GET to the MCP endpoint. - // This can be used to open an SSE stream, allowing the server to - // communicate to the client, without the client first sending data via HTTP POST. - go conn.handleSSE(nil, true) - return conn, nil } type streamableClientConn struct { url string + ReconnectOptions *StreamableReconnectOptions client *http.Client + ctx context.Context + cancel context.CancelFunc incoming chan []byte - done chan struct{} - ReconnectOptions *StreamableReconnectOptions + // Guard calls to Close, as it may be called multiple times. closeOnce sync.Once closeErr error - ctx context.Context - cancel context.CancelFunc + done chan struct{} // signal graceful termination - mu sync.Mutex - protocolVersion string - _sessionID string - err error + // Logical reads are distributed across multiple http requests. Whenever any + // of them fails to process their response, we must break the connection, by + // failing the pending Read. + // + // Achieve this by storing the failure message, and signalling when reads are + // broken. See also [streamableClientConn.fail] and + // [streamableClientConn.failure]. + failOnce sync.Once + _failure error + failed chan struct{} // signal failure + + // Guard the initialization state. + mu sync.Mutex + initializedResult *InitializeResult + sessionID string } -func (c *streamableClientConn) setProtocolVersion(s string) { +func (c *streamableClientConn) initialized(res *InitializeResult) { c.mu.Lock() - defer c.mu.Unlock() - c.protocolVersion = s + c.initializedResult = res + c.mu.Unlock() + + // Start the persistent SSE listener as soon as we have the initialized + // result. + // + // § 2.2: The client MAY issue an HTTP GET to the MCP endpoint. This can be + // used to open an SSE stream, allowing the server to communicate to the + // client, without the client first sending data via HTTP POST. + // + // We have to wait for initialized, because until we've received + // initialized, we don't know whether the server requires a sessionID. + // + // § 2.5: A server using the Streamable HTTP transport MAY assign a session + // ID at initialization time, by including it in an Mcp-Session-Id header + // on the HTTP response containing the InitializeResult. + go c.handleSSE(nil, true) +} + +// fail handles an asynchronous error while reading. +// +// If err is non-nil, it is terminal, and subsequent (or pending) Reads will +// fail. +func (c *streamableClientConn) fail(err error) { + if err != nil { + c.failOnce.Do(func() { + c._failure = err + close(c.failed) + }) + } +} + +func (c *streamableClientConn) failure() error { + select { + case <-c.failed: + return c._failure + default: + return nil + } } func (c *streamableClientConn) SessionID() string { c.mu.Lock() defer c.mu.Unlock() - return c._sessionID + return c.sessionID } // Read implements the [Connection] interface. -func (s *streamableClientConn) Read(ctx context.Context) (jsonrpc.Message, error) { - s.mu.Lock() - err := s.err - s.mu.Unlock() - if err != nil { +func (c *streamableClientConn) Read(ctx context.Context) (jsonrpc.Message, error) { + if err := c.failure(); err != nil { return nil, err } select { case <-ctx.Done(): return nil, ctx.Err() - case <-s.done: + case <-c.failed: + return nil, c.failure() + case <-c.done: return nil, io.EOF - case data := <-s.incoming: + case data := <-c.incoming: return jsonrpc2.DecodeMessage(data) } } // Write implements the [Connection] interface. -func (s *streamableClientConn) Write(ctx context.Context, msg jsonrpc.Message) error { - s.mu.Lock() - if s.err != nil { - s.mu.Unlock() - return s.err - } - - sessionID := s._sessionID - if sessionID == "" { - // Hold lock for the first request. - defer s.mu.Unlock() - } else { - s.mu.Unlock() - } - - gotSessionID, err := s.postMessage(ctx, sessionID, msg) - if err != nil { - if sessionID != "" { - // unlocked; lock to set err - s.mu.Lock() - defer s.mu.Unlock() - } - if s.err != nil { - s.err = err - } +func (c *streamableClientConn) Write(ctx context.Context, msg jsonrpc.Message) error { + if err := c.failure(); err != nil { return err } - if sessionID == "" { - // locked - s._sessionID = gotSessionID - } - - return nil -} - -// postMessage POSTs msg to the server and reads the response. -// It returns the session ID from the response. -func (s *streamableClientConn) postMessage(ctx context.Context, sessionID string, msg jsonrpc.Message) (string, error) { data, err := jsonrpc2.EncodeMessage(msg) if err != nil { - return "", err + return err } - req, err := http.NewRequestWithContext(ctx, http.MethodPost, s.url, bytes.NewReader(data)) + req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.url, bytes.NewReader(data)) if err != nil { - return "", err - } - if s.protocolVersion != "" { - req.Header.Set(protocolVersionHeader, s.protocolVersion) - } - if sessionID != "" { - req.Header.Set(sessionIDHeader, sessionID) + return err } req.Header.Set("Content-Type", "application/json") req.Header.Set("Accept", "application/json, text/event-stream") + c.setMCPHeaders(req) - resp, err := s.client.Do(req) + resp, err := c.client.Do(req) if err != nil { - return "", err + return err } if resp.StatusCode < 200 || resp.StatusCode >= 300 { // TODO: do a best effort read of the body here, and format it in the error. resp.Body.Close() - return "", fmt.Errorf("broken session: %v", resp.Status) + return fmt.Errorf("broken session: %v", resp.Status) } - sessionID = resp.Header.Get(sessionIDHeader) - switch ct := resp.Header.Get("Content-Type"); ct { - case "text/event-stream": - // Section 2.1: The SSE stream is initiated after a POST. - go s.handleSSE(resp, false) - case "application/json": - body, err := io.ReadAll(resp.Body) - resp.Body.Close() - if err != nil { - return "", err + if sessionID := resp.Header.Get(sessionIDHeader); sessionID != "" { + c.mu.Lock() + hadSessionID := c.sessionID + if hadSessionID == "" { + c.sessionID = sessionID } - select { - case s.incoming <- body: - case <-s.done: - // The connection was closed by the client; exit gracefully. + c.mu.Unlock() + if hadSessionID != "" && hadSessionID != sessionID { + resp.Body.Close() + return fmt.Errorf("mismatching session IDs %q and %q", hadSessionID, sessionID) } - return sessionID, nil + } + if resp.StatusCode == http.StatusNoContent || resp.StatusCode == http.StatusAccepted { + resp.Body.Close() + return nil + } + + switch ct := resp.Header.Get("Content-Type"); ct { + case "application/json": + go c.handleJSON(resp) + + case "text/event-stream": + go c.handleSSE(resp, false) + default: resp.Body.Close() - return "", fmt.Errorf("unsupported content type %q", ct) + return fmt.Errorf("unsupported content type %q", ct) + } + return nil +} + +func (c *streamableClientConn) setMCPHeaders(req *http.Request) { + c.mu.Lock() + defer c.mu.Unlock() + + if c.initializedResult != nil { + req.Header.Set(protocolVersionHeader, c.initializedResult.ProtocolVersion) + } + if c.sessionID != "" { + req.Header.Set(sessionIDHeader, c.sessionID) + } +} + +func (c *streamableClientConn) handleJSON(resp *http.Response) { + body, err := io.ReadAll(resp.Body) + resp.Body.Close() + if err != nil { + c.fail(err) + return + } + select { + case c.incoming <- body: + case <-c.done: + // The connection was closed by the client; exit gracefully. } - return sessionID, nil } // handleSSE manages the lifecycle of an SSE connection. It can be either // persistent (for the main GET listener) or temporary (for a POST response). -func (s *streamableClientConn) handleSSE(initialResp *http.Response, persistent bool) { +func (c *streamableClientConn) handleSSE(initialResp *http.Response, persistent bool) { resp := initialResp var lastEventID string for { - eventID, clientClosed := s.processStream(resp) + eventID, clientClosed := c.processStream(resp) lastEventID = eventID // If the connection was closed by the client, we're done. @@ -913,14 +1099,10 @@ func (s *streamableClientConn) handleSSE(initialResp *http.Response, persistent } // The stream was interrupted or ended by the server. Attempt to reconnect. - newResp, err := s.reconnect(lastEventID) + newResp, err := c.reconnect(lastEventID) if err != nil { - // All reconnection attempts failed. Set the final error, close the - // connection, and exit the goroutine. - s.mu.Lock() - s.err = err - s.mu.Unlock() - s.Close() + // All reconnection attempts failed: fail the connection. + c.fail(err) return } @@ -930,11 +1112,12 @@ func (s *streamableClientConn) handleSSE(initialResp *http.Response, persistent } // processStream reads from a single response body, sending events to the -// incoming channel. It returns the ID of the last processed event, any error -// that occurred, and a flag indicating if the connection was closed by the client. -// If resp is nil, it returns "", false. -func (s *streamableClientConn) processStream(resp *http.Response) (lastEventID string, clientClosed bool) { +// incoming channel. It returns the ID of the last processed event and a flag +// indicating if the connection was closed by the client. If resp is nil, it +// returns "", false. +func (c *streamableClientConn) processStream(resp *http.Response) (lastEventID string, clientClosed bool) { if resp == nil { + // TODO(rfindley): avoid this special handling. return "", false } @@ -949,8 +1132,8 @@ func (s *streamableClientConn) processStream(resp *http.Response) (lastEventID s } select { - case s.incoming <- evt.Data: - case <-s.done: + case c.incoming <- evt.Data: + case <-c.done: // The connection was closed by the client; exit gracefully. return "", true } @@ -962,15 +1145,15 @@ func (s *streamableClientConn) processStream(resp *http.Response) (lastEventID s // reconnect handles the logic of retrying a connection with an exponential // backoff strategy. It returns a new, valid HTTP response if successful, or // an error if all retries are exhausted. -func (s *streamableClientConn) reconnect(lastEventID string) (*http.Response, error) { +func (c *streamableClientConn) reconnect(lastEventID string) (*http.Response, error) { var finalErr error - for attempt := 0; attempt < s.ReconnectOptions.MaxRetries; attempt++ { + for attempt := 0; attempt < c.ReconnectOptions.MaxRetries; attempt++ { select { - case <-s.done: + case <-c.done: return nil, fmt.Errorf("connection closed by client during reconnect") - case <-time.After(calculateReconnectDelay(s.ReconnectOptions, attempt)): - resp, err := s.establishSSE(lastEventID) + case <-time.After(calculateReconnectDelay(c.ReconnectOptions, attempt)): + resp, err := c.establishSSE(lastEventID) if err != nil { finalErr = err // Store the error and try again. continue @@ -987,9 +1170,9 @@ func (s *streamableClientConn) reconnect(lastEventID string) (*http.Response, er } // If the loop completes, all retries have failed. if finalErr != nil { - return nil, fmt.Errorf("connection failed after %d attempts: %w", s.ReconnectOptions.MaxRetries, finalErr) + return nil, fmt.Errorf("connection failed after %d attempts: %w", c.ReconnectOptions.MaxRetries, finalErr) } - return nil, fmt.Errorf("connection failed after %d attempts", s.ReconnectOptions.MaxRetries) + return nil, fmt.Errorf("connection failed after %d attempts", c.ReconnectOptions.MaxRetries) } // isResumable checks if an HTTP response indicates a valid SSE stream that can be processed. @@ -1003,48 +1186,40 @@ func isResumable(resp *http.Response) bool { } // Close implements the [Connection] interface. -func (s *streamableClientConn) Close() error { - s.closeOnce.Do(func() { +func (c *streamableClientConn) Close() error { + c.closeOnce.Do(func() { // Cancel any hanging network requests. - s.cancel() - close(s.done) + c.cancel() + close(c.done) - req, err := http.NewRequest(http.MethodDelete, s.url, nil) + req, err := http.NewRequest(http.MethodDelete, c.url, nil) if err != nil { - s.closeErr = err + c.closeErr = err } else { - // TODO(jba): confirm that we don't need a lock here, or add locking. - if s.protocolVersion != "" { - req.Header.Set(protocolVersionHeader, s.protocolVersion) - } - req.Header.Set(sessionIDHeader, s._sessionID) - if _, err := s.client.Do(req); err != nil { - s.closeErr = err + c.setMCPHeaders(req) + if _, err := c.client.Do(req); err != nil { + c.closeErr = err } } }) - return s.closeErr + return c.closeErr } // establishSSE establishes the persistent SSE listening stream. // It is used for reconnect attempts using the Last-Event-ID header to // resume a broken stream where it left off. -func (s *streamableClientConn) establishSSE(lastEventID string) (*http.Response, error) { - req, err := http.NewRequestWithContext(s.ctx, http.MethodGet, s.url, nil) +func (c *streamableClientConn) establishSSE(lastEventID string) (*http.Response, error) { + req, err := http.NewRequestWithContext(c.ctx, http.MethodGet, c.url, nil) if err != nil { return nil, err } - s.mu.Lock() - if s._sessionID != "" { - req.Header.Set("Mcp-Session-Id", s._sessionID) - } - s.mu.Unlock() + c.setMCPHeaders(req) if lastEventID != "" { req.Header.Set("Last-Event-ID", lastEventID) } req.Header.Set("Accept", "text/event-stream") - return s.client.Do(req) + return c.client.Do(req) } // calculateReconnectDelay calculates a delay using exponential backoff with full jitter. @@ -1059,3 +1234,6 @@ func calculateReconnectDelay(opts *StreamableReconnectOptions, attempt int) time return backoffDuration + jitter } + +// For testing. +var newSessionID = randText diff --git a/mcp/streamable_test.go b/mcp/streamable_test.go index af06bd39..1f4a0a67 100644 --- a/mcp/streamable_test.go +++ b/mcp/streamable_test.go @@ -16,6 +16,7 @@ import ( "net/http/httptest" "net/http/httputil" "net/url" + "slices" "strings" "sync" "sync/atomic" @@ -24,9 +25,9 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/jsonschema-go/jsonschema" "github.com/modelcontextprotocol/go-sdk/internal/jsonrpc2" "github.com/modelcontextprotocol/go-sdk/jsonrpc" - "github.com/modelcontextprotocol/go-sdk/jsonschema" ) func TestStreamableTransports(t *testing.T) { @@ -35,77 +36,90 @@ func TestStreamableTransports(t *testing.T) { ctx := context.Background() - // 1. Create a server with a simple "greet" tool. - server := NewServer(testImpl, nil) - AddTool(server, &Tool{Name: "greet", Description: "say hi"}, sayHi) - // 2. Start an httptest.Server with the StreamableHTTPHandler, wrapped in a - // cookie-checking middleware. - handler := NewStreamableHTTPHandler(func(req *http.Request) *Server { return server }, nil) - var header http.Header - httpServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - header = r.Header - cookie, err := r.Cookie("test-cookie") - if err != nil { - t.Errorf("missing cookie: %v", err) - } else if cookie.Value != "test-value" { - t.Errorf("got cookie %q, want %q", cookie.Value, "test-value") - } - handler.ServeHTTP(w, r) - })) - defer httpServer.Close() + for _, useJSON := range []bool{false, true} { + t.Run(fmt.Sprintf("JSONResponse=%v", useJSON), func(t *testing.T) { + // 1. Create a server with a simple "greet" tool. + server := NewServer(testImpl, nil) + AddTool(server, &Tool{Name: "greet", Description: "say hi"}, sayHi) - // 3. Create a client and connect it to the server using our StreamableClientTransport. - // Check that all requests honor a custom client. - jar, err := cookiejar.New(nil) - if err != nil { - t.Fatal(err) - } - u, err := url.Parse(httpServer.URL) - if err != nil { - t.Fatal(err) - } - jar.SetCookies(u, []*http.Cookie{{Name: "test-cookie", Value: "test-value"}}) - httpClient := &http.Client{Jar: jar} - transport := NewStreamableClientTransport(httpServer.URL, &StreamableClientTransportOptions{ - HTTPClient: httpClient, - }) - client := NewClient(testImpl, nil) - session, err := client.Connect(ctx, transport) - if err != nil { - t.Fatalf("client.Connect() failed: %v", err) - } - defer session.Close() - sid := session.ID() - if sid == "" { - t.Error("empty session ID") - } - if g, w := session.mcpConn.(*streamableClientConn).protocolVersion, latestProtocolVersion; g != w { - t.Fatalf("got protocol version %q, want %q", g, w) - } - // 4. The client calls the "greet" tool. - params := &CallToolParams{ - Name: "greet", - Arguments: map[string]any{"name": "streamy"}, - } - got, err := session.CallTool(ctx, params) - if err != nil { - t.Fatalf("CallTool() failed: %v", err) - } - if g := session.ID(); g != sid { - t.Errorf("session ID: got %q, want %q", g, sid) - } - if g, w := header.Get(protocolVersionHeader), latestProtocolVersion; g != w { - t.Errorf("got protocol version header %q, want %q", g, w) - } + // 2. Start an httptest.Server with the StreamableHTTPHandler, wrapped in a + // cookie-checking middleware. + handler := NewStreamableHTTPHandler(func(req *http.Request) *Server { return server }, &StreamableHTTPOptions{ + transportOptions: &StreamableServerTransportOptions{jsonResponse: useJSON}, + }) - // 5. Verify that the correct response is received. - want := &CallToolResult{ - Content: []Content{ - &TextContent{Text: "hi streamy"}, - }, - } - if diff := cmp.Diff(want, got); diff != "" { - t.Errorf("CallTool() returned unexpected content (-want +got):\n%s", diff) + var ( + headerMu sync.Mutex + lastHeader http.Header + ) + httpServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + headerMu.Lock() + lastHeader = r.Header + headerMu.Unlock() + cookie, err := r.Cookie("test-cookie") + if err != nil { + t.Errorf("missing cookie: %v", err) + } else if cookie.Value != "test-value" { + t.Errorf("got cookie %q, want %q", cookie.Value, "test-value") + } + handler.ServeHTTP(w, r) + })) + defer httpServer.Close() + + // 3. Create a client and connect it to the server using our StreamableClientTransport. + // Check that all requests honor a custom client. + jar, err := cookiejar.New(nil) + if err != nil { + t.Fatal(err) + } + u, err := url.Parse(httpServer.URL) + if err != nil { + t.Fatal(err) + } + jar.SetCookies(u, []*http.Cookie{{Name: "test-cookie", Value: "test-value"}}) + httpClient := &http.Client{Jar: jar} + transport := NewStreamableClientTransport(httpServer.URL, &StreamableClientTransportOptions{ + HTTPClient: httpClient, + }) + client := NewClient(testImpl, nil) + session, err := client.Connect(ctx, transport) + if err != nil { + t.Fatalf("client.Connect() failed: %v", err) + } + defer session.Close() + sid := session.ID() + if sid == "" { + t.Error("empty session ID") + } + if g, w := session.mcpConn.(*streamableClientConn).initializedResult.ProtocolVersion, latestProtocolVersion; g != w { + t.Fatalf("got protocol version %q, want %q", g, w) + } + // 4. The client calls the "greet" tool. + params := &CallToolParams{ + Name: "greet", + Arguments: map[string]any{"name": "streamy"}, + } + got, err := session.CallTool(ctx, params) + if err != nil { + t.Fatalf("CallTool() failed: %v", err) + } + if g := session.ID(); g != sid { + t.Errorf("session ID: got %q, want %q", g, sid) + } + if g, w := lastHeader.Get(protocolVersionHeader), latestProtocolVersion; g != w { + t.Errorf("got protocol version header %q, want %q", g, w) + } + + // 5. Verify that the correct response is received. + want := &CallToolResult{ + Content: []Content{ + &TextContent{Text: "hi streamy"}, + }, + } + if diff := cmp.Diff(want, got); diff != "" { + t.Errorf("CallTool() returned unexpected content (-want +got):\n%s", diff) + } + }) } } @@ -222,9 +236,10 @@ func TestServerInitiatedSSE(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() - client := NewClient(testImpl, &ClientOptions{ToolListChangedHandler: func(ctx context.Context, cc *ClientSession, params *ToolListChangedParams) { - notifications <- "toolListChanged" - }, + client := NewClient(testImpl, &ClientOptions{ + ToolListChangedHandler: func(ctx context.Context, cc *ClientSession, params *ToolListChangedParams) { + notifications <- "toolListChanged" + }, }) clientSession, err := client.Connect(ctx, NewStreamableClientTransport(httpServer.URL, nil)) if err != nil { @@ -745,6 +760,7 @@ func TestStreamableClientTransportApplicationJSON(t *testing.T) { t.Fatal(err) } w.Header().Set("Content-Type", "application/json") + w.Header().Set("Mcp-Session-Id", "123") w.Write(data) } @@ -807,3 +823,87 @@ func TestEventID(t *testing.T) { }) } } + +func TestDistributedSessionStore(t *testing.T) { + // To simulate a distributed server with a shared durable SessionStore, we use two distinct + // HTTP servers in memory with a shared MemorySessionStore, and two sessions with the same ID. + + defer func(f func() string) { + newSessionID = f + }(newSessionID) + newSessionID = func() string { return "test-session" } + + ctx := context.Background() + + // Start a server with a single tool. + server := NewServer(testImpl, nil) + AddTool(server, &Tool{Name: "tool"}, func(ctx context.Context, ss *ServerSession, params *CallToolParamsFor[any]) (*CallToolResultFor[int], error) { + ss.Log(ctx, &LoggingMessageParams{ + Level: "info", + Logger: "tool", + }) + return &CallToolResultFor[int]{StructuredContent: 3}, nil + }) + // indexes are: SetLevel, CallTool. + for bits := range 1 << 2 { + t.Run(fmt.Sprintf("%04b", bits), func(t *testing.T) { + indexes := bitsToSlice(bits, 4) + opts := &StreamableHTTPOptions{SessionStore: NewMemorySessionStore()} + var urls []string + for range 2 { + handler := NewStreamableHTTPHandler(func(req *http.Request) *Server { return server }, opts) + + defer handler.closeAll() + httpServer := httptest.NewServer(handler) + defer httpServer.Close() + urls = append(urls, httpServer.URL) + } + + // The log handler will only be called in all cases if the SetLevel change is properly stored in the state. + logCalled := make(chan struct{}) + logHandler := func(_ context.Context, _ *ClientSession, params *LoggingMessageParams) { + close(logCalled) + } + + // Connect clients to each HTTP server. + var clientSessions []*ClientSession + for i := range 2 { + client := NewClient(testImpl, &ClientOptions{LoggingMessageHandler: logHandler}) + // TODO: split initialization handshake between servers. This will send both init messages to the same one. + cs, err := client.Connect(ctx, NewStreamableClientTransport(urls[i], nil)) + if err != nil { + t.Fatal(err) + } + clientSessions = append(clientSessions, cs) + } + + clientSessions[indexes[0]].SetLevel(ctx, &SetLevelParams{Level: "info"}) + + res, err := clientSessions[indexes[1]].CallTool(ctx, &CallToolParams{Name: "tool"}) + if err != nil { + t.Fatal(err) + } + // The logging notification might arrive after CallTool returns. + select { + case <-logCalled: + case <-time.After(time.Second): + t.Error("log not called") + } + if g, w := res.StructuredContent, 3.0; g != w { + t.Errorf("result: got %v %[1]T, want %v %[2]T", g, w) + } + }) + } +} + +// bitsToSlice splits the low-order n bits of bits into a slice of individual bit values. +// For example, 0101 => []int{0, 1, 0, 1}. +func bitsToSlice(bits, n int) []int { + var ints []int + for range n { + ints = append(ints, bits&1) + bits >>= 1 + } + slices.Reverse(ints) + return ints +} diff --git a/mcp/testdata/conformance/server/bad_requests.txtar b/mcp/testdata/conformance/server/bad_requests.txtar index e9e9d483..44816189 100644 --- a/mcp/testdata/conformance/server/bad_requests.txtar +++ b/mcp/testdata/conformance/server/bad_requests.txtar @@ -38,11 +38,12 @@ code_review "clientInfo": { "name": "ExampleClient", "version": "1.0.0" } } } -{"jsonrpc":"2.0", "id": 3, "method": "notifications/initialized"} -{"jsonrpc":"2.0", "method":"ping"} -{"jsonrpc":"2.0", "id": 4, "method": "logging/setLevel"} -{"jsonrpc":"2.0", "id": 5, "method": "completion/complete"} -{"jsonrpc":"2.0", "id": 4, "method": "logging/setLevel", "params": null} +{ "jsonrpc":"2.0", "id": 3, "method": "notifications/initialized" } +{ "jsonrpc":"2.0", "method": "notifications/initialized" } +{ "jsonrpc":"2.0", "method":"ping" } +{ "jsonrpc":"2.0", "id": 4, "method": "logging/setLevel" } +{ "jsonrpc":"2.0", "id": 5, "method": "completion/complete" } +{ "jsonrpc":"2.0", "id": 4, "method": "logging/setLevel", "params": null } -- server -- { diff --git a/mcp/testdata/conformance/server/lifecycle.txtar b/mcp/testdata/conformance/server/lifecycle.txtar new file mode 100644 index 00000000..eba287e0 --- /dev/null +++ b/mcp/testdata/conformance/server/lifecycle.txtar @@ -0,0 +1,57 @@ +This test checks that the server obeys the rules for initialization lifecycle, +and rejects non-ping requests until 'initialized' is received. + +See also modelcontextprotocol/go-sdk#225. + +-- client -- +{ "jsonrpc":"2.0", "method": "notifications/initialized" } +{ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": "2024-11-05", + "capabilities": {}, + "clientInfo": { "name": "ExampleClient", "version": "1.0.0" } + } +} +{ "jsonrpc":"2.0", "id": 1, "method":"ping" } +{ "jsonrpc": "2.0", "id": 2, "method": "tools/list" } +{ "jsonrpc":"2.0", "method": "notifications/initialized" } +{ "jsonrpc": "2.0", "id": 3, "method": "tools/list" } + +-- server -- +{ + "jsonrpc": "2.0", + "id": 1, + "result": { + "capabilities": { + "logging": {} + }, + "protocolVersion": "2024-11-05", + "serverInfo": { + "name": "testServer", + "version": "v1.0.0" + } + } +} +{ + "jsonrpc": "2.0", + "id": 1, + "result": {} +} +{ + "jsonrpc": "2.0", + "id": 2, + "error": { + "code": 0, + "message": "method \"tools/list\" is invalid during session initialization" + } +} +{ + "jsonrpc": "2.0", + "id": 3, + "result": { + "tools": [] + } +} diff --git a/mcp/testdata/conformance/server/prompts.txtar b/mcp/testdata/conformance/server/prompts.txtar index 3fd036e6..fdaf7932 100644 --- a/mcp/testdata/conformance/server/prompts.txtar +++ b/mcp/testdata/conformance/server/prompts.txtar @@ -18,9 +18,11 @@ code_review "clientInfo": { "name": "ExampleClient", "version": "1.0.0" } } } +{ "jsonrpc":"2.0", "method": "notifications/initialized" } { "jsonrpc": "2.0", "id": 2, "method": "tools/list" } { "jsonrpc": "2.0", "id": 4, "method": "prompts/list" } { "jsonrpc": "2.0", "id": 5, "method": "prompts/get" } + -- server -- { "jsonrpc": "2.0", diff --git a/mcp/testdata/conformance/server/resources.txtar b/mcp/testdata/conformance/server/resources.txtar index ae2e23cb..314817b8 100644 --- a/mcp/testdata/conformance/server/resources.txtar +++ b/mcp/testdata/conformance/server/resources.txtar @@ -21,6 +21,7 @@ info.txt "clientInfo": { "name": "ExampleClient", "version": "1.0.0" } } } +{ "jsonrpc":"2.0", "method": "notifications/initialized" } { "jsonrpc": "2.0", "id": 2, "method": "resources/list" } { "jsonrpc": "2.0", "id": 3, diff --git a/mcp/testdata/conformance/server/tools.txtar b/mcp/testdata/conformance/server/tools.txtar index b4068d1c..29dfdc18 100644 --- a/mcp/testdata/conformance/server/tools.txtar +++ b/mcp/testdata/conformance/server/tools.txtar @@ -20,6 +20,7 @@ greet "clientInfo": { "name": "ExampleClient", "version": "1.0.0" } } } +{"jsonrpc":"2.0", "method": "notifications/initialized"} { "jsonrpc": "2.0", "id": 2, "method": "tools/list" } { "jsonrpc": "2.0", "id": 3, "method": "resources/list" } { "jsonrpc": "2.0", "id": 4, "method": "prompts/list" } @@ -59,9 +60,7 @@ greet "type": "string" } }, - "additionalProperties": { - "not": {} - } + "additionalProperties": false }, "name": "greet" } diff --git a/mcp/tool.go b/mcp/tool.go index ed80b660..234cd659 100644 --- a/mcp/tool.go +++ b/mcp/tool.go @@ -11,7 +11,7 @@ import ( "fmt" "reflect" - "github.com/modelcontextprotocol/go-sdk/jsonschema" + "github.com/google/jsonschema-go/jsonschema" ) // A ToolHandler handles a call to tools/call. diff --git a/mcp/tool_test.go b/mcp/tool_test.go index 4d0a329b..52cac9fc 100644 --- a/mcp/tool_test.go +++ b/mcp/tool_test.go @@ -12,7 +12,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" - "github.com/modelcontextprotocol/go-sdk/jsonschema" + "github.com/google/jsonschema-go/jsonschema" ) // testToolHandler is used for type inference in TestNewServerTool. diff --git a/mcp/transport.go b/mcp/transport.go index 5175f6f0..02b21806 100644 --- a/mcp/transport.go +++ b/mcp/transport.go @@ -38,16 +38,33 @@ type Transport interface { // A Connection is a logical bidirectional JSON-RPC connection. type Connection interface { + // Read reads the next message to process off the connection. + // + // Read need not be safe for concurrent use: Read is called in a + // concurrency-safe manner by the JSON-RPC library. Read(context.Context) (jsonrpc.Message, error) + + // Write writes a new message to the connection. + // + // Write may be called concurrently, as calls or reponses may occur + // concurrently in user code. Write(context.Context, jsonrpc.Message) error - Close() error // may be called concurrently by both peers + + // Close closes the connection. It is implicitly called whenever a Read or + // Write fails. + // + // Close may be called multiple times, potentially concurrently. + Close() error + + // TODO(#148): remove SessionID from this interface. SessionID() string } -// An httpConnection is a [Connection] that runs over HTTP. -type httpConnection interface { +// A clientConnection is a [Connection] that is specific to the MCP client, and +// so may receive information about the client session. +type clientConnection interface { Connection - setProtocolVersion(string) + initialized(*InitializeResult) } // A StdioTransport is a [Transport] that communicates over stdin/stdout using @@ -264,8 +281,11 @@ func (r rwc) Close() error { // // See [msgBatch] for more discussion of message batching. type ioConn struct { - rwc io.ReadWriteCloser // the underlying stream - in *json.Decoder // a decoder bound to rwc + writeMu sync.Mutex // guards Write, which must be concurrency safe. + rwc io.ReadWriteCloser // the underlying stream + + // incoming receives messages from the read loop started in [newIOConn]. + incoming <-chan msgOrErr // If outgoiBatch has a positive capacity, it will be used to batch requests // and notifications before sending. @@ -279,12 +299,60 @@ type ioConn struct { // Since writes may be concurrent to reads, we need to guard this with a mutex. batchMu sync.Mutex batches map[jsonrpc2.ID]*msgBatch // lazily allocated + + closeOnce sync.Once + closed chan struct{} + closeErr error +} + +type msgOrErr struct { + msg json.RawMessage + err error } func newIOConn(rwc io.ReadWriteCloser) *ioConn { + var ( + incoming = make(chan msgOrErr) + closed = make(chan struct{}) + ) + // Start a goroutine for reads, so that we can select on the incoming channel + // in [ioConn.Read] and unblock the read as soon as Close is called (see #224). + // + // This leaks a goroutine, but that is unavoidable since AFAIK there is no + // (easy and portable) way to guarantee that reads of stdin are unblocked + // when closed. + go func() { + dec := json.NewDecoder(rwc) + for { + var raw json.RawMessage + err := dec.Decode(&raw) + // If decoding was successful, check for trailing data at the end of the stream. + if err == nil { + // Read the next byte to check if there is trailing data. + var tr [1]byte + if n, readErr := dec.Buffered().Read(tr[:]); n > 0 { + // If read byte is not a newline, it is an error. + if tr[0] != '\n' { + err = fmt.Errorf("invalid trailing data at the end of stream") + } + } else if readErr != nil && readErr != io.EOF { + err = readErr + } + } + select { + case incoming <- msgOrErr{msg: raw, err: err}: + case <-closed: + return + } + if err != nil { + return + } + } + }() return &ioConn{ - rwc: rwc, - in: json.NewDecoder(rwc), + rwc: rwc, + incoming: incoming, + closed: closed, } } @@ -356,10 +424,8 @@ type msgBatch struct { } func (t *ioConn) Read(ctx context.Context) (jsonrpc.Message, error) { - return t.read(ctx, t.in) -} - -func (t *ioConn) read(ctx context.Context, in *json.Decoder) (jsonrpc.Message, error) { + // As a matter of principle, enforce that reads on a closed context return an + // error. select { case <-ctx.Done(): return nil, ctx.Err() @@ -372,9 +438,20 @@ func (t *ioConn) read(ctx context.Context, in *json.Decoder) (jsonrpc.Message, e } var raw json.RawMessage - if err := in.Decode(&raw); err != nil { - return nil, err + select { + case <-ctx.Done(): + return nil, ctx.Err() + + case v := <-t.incoming: + if v.err != nil { + return nil, v.err + } + raw = v.msg + + case <-t.closed: + return nil, io.EOF } + msgs, batch, err := readBatch(raw) if err != nil { return nil, err @@ -431,12 +508,16 @@ func readBatch(data []byte) (msgs []jsonrpc.Message, isBatch bool, _ error) { } func (t *ioConn) Write(ctx context.Context, msg jsonrpc.Message) error { + // As in [ioConn.Read], enforce that Writes on a closed context are an error. select { case <-ctx.Done(): return ctx.Err() default: } + t.writeMu.Lock() + defer t.writeMu.Unlock() + // Batching support: if msg is a Response, it may have completed a batch, so // check that first. Otherwise, it is a request or notification, and we may // want to collect it into a batch before sending, if we're configured to use @@ -478,7 +559,11 @@ func (t *ioConn) Write(ctx context.Context, msg jsonrpc.Message) error { } func (t *ioConn) Close() error { - return t.rwc.Close() + t.closeOnce.Do(func() { + t.closeErr = t.rwc.Close() + close(t.closed) + }) + return t.closeErr } func marshalMessages[T jsonrpc.Message](msgs []T) ([]byte, error) { diff --git a/mcp/transport_test.go b/mcp/transport_test.go index c63b84ee..18a326e8 100644 --- a/mcp/transport_test.go +++ b/mcp/transport_test.go @@ -7,6 +7,7 @@ package mcp import ( "context" "io" + "strings" "testing" "github.com/modelcontextprotocol/go-sdk/internal/jsonrpc2" @@ -51,3 +52,41 @@ func TestBatchFraming(t *testing.T) { } } } + +func TestIOConnRead(t *testing.T) { + tests := []struct { + name string + input string + want string + }{ + + { + name: "valid json input", + input: `{"jsonrpc":"2.0","id":1,"method":"test","params":{}}`, + want: "", + }, + + { + name: "newline at the end of first valid json input", + input: `{"jsonrpc":"2.0","id":1,"method":"test","params":{}} + `, + want: "", + }, + { + name: "bad data at the end of first valid json input", + input: `{"jsonrpc":"2.0","id":1,"method":"test","params":{}},`, + want: "invalid trailing data at the end of stream", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tr := newIOConn(rwc{ + rc: io.NopCloser(strings.NewReader(tt.input)), + }) + _, err := tr.Read(context.Background()) + if err != nil && err.Error() != tt.want { + t.Errorf("ioConn.Read() = %v, want %v", err.Error(), tt.want) + } + }) + } +} diff --git a/mcp/util.go b/mcp/util.go index 82d87940..102c0885 100644 --- a/mcp/util.go +++ b/mcp/util.go @@ -6,12 +6,6 @@ package mcp import ( "crypto/rand" - "encoding/json" - "fmt" - "reflect" - "sync" - - "github.com/modelcontextprotocol/go-sdk/internal/util" ) func assert(cond bool, msg string) { @@ -33,114 +27,3 @@ func randText() string { } return string(src) } - -// marshalStructWithMap marshals its first argument to JSON, treating the field named -// mapField as an embedded map. The first argument must be a pointer to -// a struct. The underlying type of mapField must be a map[string]any, and it must have -// an "omitempty" json tag. -// -// For example, given this struct: -// -// type S struct { -// A int -// Extra map[string] any `json:,omitempty` -// } -// -// and this value: -// -// s := S{A: 1, Extra: map[string]any{"B": 2}} -// -// the call marshalJSONWithMap(s, "Extra") would return -// -// {"A": 1, "B": 2} -// -// It is an error if the map contains the same key as another struct field's -// JSON name. -// -// marshalStructWithMap calls json.Marshal on a value of type T, so T must not -// have a MarshalJSON method that calls this function, on pain of infinite regress. -// -// TODO: avoid this restriction on T by forcing it to marshal in a default way. -// See https://go.dev/play/p/EgXKJHxEx_R. -func marshalStructWithMap[T any](s *T, mapField string) ([]byte, error) { - // Marshal the struct and the map separately, and concatenate the bytes. - // This strategy is dramatically less complicated than - // constructing a synthetic struct or map with the combined keys. - if s == nil { - return []byte("null"), nil - } - s2 := *s - vMapField := reflect.ValueOf(&s2).Elem().FieldByName(mapField) - mapVal := vMapField.Interface().(map[string]any) - - // Check for duplicates. - names := jsonNames(reflect.TypeFor[T]()) - for key := range mapVal { - if names[key] { - return nil, fmt.Errorf("map key %q duplicates struct field", key) - } - } - - // Clear the map field, relying on the omitempty tag to omit it. - vMapField.Set(reflect.Zero(vMapField.Type())) - structBytes, err := json.Marshal(s2) - if err != nil { - return nil, fmt.Errorf("marshalStructWithMap(%+v): %w", s, err) - } - if len(mapVal) == 0 { - return structBytes, nil - } - mapBytes, err := json.Marshal(mapVal) - if err != nil { - return nil, err - } - if len(structBytes) == 2 { // must be "{}" - return mapBytes, nil - } - // "{X}" + "{Y}" => "{X,Y}" - res := append(structBytes[:len(structBytes)-1], ',') - res = append(res, mapBytes[1:]...) - return res, nil -} - -// unmarshalStructWithMap is the inverse of marshalStructWithMap. -// T has the same restrictions as in that function. -func unmarshalStructWithMap[T any](data []byte, v *T, mapField string) error { - // Unmarshal into the struct, ignoring unknown fields. - if err := json.Unmarshal(data, v); err != nil { - return err - } - // Unmarshal into the map. - m := map[string]any{} - if err := json.Unmarshal(data, &m); err != nil { - return err - } - // Delete from the map the fields of the struct. - for n := range jsonNames(reflect.TypeFor[T]()) { - delete(m, n) - } - if len(m) != 0 { - reflect.ValueOf(v).Elem().FieldByName(mapField).Set(reflect.ValueOf(m)) - } - return nil -} - -var jsonNamesMap sync.Map // from reflect.Type to map[string]bool - -// jsonNames returns the set of JSON object keys that t will marshal into. -// t must be a struct type. -func jsonNames(t reflect.Type) map[string]bool { - // Lock not necessary: at worst we'll duplicate work. - if val, ok := jsonNamesMap.Load(t); ok { - return val.(map[string]bool) - } - m := map[string]bool{} - for i := range t.NumField() { - info := util.FieldJSONInfo(t.Field(i)) - if !info.Omit { - m[info.Name] = true - } - } - jsonNamesMap.Store(t, m) - return m -} diff --git a/mcp/util_test.go b/mcp/util_test.go deleted file mode 100644 index f2cb0f5c..00000000 --- a/mcp/util_test.go +++ /dev/null @@ -1,48 +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 mcp - -import ( - "strings" - "testing" - - "github.com/google/go-cmp/cmp" - "github.com/google/go-cmp/cmp/cmpopts" -) - -func TestMarshalStructWithMap(t *testing.T) { - type S struct { - A int - B string `json:"b,omitempty"` - u bool - M map[string]any `json:",omitempty"` - } - t.Run("basic", func(t *testing.T) { - s := S{A: 1, B: "two", M: map[string]any{"!@#": true}} - got, err := marshalStructWithMap(&s, "M") - if err != nil { - t.Fatal(err) - } - want := `{"A":1,"b":"two","!@#":true}` - if g := string(got); g != want { - t.Errorf("\ngot %s\nwant %s", g, want) - } - - var un S - if err := unmarshalStructWithMap(got, &un, "M"); err != nil { - t.Fatal(err) - } - if diff := cmp.Diff(s, un, cmpopts.IgnoreUnexported(S{})); diff != "" { - t.Errorf("mismatch (-want, +got):\n%s", diff) - } - }) - t.Run("duplicate", func(t *testing.T) { - s := S{A: 1, B: "two", M: map[string]any{"b": "dup"}} - _, err := marshalStructWithMap(&s, "M") - if err == nil || !strings.Contains(err.Error(), "duplicate") { - t.Errorf("got %v, want error with 'duplicate'", err) - } - }) -}