pax_global_header00006660000000000000000000000064144570222220014512gustar00rootroot0000000000000052 comment=16bce71af51f6a4a775f11e649a347a8803940d3 jsonschema-5.3.1/000077500000000000000000000000001445702222200136525ustar00rootroot00000000000000jsonschema-5.3.1/.github/000077500000000000000000000000001445702222200152125ustar00rootroot00000000000000jsonschema-5.3.1/.github/workflows/000077500000000000000000000000001445702222200172475ustar00rootroot00000000000000jsonschema-5.3.1/.github/workflows/go.yaml000066400000000000000000000014211445702222200205360ustar00rootroot00000000000000name: gotest on: [push, pull_request] jobs: gotest: name: gotest runs-on: ${{ matrix.os }} strategy: matrix: os: [ubuntu-latest, windows-latest, macos-latest] go: [1.19] steps: - name: setup go uses: actions/setup-go@v3 with: go-version: ${{ matrix.go }} - name: checkout uses: actions/checkout@v3 with: submodules: true # https://github.com/golang/go/issues/49138 - name: disable MallocNanoZone for macos-latest run: echo "MallocNanoZone=0" >> $GITHUB_ENV if: runner.os == 'macOS' - name: test run: go test -race -coverprofile coverage.txt -covermode atomic - name: upload coverage uses: codecov/codecov-action@v3 with: files: coverage.txt jsonschema-5.3.1/.gitignore000066400000000000000000000000361445702222200156410ustar00rootroot00000000000000.vscode .idea *.swp cmd/jv/jv jsonschema-5.3.1/.gitmodules000066400000000000000000000002331445702222200160250ustar00rootroot00000000000000[submodule "testdata/JSON-Schema-Test-Suite"] path = testdata/JSON-Schema-Test-Suite url = https://github.com/json-schema-org/JSON-Schema-Test-Suite.git jsonschema-5.3.1/LICENSE000066400000000000000000000236351445702222200146700ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.jsonschema-5.3.1/README.md000066400000000000000000000204171445702222200151350ustar00rootroot00000000000000# jsonschema v5.3.1 [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) [![GoDoc](https://godoc.org/github.com/santhosh-tekuri/jsonschema?status.svg)](https://pkg.go.dev/github.com/santhosh-tekuri/jsonschema/v5) [![Go Report Card](https://goreportcard.com/badge/github.com/santhosh-tekuri/jsonschema/v5)](https://goreportcard.com/report/github.com/santhosh-tekuri/jsonschema/v5) [![Build Status](https://github.com/santhosh-tekuri/jsonschema/actions/workflows/go.yaml/badge.svg?branch=master)](https://github.com/santhosh-tekuri/jsonschema/actions/workflows/go.yaml) [![codecov](https://codecov.io/gh/santhosh-tekuri/jsonschema/branch/master/graph/badge.svg?token=JMVj1pFT2l)](https://codecov.io/gh/santhosh-tekuri/jsonschema) Package jsonschema provides json-schema compilation and validation. [Benchmarks](https://dev.to/vearutop/benchmarking-correctness-and-performance-of-go-json-schema-validators-3247) ### Features: - implements [draft 2020-12](https://json-schema.org/specification-links.html#2020-12), [draft 2019-09](https://json-schema.org/specification-links.html#draft-2019-09-formerly-known-as-draft-8), [draft-7](https://json-schema.org/specification-links.html#draft-7), [draft-6](https://json-schema.org/specification-links.html#draft-6), [draft-4](https://json-schema.org/specification-links.html#draft-4) - fully compliant with [JSON-Schema-Test-Suite](https://github.com/json-schema-org/JSON-Schema-Test-Suite), (excluding some optional) - list of optional tests that are excluded can be found in schema_test.go(variable [skipTests](https://github.com/santhosh-tekuri/jsonschema/blob/master/schema_test.go#L24)) - validates schemas against meta-schema - full support of remote references - support of recursive references between schemas - detects infinite loop in schemas - thread safe validation - rich, intuitive hierarchial error messages with json-pointers to exact location - supports output formats flag, basic and detailed - supports enabling format and content Assertions in draft2019-09 or above - change `Compiler.AssertFormat`, `Compiler.AssertContent` to `true` - compiled schema can be introspected. easier to develop tools like generating go structs given schema - supports user-defined keywords via [extensions](https://pkg.go.dev/github.com/santhosh-tekuri/jsonschema/v5/#example-package-Extension) - implements following formats (supports [user-defined](https://pkg.go.dev/github.com/santhosh-tekuri/jsonschema/v5/#example-package-UserDefinedFormat)) - date-time, date, time, duration, period (supports leap-second) - uuid, hostname, email - ip-address, ipv4, ipv6 - uri, uriref, uri-template(limited validation) - json-pointer, relative-json-pointer - regex, format - implements following contentEncoding (supports [user-defined](https://pkg.go.dev/github.com/santhosh-tekuri/jsonschema/v5/#example-package-UserDefinedContent)) - base64 - implements following contentMediaType (supports [user-defined](https://pkg.go.dev/github.com/santhosh-tekuri/jsonschema/v5/#example-package-UserDefinedContent)) - application/json - can load from files/http/https/[string](https://pkg.go.dev/github.com/santhosh-tekuri/jsonschema/v5/#example-package-FromString)/[]byte/io.Reader (supports [user-defined](https://pkg.go.dev/github.com/santhosh-tekuri/jsonschema/v5/#example-package-UserDefinedLoader)) see examples in [godoc](https://pkg.go.dev/github.com/santhosh-tekuri/jsonschema/v5) The schema is compiled against the version specified in `$schema` property. If "$schema" property is missing, it uses latest draft which currently implemented by this library. You can force to use specific version, when `$schema` is missing, as follows: ```go compiler := jsonschema.NewCompiler() compiler.Draft = jsonschema.Draft4 ``` This package supports loading json-schema from filePath and fileURL. To load json-schema from HTTPURL, add following import: ```go import _ "github.com/santhosh-tekuri/jsonschema/v5/httploader" ``` ## Rich Errors The ValidationError returned by Validate method contains detailed context to understand why and where the error is. schema.json: ```json { "$ref": "t.json#/definitions/employee" } ``` t.json: ```json { "definitions": { "employee": { "type": "string" } } } ``` doc.json: ```json 1 ``` assuming `err` is the ValidationError returned when `doc.json` validated with `schema.json`, ```go fmt.Printf("%#v\n", err) // using %#v prints errors hierarchy ``` Prints: ``` [I#] [S#] doesn't validate with file:///Users/santhosh/jsonschema/schema.json# [I#] [S#/$ref] doesn't validate with 'file:///Users/santhosh/jsonschema/t.json#/definitions/employee' [I#] [S#/definitions/employee/type] expected string, but got number ``` Here `I` stands for instance document and `S` stands for schema document. The json-fragments that caused error in instance and schema documents are represented using json-pointer notation. Nested causes are printed with indent. To output `err` in `flag` output format: ```go b, _ := json.MarshalIndent(err.FlagOutput(), "", " ") fmt.Println(string(b)) ``` Prints: ```json { "valid": false } ``` To output `err` in `basic` output format: ```go b, _ := json.MarshalIndent(err.BasicOutput(), "", " ") fmt.Println(string(b)) ``` Prints: ```json { "valid": false, "errors": [ { "keywordLocation": "", "absoluteKeywordLocation": "file:///Users/santhosh/jsonschema/schema.json#", "instanceLocation": "", "error": "doesn't validate with file:///Users/santhosh/jsonschema/schema.json#" }, { "keywordLocation": "/$ref", "absoluteKeywordLocation": "file:///Users/santhosh/jsonschema/schema.json#/$ref", "instanceLocation": "", "error": "doesn't validate with 'file:///Users/santhosh/jsonschema/t.json#/definitions/employee'" }, { "keywordLocation": "/$ref/type", "absoluteKeywordLocation": "file:///Users/santhosh/jsonschema/t.json#/definitions/employee/type", "instanceLocation": "", "error": "expected string, but got number" } ] } ``` To output `err` in `detailed` output format: ```go b, _ := json.MarshalIndent(err.DetailedOutput(), "", " ") fmt.Println(string(b)) ``` Prints: ```json { "valid": false, "keywordLocation": "", "absoluteKeywordLocation": "file:///Users/santhosh/jsonschema/schema.json#", "instanceLocation": "", "errors": [ { "valid": false, "keywordLocation": "/$ref", "absoluteKeywordLocation": "file:///Users/santhosh/jsonschema/schema.json#/$ref", "instanceLocation": "", "errors": [ { "valid": false, "keywordLocation": "/$ref/type", "absoluteKeywordLocation": "file:///Users/santhosh/jsonschema/t.json#/definitions/employee/type", "instanceLocation": "", "error": "expected string, but got number" } ] } ] } ``` ## CLI to install `go install github.com/santhosh-tekuri/jsonschema/cmd/jv@latest` ```bash jv [-draft INT] [-output FORMAT] [-assertformat] [-assertcontent] []... -assertcontent enable content assertions with draft >= 2019 -assertformat enable format assertions with draft >= 2019 -draft int draft used when '$schema' attribute is missing. valid values 4, 5, 7, 2019, 2020 (default 2020) -output string output format. valid values flag, basic, detailed ``` if no `` arguments are passed, it simply validates the ``. if `$schema` attribute is missing in schema, it uses latest version. this can be overridden by passing `-draft` flag exit-code is 1, if there are any validation errors `jv` can also validate yaml files. It also accepts schema from yaml files. ## Validating YAML Documents since yaml supports non-string keys, such yaml documents are rendered as invalid json documents. most yaml parser use `map[interface{}]interface{}` for object, whereas json parser uses `map[string]interface{}`. so we need to manually convert them to `map[string]interface{}`. below code shows such conversion by `toStringKeys` function. https://play.golang.org/p/Hhax3MrtD8r NOTE: if you are using `gopkg.in/yaml.v3`, then you do not need such conversion. since this library returns `map[string]interface{}` if all keys are strings.jsonschema-5.3.1/cmd/000077500000000000000000000000001445702222200144155ustar00rootroot00000000000000jsonschema-5.3.1/cmd/jv/000077500000000000000000000000001445702222200150345ustar00rootroot00000000000000jsonschema-5.3.1/cmd/jv/go.mod000066400000000000000000000002241445702222200161400ustar00rootroot00000000000000module github.com/santhosh-tekuri/jsonschema/cmd/jv go 1.15 require ( github.com/santhosh-tekuri/jsonschema/v5 v5.3.0 gopkg.in/yaml.v3 v3.0.1 ) jsonschema-5.3.1/cmd/jv/go.sum000066400000000000000000000010571445702222200161720ustar00rootroot00000000000000github.com/santhosh-tekuri/jsonschema/v5 v5.3.0 h1:uIkTLo0AGRc8l7h5l9r+GcYi9qfVPt6lD4/bhmzfiKo= github.com/santhosh-tekuri/jsonschema/v5 v5.3.0/go.mod h1:FKdcjfQW6rpZSnxxUvEA5H/cDPdvJ/SZJQLWWXWGrZ0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= jsonschema-5.3.1/cmd/jv/main.go000066400000000000000000000073221445702222200163130ustar00rootroot00000000000000package main import ( "bytes" "encoding/json" "flag" "fmt" "io" "io/ioutil" "os" "path/filepath" "strings" "github.com/santhosh-tekuri/jsonschema/v5" _ "github.com/santhosh-tekuri/jsonschema/v5/httploader" "gopkg.in/yaml.v3" ) func usage() { fmt.Fprintln(os.Stderr, "jv [-draft INT] [-output FORMAT] [-assertformat] [-assertcontent] []...") flag.PrintDefaults() } func main() { draft := flag.Int("draft", 2020, "draft used when '$schema' attribute is missing. valid values 4, 5, 7, 2019, 2020") output := flag.String("output", "", "output format. valid values flag, basic, detailed") assertFormat := flag.Bool("assertformat", false, "enable format assertions with draft >= 2019") assertContent := flag.Bool("assertcontent", false, "enable content assertions with draft >= 2019") flag.Usage = usage flag.Parse() if len(flag.Args()) == 0 { usage() os.Exit(1) } compiler := jsonschema.NewCompiler() switch *draft { case 4: compiler.Draft = jsonschema.Draft4 case 6: compiler.Draft = jsonschema.Draft6 case 7: compiler.Draft = jsonschema.Draft7 case 2019: compiler.Draft = jsonschema.Draft2019 case 2020: compiler.Draft = jsonschema.Draft2020 default: fmt.Fprintln(os.Stderr, "draft must be 4, 5, 7, 2019 or 2020") os.Exit(1) } compiler.LoadURL = loadURL compiler.AssertFormat = *assertFormat compiler.AssertContent = *assertContent var validOutput bool for _, out := range []string{"", "flag", "basic", "detailed"} { if *output == out { validOutput = true break } } if !validOutput { fmt.Fprintln(os.Stderr, "output must be flag, basic or detailed") os.Exit(1) } schema, err := compiler.Compile(flag.Arg(0)) if err != nil { fmt.Fprintf(os.Stderr, "%#v\n", err) os.Exit(1) } exitCode := 0 for _, f := range flag.Args()[1:] { file, err := os.Open(f) if err != nil { fmt.Fprintf(os.Stderr, "%v\n", err) exitCode = 1 continue } defer file.Close() v, err := decodeFile(file) if err != nil { fmt.Fprintf(os.Stderr, "%s\n", err) exitCode = 1 continue } err = schema.Validate(v) if err != nil { exitCode = 1 if ve, ok := err.(*jsonschema.ValidationError); ok { var out interface{} switch *output { case "flag": out = ve.FlagOutput() case "basic": out = ve.BasicOutput() case "detailed": out = ve.DetailedOutput() } if out == nil { fmt.Fprintf(os.Stderr, "%#v\n", err) } else { b, _ := json.MarshalIndent(out, "", " ") fmt.Fprintln(os.Stderr, string(b)) } } else { fmt.Fprintf(os.Stderr, "validation failed: %v\n", err) } } else { if *output != "" { fmt.Println("{\n \"valid\": true\n}") } } } os.Exit(exitCode) } func loadURL(s string) (io.ReadCloser, error) { r, err := jsonschema.LoadURL(s) if err != nil { return nil, err } if strings.HasSuffix(s, ".yaml") || strings.HasSuffix(s, ".yml") { defer r.Close() v, err := decodeYAML(r, s) if err != nil { return nil, err } b, err := json.Marshal(v) if err != nil { return nil, err } return ioutil.NopCloser(bytes.NewReader(b)), nil } return r, err } func decodeFile(file *os.File) (interface{}, error) { ext := filepath.Ext(file.Name()) if ext == ".yaml" || ext == ".yml" { return decodeYAML(file, file.Name()) } // json file var v interface{} dec := json.NewDecoder(file) dec.UseNumber() if err := dec.Decode(&v); err != nil { return nil, fmt.Errorf("invalid json file %s: %v", file.Name(), err) } return v, nil } func decodeYAML(r io.Reader, name string) (interface{}, error) { var v interface{} dec := yaml.NewDecoder(r) if err := dec.Decode(&v); err != nil { return nil, fmt.Errorf("invalid yaml file %s: %v", name, err) } return v, nil } jsonschema-5.3.1/compiler.go000066400000000000000000000501311445702222200160130ustar00rootroot00000000000000package jsonschema import ( "encoding/json" "fmt" "io" "math/big" "regexp" "strconv" "strings" ) // A Compiler represents a json-schema compiler. type Compiler struct { // Draft represents the draft used when '$schema' attribute is missing. // // This defaults to latest supported draft (currently 2020-12). Draft *Draft resources map[string]*resource // Extensions is used to register extensions. extensions map[string]extension // ExtractAnnotations tells whether schema annotations has to be extracted // in compiled Schema or not. ExtractAnnotations bool // LoadURL loads the document at given absolute URL. // // If nil, package global LoadURL is used. LoadURL func(s string) (io.ReadCloser, error) // Formats can be registered by adding to this map. Key is format name, // value is function that knows how to validate that format. Formats map[string]func(interface{}) bool // AssertFormat for specifications >= draft2019-09. AssertFormat bool // Decoders can be registered by adding to this map. Key is encoding name, // value is function that knows how to decode string in that format. Decoders map[string]func(string) ([]byte, error) // MediaTypes can be registered by adding to this map. Key is mediaType name, // value is function that knows how to validate that mediaType. MediaTypes map[string]func([]byte) error // AssertContent for specifications >= draft2019-09. AssertContent bool } // Compile parses json-schema at given url returns, if successful, // a Schema object that can be used to match against json. // // Returned error can be *SchemaError func Compile(url string) (*Schema, error) { return NewCompiler().Compile(url) } // MustCompile is like Compile but panics if the url cannot be compiled to *Schema. // It simplifies safe initialization of global variables holding compiled Schemas. func MustCompile(url string) *Schema { return NewCompiler().MustCompile(url) } // CompileString parses and compiles the given schema with given base url. func CompileString(url, schema string) (*Schema, error) { c := NewCompiler() if err := c.AddResource(url, strings.NewReader(schema)); err != nil { return nil, err } return c.Compile(url) } // MustCompileString is like CompileString but panics on error. // It simplified safe initialization of global variables holding compiled Schema. func MustCompileString(url, schema string) *Schema { c := NewCompiler() if err := c.AddResource(url, strings.NewReader(schema)); err != nil { panic(err) } return c.MustCompile(url) } // NewCompiler returns a json-schema Compiler object. // if '$schema' attribute is missing, it is treated as draft7. to change this // behavior change Compiler.Draft value func NewCompiler() *Compiler { return &Compiler{ Draft: latest, resources: make(map[string]*resource), Formats: make(map[string]func(interface{}) bool), Decoders: make(map[string]func(string) ([]byte, error)), MediaTypes: make(map[string]func([]byte) error), extensions: make(map[string]extension), } } // AddResource adds in-memory resource to the compiler. // // Note that url must not have fragment func (c *Compiler) AddResource(url string, r io.Reader) error { res, err := newResource(url, r) if err != nil { return err } c.resources[res.url] = res return nil } // MustCompile is like Compile but panics if the url cannot be compiled to *Schema. // It simplifies safe initialization of global variables holding compiled Schemas. func (c *Compiler) MustCompile(url string) *Schema { s, err := c.Compile(url) if err != nil { panic(fmt.Sprintf("jsonschema: %#v", err)) } return s } // Compile parses json-schema at given url returns, if successful, // a Schema object that can be used to match against json. // // error returned will be of type *SchemaError func (c *Compiler) Compile(url string) (*Schema, error) { // make url absolute u, err := toAbs(url) if err != nil { return nil, &SchemaError{url, err} } url = u sch, err := c.compileURL(url, nil, "#") if err != nil { err = &SchemaError{url, err} } return sch, err } func (c *Compiler) findResource(url string) (*resource, error) { if _, ok := c.resources[url]; !ok { // load resource var rdr io.Reader if sch, ok := vocabSchemas[url]; ok { rdr = strings.NewReader(sch) } else { loadURL := LoadURL if c.LoadURL != nil { loadURL = c.LoadURL } r, err := loadURL(url) if err != nil { return nil, err } defer r.Close() rdr = r } if err := c.AddResource(url, rdr); err != nil { return nil, err } } r := c.resources[url] if r.draft != nil { return r, nil } // set draft r.draft = c.Draft if m, ok := r.doc.(map[string]interface{}); ok { if sch, ok := m["$schema"]; ok { sch, ok := sch.(string) if !ok { return nil, fmt.Errorf("jsonschema: invalid $schema in %s", url) } if !isURI(sch) { return nil, fmt.Errorf("jsonschema: $schema must be uri in %s", url) } r.draft = findDraft(sch) if r.draft == nil { sch, _ := split(sch) if sch == url { return nil, fmt.Errorf("jsonschema: unsupported draft in %s", url) } mr, err := c.findResource(sch) if err != nil { return nil, err } r.draft = mr.draft } } } id, err := r.draft.resolveID(r.url, r.doc) if err != nil { return nil, err } if id != "" { r.url = id } if err := r.fillSubschemas(c, r); err != nil { return nil, err } return r, nil } func (c *Compiler) compileURL(url string, stack []schemaRef, ptr string) (*Schema, error) { // if url points to a draft, return Draft.meta if d := findDraft(url); d != nil && d.meta != nil { return d.meta, nil } b, f := split(url) r, err := c.findResource(b) if err != nil { return nil, err } return c.compileRef(r, stack, ptr, r, f) } func (c *Compiler) compileRef(r *resource, stack []schemaRef, refPtr string, res *resource, ref string) (*Schema, error) { base := r.baseURL(res.floc) ref, err := resolveURL(base, ref) if err != nil { return nil, err } u, f := split(ref) sr := r.findResource(u) if sr == nil { // external resource return c.compileURL(ref, stack, refPtr) } // ensure root resource is always compiled first. // this is required to get schema.meta from root resource if r.schema == nil { r.schema = newSchema(r.url, r.floc, r.draft, r.doc) if _, err := c.compile(r, nil, schemaRef{"#", r.schema, false}, r); err != nil { return nil, err } } sr, err = r.resolveFragment(c, sr, f) if err != nil { return nil, err } if sr == nil { return nil, fmt.Errorf("jsonschema: %s not found", ref) } if sr.schema != nil { if err := checkLoop(stack, schemaRef{refPtr, sr.schema, false}); err != nil { return nil, err } return sr.schema, nil } sr.schema = newSchema(r.url, sr.floc, r.draft, sr.doc) return c.compile(r, stack, schemaRef{refPtr, sr.schema, false}, sr) } func (c *Compiler) compileDynamicAnchors(r *resource, res *resource) error { if r.draft.version < 2020 { return nil } rr := r.listResources(res) rr = append(rr, res) for _, sr := range rr { if m, ok := sr.doc.(map[string]interface{}); ok { if _, ok := m["$dynamicAnchor"]; ok { sch, err := c.compileRef(r, nil, "IGNORED", r, sr.floc) if err != nil { return err } res.schema.dynamicAnchors = append(res.schema.dynamicAnchors, sch) } } } return nil } func (c *Compiler) compile(r *resource, stack []schemaRef, sref schemaRef, res *resource) (*Schema, error) { if err := c.compileDynamicAnchors(r, res); err != nil { return nil, err } switch v := res.doc.(type) { case bool: res.schema.Always = &v return res.schema, nil default: return res.schema, c.compileMap(r, stack, sref, res) } } func (c *Compiler) compileMap(r *resource, stack []schemaRef, sref schemaRef, res *resource) error { m := res.doc.(map[string]interface{}) if err := checkLoop(stack, sref); err != nil { return err } stack = append(stack, sref) var s = res.schema var err error if r == res { // root schema if sch, ok := m["$schema"]; ok { sch := sch.(string) if d := findDraft(sch); d != nil { s.meta = d.meta } else { if s.meta, err = c.compileRef(r, stack, "$schema", res, sch); err != nil { return err } } } } if ref, ok := m["$ref"]; ok { s.Ref, err = c.compileRef(r, stack, "$ref", res, ref.(string)) if err != nil { return err } if r.draft.version < 2019 { // All other properties in a "$ref" object MUST be ignored return nil } } if r.draft.version >= 2019 { if r == res { // root schema if vocab, ok := m["$vocabulary"]; ok { for url, reqd := range vocab.(map[string]interface{}) { if reqd, ok := reqd.(bool); ok && !reqd { continue } if !r.draft.isVocab(url) { return fmt.Errorf("jsonschema: unsupported vocab %q in %s", url, res) } s.vocab = append(s.vocab, url) } } else { s.vocab = r.draft.defaultVocab } } if ref, ok := m["$recursiveRef"]; ok { s.RecursiveRef, err = c.compileRef(r, stack, "$recursiveRef", res, ref.(string)) if err != nil { return err } } } if r.draft.version >= 2020 { if dref, ok := m["$dynamicRef"]; ok { s.DynamicRef, err = c.compileRef(r, stack, "$dynamicRef", res, dref.(string)) if err != nil { return err } if dref, ok := dref.(string); ok { _, frag := split(dref) if frag != "#" && !strings.HasPrefix(frag, "#/") { // frag is anchor s.dynamicRefAnchor = frag[1:] } } } } loadInt := func(pname string) int { if num, ok := m[pname]; ok { i, _ := num.(json.Number).Float64() return int(i) } return -1 } loadRat := func(pname string) *big.Rat { if num, ok := m[pname]; ok { r, _ := new(big.Rat).SetString(string(num.(json.Number))) return r } return nil } if r.draft.version < 2019 || r.schema.meta.hasVocab("validation") { if t, ok := m["type"]; ok { switch t := t.(type) { case string: s.Types = []string{t} case []interface{}: s.Types = toStrings(t) } } if e, ok := m["enum"]; ok { s.Enum = e.([]interface{}) allPrimitives := true for _, item := range s.Enum { switch jsonType(item) { case "object", "array": allPrimitives = false break } } s.enumError = "enum failed" if allPrimitives { if len(s.Enum) == 1 { s.enumError = fmt.Sprintf("value must be %#v", s.Enum[0]) } else { strEnum := make([]string, len(s.Enum)) for i, item := range s.Enum { strEnum[i] = fmt.Sprintf("%#v", item) } s.enumError = fmt.Sprintf("value must be one of %s", strings.Join(strEnum, ", ")) } } } s.Minimum = loadRat("minimum") if exclusive, ok := m["exclusiveMinimum"]; ok { if exclusive, ok := exclusive.(bool); ok { if exclusive { s.Minimum, s.ExclusiveMinimum = nil, s.Minimum } } else { s.ExclusiveMinimum = loadRat("exclusiveMinimum") } } s.Maximum = loadRat("maximum") if exclusive, ok := m["exclusiveMaximum"]; ok { if exclusive, ok := exclusive.(bool); ok { if exclusive { s.Maximum, s.ExclusiveMaximum = nil, s.Maximum } } else { s.ExclusiveMaximum = loadRat("exclusiveMaximum") } } s.MultipleOf = loadRat("multipleOf") s.MinProperties, s.MaxProperties = loadInt("minProperties"), loadInt("maxProperties") if req, ok := m["required"]; ok { s.Required = toStrings(req.([]interface{})) } s.MinItems, s.MaxItems = loadInt("minItems"), loadInt("maxItems") if unique, ok := m["uniqueItems"]; ok { s.UniqueItems = unique.(bool) } s.MinLength, s.MaxLength = loadInt("minLength"), loadInt("maxLength") if pattern, ok := m["pattern"]; ok { s.Pattern = regexp.MustCompile(pattern.(string)) } if r.draft.version >= 2019 { s.MinContains, s.MaxContains = loadInt("minContains"), loadInt("maxContains") if s.MinContains == -1 { s.MinContains = 1 } if deps, ok := m["dependentRequired"]; ok { deps := deps.(map[string]interface{}) s.DependentRequired = make(map[string][]string, len(deps)) for pname, pvalue := range deps { s.DependentRequired[pname] = toStrings(pvalue.([]interface{})) } } } } compile := func(stack []schemaRef, ptr string) (*Schema, error) { return c.compileRef(r, stack, ptr, res, r.url+res.floc+"/"+ptr) } loadSchema := func(pname string, stack []schemaRef) (*Schema, error) { if _, ok := m[pname]; ok { return compile(stack, escape(pname)) } return nil, nil } loadSchemas := func(pname string, stack []schemaRef) ([]*Schema, error) { if pvalue, ok := m[pname]; ok { pvalue := pvalue.([]interface{}) schemas := make([]*Schema, len(pvalue)) for i := range pvalue { sch, err := compile(stack, escape(pname)+"/"+strconv.Itoa(i)) if err != nil { return nil, err } schemas[i] = sch } return schemas, nil } return nil, nil } if r.draft.version < 2019 || r.schema.meta.hasVocab("applicator") { if s.Not, err = loadSchema("not", stack); err != nil { return err } if s.AllOf, err = loadSchemas("allOf", stack); err != nil { return err } if s.AnyOf, err = loadSchemas("anyOf", stack); err != nil { return err } if s.OneOf, err = loadSchemas("oneOf", stack); err != nil { return err } if props, ok := m["properties"]; ok { props := props.(map[string]interface{}) s.Properties = make(map[string]*Schema, len(props)) for pname := range props { s.Properties[pname], err = compile(nil, "properties/"+escape(pname)) if err != nil { return err } } } if regexProps, ok := m["regexProperties"]; ok { s.RegexProperties = regexProps.(bool) } if patternProps, ok := m["patternProperties"]; ok { patternProps := patternProps.(map[string]interface{}) s.PatternProperties = make(map[*regexp.Regexp]*Schema, len(patternProps)) for pattern := range patternProps { s.PatternProperties[regexp.MustCompile(pattern)], err = compile(nil, "patternProperties/"+escape(pattern)) if err != nil { return err } } } if additionalProps, ok := m["additionalProperties"]; ok { switch additionalProps := additionalProps.(type) { case bool: s.AdditionalProperties = additionalProps case map[string]interface{}: s.AdditionalProperties, err = compile(nil, "additionalProperties") if err != nil { return err } } } if deps, ok := m["dependencies"]; ok { deps := deps.(map[string]interface{}) s.Dependencies = make(map[string]interface{}, len(deps)) for pname, pvalue := range deps { switch pvalue := pvalue.(type) { case []interface{}: s.Dependencies[pname] = toStrings(pvalue) default: s.Dependencies[pname], err = compile(stack, "dependencies/"+escape(pname)) if err != nil { return err } } } } if r.draft.version >= 6 { if s.PropertyNames, err = loadSchema("propertyNames", nil); err != nil { return err } if s.Contains, err = loadSchema("contains", nil); err != nil { return err } } if r.draft.version >= 7 { if m["if"] != nil { if s.If, err = loadSchema("if", stack); err != nil { return err } if s.Then, err = loadSchema("then", stack); err != nil { return err } if s.Else, err = loadSchema("else", stack); err != nil { return err } } } if r.draft.version >= 2019 { if deps, ok := m["dependentSchemas"]; ok { deps := deps.(map[string]interface{}) s.DependentSchemas = make(map[string]*Schema, len(deps)) for pname := range deps { s.DependentSchemas[pname], err = compile(stack, "dependentSchemas/"+escape(pname)) if err != nil { return err } } } } if r.draft.version >= 2020 { if s.PrefixItems, err = loadSchemas("prefixItems", nil); err != nil { return err } if s.Items2020, err = loadSchema("items", nil); err != nil { return err } } else { if items, ok := m["items"]; ok { switch items.(type) { case []interface{}: s.Items, err = loadSchemas("items", nil) if err != nil { return err } if additionalItems, ok := m["additionalItems"]; ok { switch additionalItems := additionalItems.(type) { case bool: s.AdditionalItems = additionalItems case map[string]interface{}: s.AdditionalItems, err = compile(nil, "additionalItems") if err != nil { return err } } } default: s.Items, err = compile(nil, "items") if err != nil { return err } } } } } // unevaluatedXXX keywords were in "applicator" vocab in 2019, but moved to new vocab "unevaluated" in 2020 if (r.draft.version == 2019 && r.schema.meta.hasVocab("applicator")) || (r.draft.version >= 2020 && r.schema.meta.hasVocab("unevaluated")) { if s.UnevaluatedProperties, err = loadSchema("unevaluatedProperties", nil); err != nil { return err } if s.UnevaluatedItems, err = loadSchema("unevaluatedItems", nil); err != nil { return err } if r.draft.version >= 2020 { // any item in an array that passes validation of the contains schema is considered "evaluated" s.ContainsEval = true } } if format, ok := m["format"]; ok { s.Format = format.(string) if r.draft.version < 2019 || c.AssertFormat || r.schema.meta.hasVocab("format-assertion") { if format, ok := c.Formats[s.Format]; ok { s.format = format } else { s.format, _ = Formats[s.Format] } } } if c.ExtractAnnotations { if title, ok := m["title"]; ok { s.Title = title.(string) } if description, ok := m["description"]; ok { s.Description = description.(string) } s.Default = m["default"] } if r.draft.version >= 6 { if c, ok := m["const"]; ok { s.Constant = []interface{}{c} } } if r.draft.version >= 7 { if encoding, ok := m["contentEncoding"]; ok { s.ContentEncoding = encoding.(string) if decoder, ok := c.Decoders[s.ContentEncoding]; ok { s.decoder = decoder } else { s.decoder, _ = Decoders[s.ContentEncoding] } } if mediaType, ok := m["contentMediaType"]; ok { s.ContentMediaType = mediaType.(string) if mediaType, ok := c.MediaTypes[s.ContentMediaType]; ok { s.mediaType = mediaType } else { s.mediaType, _ = MediaTypes[s.ContentMediaType] } if s.ContentSchema, err = loadSchema("contentSchema", stack); err != nil { return err } } if c.ExtractAnnotations { if comment, ok := m["$comment"]; ok { s.Comment = comment.(string) } if readOnly, ok := m["readOnly"]; ok { s.ReadOnly = readOnly.(bool) } if writeOnly, ok := m["writeOnly"]; ok { s.WriteOnly = writeOnly.(bool) } if examples, ok := m["examples"]; ok { s.Examples = examples.([]interface{}) } } } if r.draft.version >= 2019 { if !c.AssertContent { s.decoder = nil s.mediaType = nil s.ContentSchema = nil } if c.ExtractAnnotations { if deprecated, ok := m["deprecated"]; ok { s.Deprecated = deprecated.(bool) } } } for name, ext := range c.extensions { es, err := ext.compiler.Compile(CompilerContext{c, r, stack, res}, m) if err != nil { return err } if es != nil { if s.Extensions == nil { s.Extensions = make(map[string]ExtSchema) } s.Extensions[name] = es } } return nil } func (c *Compiler) validateSchema(r *resource, v interface{}, vloc string) error { validate := func(meta *Schema) error { if meta == nil { return nil } return meta.validateValue(v, vloc) } if err := validate(r.draft.meta); err != nil { return err } for _, ext := range c.extensions { if err := validate(ext.meta); err != nil { return err } } return nil } func toStrings(arr []interface{}) []string { s := make([]string, len(arr)) for i, v := range arr { s[i] = v.(string) } return s } // SchemaRef captures schema and the path referring to it. type schemaRef struct { path string // relative-json-pointer to schema schema *Schema // target schema discard bool // true when scope left } func (sr schemaRef) String() string { return fmt.Sprintf("(%s)%v", sr.path, sr.schema) } func checkLoop(stack []schemaRef, sref schemaRef) error { for _, ref := range stack { if ref.schema == sref.schema { return infiniteLoopError(stack, sref) } } return nil } func keywordLocation(stack []schemaRef, path string) string { var loc string for _, ref := range stack[1:] { loc += "/" + ref.path } if path != "" { loc = loc + "/" + path } return loc } jsonschema-5.3.1/content.go000066400000000000000000000015471445702222200156620ustar00rootroot00000000000000package jsonschema import ( "encoding/base64" "encoding/json" ) // Decoders is a registry of functions, which know how to decode // string encoded in specific format. // // New Decoders can be registered by adding to this map. Key is encoding name, // value is function that knows how to decode string in that format. var Decoders = map[string]func(string) ([]byte, error){ "base64": base64.StdEncoding.DecodeString, } // MediaTypes is a registry of functions, which know how to validate // whether the bytes represent data of that mediaType. // // New mediaTypes can be registered by adding to this map. Key is mediaType name, // value is function that knows how to validate that mediaType. var MediaTypes = map[string]func([]byte) error{ "application/json": validateJSON, } func validateJSON(b []byte) error { var v interface{} return json.Unmarshal(b, &v) } jsonschema-5.3.1/doc.go000066400000000000000000000040531445702222200147500ustar00rootroot00000000000000/* Package jsonschema provides json-schema compilation and validation. Features: - implements draft 2020-12, 2019-09, draft-7, draft-6, draft-4 - fully compliant with JSON-Schema-Test-Suite, (excluding some optional) - list of optional tests that are excluded can be found in schema_test.go(variable skipTests) - validates schemas against meta-schema - full support of remote references - support of recursive references between schemas - detects infinite loop in schemas - thread safe validation - rich, intuitive hierarchial error messages with json-pointers to exact location - supports output formats flag, basic and detailed - supports enabling format and content Assertions in draft2019-09 or above - change Compiler.AssertFormat, Compiler.AssertContent to true - compiled schema can be introspected. easier to develop tools like generating go structs given schema - supports user-defined keywords via extensions - implements following formats (supports user-defined) - date-time, date, time, duration (supports leap-second) - uuid, hostname, email - ip-address, ipv4, ipv6 - uri, uriref, uri-template(limited validation) - json-pointer, relative-json-pointer - regex, format - implements following contentEncoding (supports user-defined) - base64 - implements following contentMediaType (supports user-defined) - application/json - can load from files/http/https/string/[]byte/io.Reader (supports user-defined) The schema is compiled against the version specified in "$schema" property. If "$schema" property is missing, it uses latest draft which currently implemented by this library. You can force to use specific draft, when "$schema" is missing, as follows: compiler := jsonschema.NewCompiler() compiler.Draft = jsonschema.Draft4 This package supports loading json-schema from filePath and fileURL. To load json-schema from HTTPURL, add following import: import _ "github.com/santhosh-tekuri/jsonschema/v5/httploader" you can validate yaml documents. see https://play.golang.org/p/sJy1qY7dXgA */ package jsonschema jsonschema-5.3.1/draft.go000066400000000000000000001100011445702222200152720ustar00rootroot00000000000000package jsonschema import ( "fmt" "strconv" "strings" ) // A Draft represents json-schema draft type Draft struct { version int meta *Schema id string // property name used to represent schema id. boolSchema bool // is boolean valid schema vocab []string // built-in vocab defaultVocab []string // vocabs when $vocabulary is not used subschemas map[string]position } func (d *Draft) URL() string { switch d.version { case 2020: return "https://json-schema.org/draft/2020-12/schema" case 2019: return "https://json-schema.org/draft/2019-09/schema" case 7: return "https://json-schema.org/draft-07/schema" case 6: return "https://json-schema.org/draft-06/schema" case 4: return "https://json-schema.org/draft-04/schema" } return "" } func (d *Draft) String() string { return fmt.Sprintf("Draft%d", d.version) } func (d *Draft) loadMeta(url, schema string) { c := NewCompiler() c.AssertFormat = true if err := c.AddResource(url, strings.NewReader(schema)); err != nil { panic(err) } d.meta = c.MustCompile(url) d.meta.meta = d.meta } func (d *Draft) getID(sch interface{}) string { m, ok := sch.(map[string]interface{}) if !ok { return "" } if _, ok := m["$ref"]; ok && d.version <= 7 { // $ref prevents a sibling id from changing the base uri return "" } v, ok := m[d.id] if !ok { return "" } id, ok := v.(string) if !ok { return "" } return id } func (d *Draft) resolveID(base string, sch interface{}) (string, error) { id, _ := split(d.getID(sch)) // strip fragment if id == "" { return "", nil } url, err := resolveURL(base, id) url, _ = split(url) // strip fragment return url, err } func (d *Draft) anchors(sch interface{}) []string { m, ok := sch.(map[string]interface{}) if !ok { return nil } var anchors []string // before draft2019, anchor is specified in id _, f := split(d.getID(m)) if f != "#" { anchors = append(anchors, f[1:]) } if v, ok := m["$anchor"]; ok && d.version >= 2019 { anchors = append(anchors, v.(string)) } if v, ok := m["$dynamicAnchor"]; ok && d.version >= 2020 { anchors = append(anchors, v.(string)) } return anchors } // listSubschemas collects subschemas in r into rr. func (d *Draft) listSubschemas(r *resource, base string, rr map[string]*resource) error { add := func(loc string, sch interface{}) error { url, err := d.resolveID(base, sch) if err != nil { return err } floc := r.floc + "/" + loc sr := &resource{url: url, floc: floc, doc: sch} rr[floc] = sr base := base if url != "" { base = url } return d.listSubschemas(sr, base, rr) } sch, ok := r.doc.(map[string]interface{}) if !ok { return nil } for kw, pos := range d.subschemas { v, ok := sch[kw] if !ok { continue } if pos&self != 0 { switch v := v.(type) { case map[string]interface{}: if err := add(kw, v); err != nil { return err } case bool: if d.boolSchema { if err := add(kw, v); err != nil { return err } } } } if pos&item != 0 { if v, ok := v.([]interface{}); ok { for i, item := range v { if err := add(kw+"/"+strconv.Itoa(i), item); err != nil { return err } } } } if pos&prop != 0 { if v, ok := v.(map[string]interface{}); ok { for pname, pval := range v { if err := add(kw+"/"+escape(pname), pval); err != nil { return err } } } } } return nil } // isVocab tells whether url is built-in vocab. func (d *Draft) isVocab(url string) bool { for _, v := range d.vocab { if url == v { return true } } return false } type position uint const ( self position = 1 << iota prop item ) // supported drafts var ( Draft4 = &Draft{version: 4, id: "id", boolSchema: false} Draft6 = &Draft{version: 6, id: "$id", boolSchema: true} Draft7 = &Draft{version: 7, id: "$id", boolSchema: true} Draft2019 = &Draft{ version: 2019, id: "$id", boolSchema: true, vocab: []string{ "https://json-schema.org/draft/2019-09/vocab/core", "https://json-schema.org/draft/2019-09/vocab/applicator", "https://json-schema.org/draft/2019-09/vocab/validation", "https://json-schema.org/draft/2019-09/vocab/meta-data", "https://json-schema.org/draft/2019-09/vocab/format", "https://json-schema.org/draft/2019-09/vocab/content", }, defaultVocab: []string{ "https://json-schema.org/draft/2019-09/vocab/core", "https://json-schema.org/draft/2019-09/vocab/applicator", "https://json-schema.org/draft/2019-09/vocab/validation", }, } Draft2020 = &Draft{ version: 2020, id: "$id", boolSchema: true, vocab: []string{ "https://json-schema.org/draft/2020-12/vocab/core", "https://json-schema.org/draft/2020-12/vocab/applicator", "https://json-schema.org/draft/2020-12/vocab/unevaluated", "https://json-schema.org/draft/2020-12/vocab/validation", "https://json-schema.org/draft/2020-12/vocab/meta-data", "https://json-schema.org/draft/2020-12/vocab/format-annotation", "https://json-schema.org/draft/2020-12/vocab/format-assertion", "https://json-schema.org/draft/2020-12/vocab/content", }, defaultVocab: []string{ "https://json-schema.org/draft/2020-12/vocab/core", "https://json-schema.org/draft/2020-12/vocab/applicator", "https://json-schema.org/draft/2020-12/vocab/unevaluated", "https://json-schema.org/draft/2020-12/vocab/validation", }, } latest = Draft2020 ) func findDraft(url string) *Draft { if strings.HasPrefix(url, "http://") { url = "https://" + strings.TrimPrefix(url, "http://") } if strings.HasSuffix(url, "#") || strings.HasSuffix(url, "#/") { url = url[:strings.IndexByte(url, '#')] } switch url { case "https://json-schema.org/schema": return latest case "https://json-schema.org/draft/2020-12/schema": return Draft2020 case "https://json-schema.org/draft/2019-09/schema": return Draft2019 case "https://json-schema.org/draft-07/schema": return Draft7 case "https://json-schema.org/draft-06/schema": return Draft6 case "https://json-schema.org/draft-04/schema": return Draft4 } return nil } func init() { subschemas := map[string]position{ // type agnostic "definitions": prop, "not": self, "allOf": item, "anyOf": item, "oneOf": item, // object "properties": prop, "additionalProperties": self, "patternProperties": prop, // array "items": self | item, "additionalItems": self, "dependencies": prop, } Draft4.subschemas = clone(subschemas) subschemas["propertyNames"] = self subschemas["contains"] = self Draft6.subschemas = clone(subschemas) subschemas["if"] = self subschemas["then"] = self subschemas["else"] = self Draft7.subschemas = clone(subschemas) subschemas["$defs"] = prop subschemas["dependentSchemas"] = prop subschemas["unevaluatedProperties"] = self subschemas["unevaluatedItems"] = self subschemas["contentSchema"] = self Draft2019.subschemas = clone(subschemas) subschemas["prefixItems"] = item Draft2020.subschemas = clone(subschemas) Draft4.loadMeta("http://json-schema.org/draft-04/schema", `{ "$schema": "http://json-schema.org/draft-04/schema#", "description": "Core schema meta-schema", "definitions": { "schemaArray": { "type": "array", "minItems": 1, "items": { "$ref": "#" } }, "positiveInteger": { "type": "integer", "minimum": 0 }, "positiveIntegerDefault0": { "allOf": [ { "$ref": "#/definitions/positiveInteger" }, { "default": 0 } ] }, "simpleTypes": { "enum": [ "array", "boolean", "integer", "null", "number", "object", "string" ] }, "stringArray": { "type": "array", "items": { "type": "string" }, "minItems": 1, "uniqueItems": true } }, "type": "object", "properties": { "id": { "type": "string", "format": "uriref" }, "$schema": { "type": "string", "format": "uri" }, "title": { "type": "string" }, "description": { "type": "string" }, "default": {}, "multipleOf": { "type": "number", "minimum": 0, "exclusiveMinimum": true }, "maximum": { "type": "number" }, "exclusiveMaximum": { "type": "boolean", "default": false }, "minimum": { "type": "number" }, "exclusiveMinimum": { "type": "boolean", "default": false }, "maxLength": { "$ref": "#/definitions/positiveInteger" }, "minLength": { "$ref": "#/definitions/positiveIntegerDefault0" }, "pattern": { "type": "string", "format": "regex" }, "additionalItems": { "anyOf": [ { "type": "boolean" }, { "$ref": "#" } ], "default": {} }, "items": { "anyOf": [ { "$ref": "#" }, { "$ref": "#/definitions/schemaArray" } ], "default": {} }, "maxItems": { "$ref": "#/definitions/positiveInteger" }, "minItems": { "$ref": "#/definitions/positiveIntegerDefault0" }, "uniqueItems": { "type": "boolean", "default": false }, "maxProperties": { "$ref": "#/definitions/positiveInteger" }, "minProperties": { "$ref": "#/definitions/positiveIntegerDefault0" }, "required": { "$ref": "#/definitions/stringArray" }, "additionalProperties": { "anyOf": [ { "type": "boolean" }, { "$ref": "#" } ], "default": {} }, "definitions": { "type": "object", "additionalProperties": { "$ref": "#" }, "default": {} }, "properties": { "type": "object", "additionalProperties": { "$ref": "#" }, "default": {} }, "patternProperties": { "type": "object", "regexProperties": true, "additionalProperties": { "$ref": "#" }, "default": {} }, "regexProperties": { "type": "boolean" }, "dependencies": { "type": "object", "additionalProperties": { "anyOf": [ { "$ref": "#" }, { "$ref": "#/definitions/stringArray" } ] } }, "enum": { "type": "array", "minItems": 1, "uniqueItems": true }, "type": { "anyOf": [ { "$ref": "#/definitions/simpleTypes" }, { "type": "array", "items": { "$ref": "#/definitions/simpleTypes" }, "minItems": 1, "uniqueItems": true } ] }, "allOf": { "$ref": "#/definitions/schemaArray" }, "anyOf": { "$ref": "#/definitions/schemaArray" }, "oneOf": { "$ref": "#/definitions/schemaArray" }, "not": { "$ref": "#" }, "format": { "type": "string" }, "$ref": { "type": "string" } }, "dependencies": { "exclusiveMaximum": [ "maximum" ], "exclusiveMinimum": [ "minimum" ] }, "default": {} }`) Draft6.loadMeta("http://json-schema.org/draft-06/schema", `{ "$schema": "http://json-schema.org/draft-06/schema#", "$id": "http://json-schema.org/draft-06/schema#", "title": "Core schema meta-schema", "definitions": { "schemaArray": { "type": "array", "minItems": 1, "items": { "$ref": "#" } }, "nonNegativeInteger": { "type": "integer", "minimum": 0 }, "nonNegativeIntegerDefault0": { "allOf": [ { "$ref": "#/definitions/nonNegativeInteger" }, { "default": 0 } ] }, "simpleTypes": { "enum": [ "array", "boolean", "integer", "null", "number", "object", "string" ] }, "stringArray": { "type": "array", "items": { "type": "string" }, "uniqueItems": true, "default": [] } }, "type": ["object", "boolean"], "properties": { "$id": { "type": "string", "format": "uri-reference" }, "$schema": { "type": "string", "format": "uri" }, "$ref": { "type": "string", "format": "uri-reference" }, "title": { "type": "string" }, "description": { "type": "string" }, "default": {}, "multipleOf": { "type": "number", "exclusiveMinimum": 0 }, "maximum": { "type": "number" }, "exclusiveMaximum": { "type": "number" }, "minimum": { "type": "number" }, "exclusiveMinimum": { "type": "number" }, "maxLength": { "$ref": "#/definitions/nonNegativeInteger" }, "minLength": { "$ref": "#/definitions/nonNegativeIntegerDefault0" }, "pattern": { "type": "string", "format": "regex" }, "additionalItems": { "$ref": "#" }, "items": { "anyOf": [ { "$ref": "#" }, { "$ref": "#/definitions/schemaArray" } ], "default": {} }, "maxItems": { "$ref": "#/definitions/nonNegativeInteger" }, "minItems": { "$ref": "#/definitions/nonNegativeIntegerDefault0" }, "uniqueItems": { "type": "boolean", "default": false }, "contains": { "$ref": "#" }, "maxProperties": { "$ref": "#/definitions/nonNegativeInteger" }, "minProperties": { "$ref": "#/definitions/nonNegativeIntegerDefault0" }, "required": { "$ref": "#/definitions/stringArray" }, "additionalProperties": { "$ref": "#" }, "definitions": { "type": "object", "additionalProperties": { "$ref": "#" }, "default": {} }, "properties": { "type": "object", "additionalProperties": { "$ref": "#" }, "default": {} }, "patternProperties": { "type": "object", "regexProperties": true, "additionalProperties": { "$ref": "#" }, "default": {} }, "dependencies": { "type": "object", "additionalProperties": { "anyOf": [ { "$ref": "#" }, { "$ref": "#/definitions/stringArray" } ] } }, "propertyNames": { "$ref": "#" }, "const": {}, "enum": { "type": "array", "minItems": 1, "uniqueItems": true }, "type": { "anyOf": [ { "$ref": "#/definitions/simpleTypes" }, { "type": "array", "items": { "$ref": "#/definitions/simpleTypes" }, "minItems": 1, "uniqueItems": true } ] }, "format": { "type": "string" }, "allOf": { "$ref": "#/definitions/schemaArray" }, "anyOf": { "$ref": "#/definitions/schemaArray" }, "oneOf": { "$ref": "#/definitions/schemaArray" }, "not": { "$ref": "#" } }, "default": {} }`) Draft7.loadMeta("http://json-schema.org/draft-07/schema", `{ "$schema": "http://json-schema.org/draft-07/schema#", "$id": "http://json-schema.org/draft-07/schema#", "title": "Core schema meta-schema", "definitions": { "schemaArray": { "type": "array", "minItems": 1, "items": { "$ref": "#" } }, "nonNegativeInteger": { "type": "integer", "minimum": 0 }, "nonNegativeIntegerDefault0": { "allOf": [ { "$ref": "#/definitions/nonNegativeInteger" }, { "default": 0 } ] }, "simpleTypes": { "enum": [ "array", "boolean", "integer", "null", "number", "object", "string" ] }, "stringArray": { "type": "array", "items": { "type": "string" }, "uniqueItems": true, "default": [] } }, "type": ["object", "boolean"], "properties": { "$id": { "type": "string", "format": "uri-reference" }, "$schema": { "type": "string", "format": "uri" }, "$ref": { "type": "string", "format": "uri-reference" }, "$comment": { "type": "string" }, "title": { "type": "string" }, "description": { "type": "string" }, "default": true, "readOnly": { "type": "boolean", "default": false }, "writeOnly": { "type": "boolean", "default": false }, "examples": { "type": "array", "items": true }, "multipleOf": { "type": "number", "exclusiveMinimum": 0 }, "maximum": { "type": "number" }, "exclusiveMaximum": { "type": "number" }, "minimum": { "type": "number" }, "exclusiveMinimum": { "type": "number" }, "maxLength": { "$ref": "#/definitions/nonNegativeInteger" }, "minLength": { "$ref": "#/definitions/nonNegativeIntegerDefault0" }, "pattern": { "type": "string", "format": "regex" }, "additionalItems": { "$ref": "#" }, "items": { "anyOf": [ { "$ref": "#" }, { "$ref": "#/definitions/schemaArray" } ], "default": true }, "maxItems": { "$ref": "#/definitions/nonNegativeInteger" }, "minItems": { "$ref": "#/definitions/nonNegativeIntegerDefault0" }, "uniqueItems": { "type": "boolean", "default": false }, "contains": { "$ref": "#" }, "maxProperties": { "$ref": "#/definitions/nonNegativeInteger" }, "minProperties": { "$ref": "#/definitions/nonNegativeIntegerDefault0" }, "required": { "$ref": "#/definitions/stringArray" }, "additionalProperties": { "$ref": "#" }, "definitions": { "type": "object", "additionalProperties": { "$ref": "#" }, "default": {} }, "properties": { "type": "object", "additionalProperties": { "$ref": "#" }, "default": {} }, "patternProperties": { "type": "object", "additionalProperties": { "$ref": "#" }, "propertyNames": { "format": "regex" }, "default": {} }, "dependencies": { "type": "object", "additionalProperties": { "anyOf": [ { "$ref": "#" }, { "$ref": "#/definitions/stringArray" } ] } }, "propertyNames": { "$ref": "#" }, "const": true, "enum": { "type": "array", "items": true, "minItems": 1, "uniqueItems": true }, "type": { "anyOf": [ { "$ref": "#/definitions/simpleTypes" }, { "type": "array", "items": { "$ref": "#/definitions/simpleTypes" }, "minItems": 1, "uniqueItems": true } ] }, "format": { "type": "string" }, "contentMediaType": { "type": "string" }, "contentEncoding": { "type": "string" }, "if": { "$ref": "#" }, "then": { "$ref": "#" }, "else": { "$ref": "#" }, "allOf": { "$ref": "#/definitions/schemaArray" }, "anyOf": { "$ref": "#/definitions/schemaArray" }, "oneOf": { "$ref": "#/definitions/schemaArray" }, "not": { "$ref": "#" } }, "default": true }`) Draft2019.loadMeta("https://json-schema.org/draft/2019-09/schema", `{ "$schema": "https://json-schema.org/draft/2019-09/schema", "$id": "https://json-schema.org/draft/2019-09/schema", "$vocabulary": { "https://json-schema.org/draft/2019-09/vocab/core": true, "https://json-schema.org/draft/2019-09/vocab/applicator": true, "https://json-schema.org/draft/2019-09/vocab/validation": true, "https://json-schema.org/draft/2019-09/vocab/meta-data": true, "https://json-schema.org/draft/2019-09/vocab/format": false, "https://json-schema.org/draft/2019-09/vocab/content": true }, "$recursiveAnchor": true, "title": "Core and Validation specifications meta-schema", "allOf": [ {"$ref": "meta/core"}, {"$ref": "meta/applicator"}, {"$ref": "meta/validation"}, {"$ref": "meta/meta-data"}, {"$ref": "meta/format"}, {"$ref": "meta/content"} ], "type": ["object", "boolean"], "properties": { "definitions": { "$comment": "While no longer an official keyword as it is replaced by $defs, this keyword is retained in the meta-schema to prevent incompatible extensions as it remains in common use.", "type": "object", "additionalProperties": { "$recursiveRef": "#" }, "default": {} }, "dependencies": { "$comment": "\"dependencies\" is no longer a keyword, but schema authors should avoid redefining it to facilitate a smooth transition to \"dependentSchemas\" and \"dependentRequired\"", "type": "object", "additionalProperties": { "anyOf": [ { "$recursiveRef": "#" }, { "$ref": "meta/validation#/$defs/stringArray" } ] } } } }`) Draft2020.loadMeta("https://json-schema.org/draft/2020-12/schema", `{ "$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 } } }`) } var vocabSchemas = map[string]string{ "https://json-schema.org/draft/2019-09/meta/core": `{ "$schema": "https://json-schema.org/draft/2019-09/schema", "$id": "https://json-schema.org/draft/2019-09/meta/core", "$vocabulary": { "https://json-schema.org/draft/2019-09/vocab/core": true }, "$recursiveAnchor": true, "title": "Core vocabulary meta-schema", "type": ["object", "boolean"], "properties": { "$id": { "type": "string", "format": "uri-reference", "$comment": "Non-empty fragments not allowed.", "pattern": "^[^#]*#?$" }, "$schema": { "type": "string", "format": "uri" }, "$anchor": { "type": "string", "pattern": "^[A-Za-z][-A-Za-z0-9.:_]*$" }, "$ref": { "type": "string", "format": "uri-reference" }, "$recursiveRef": { "type": "string", "format": "uri-reference" }, "$recursiveAnchor": { "type": "boolean", "default": false }, "$vocabulary": { "type": "object", "propertyNames": { "type": "string", "format": "uri" }, "additionalProperties": { "type": "boolean" } }, "$comment": { "type": "string" }, "$defs": { "type": "object", "additionalProperties": { "$recursiveRef": "#" }, "default": {} } } }`, "https://json-schema.org/draft/2019-09/meta/applicator": `{ "$schema": "https://json-schema.org/draft/2019-09/schema", "$id": "https://json-schema.org/draft/2019-09/meta/applicator", "$vocabulary": { "https://json-schema.org/draft/2019-09/vocab/applicator": true }, "$recursiveAnchor": true, "title": "Applicator vocabulary meta-schema", "type": ["object", "boolean"], "properties": { "additionalItems": { "$recursiveRef": "#" }, "unevaluatedItems": { "$recursiveRef": "#" }, "items": { "anyOf": [ { "$recursiveRef": "#" }, { "$ref": "#/$defs/schemaArray" } ] }, "contains": { "$recursiveRef": "#" }, "additionalProperties": { "$recursiveRef": "#" }, "unevaluatedProperties": { "$recursiveRef": "#" }, "properties": { "type": "object", "additionalProperties": { "$recursiveRef": "#" }, "default": {} }, "patternProperties": { "type": "object", "additionalProperties": { "$recursiveRef": "#" }, "propertyNames": { "format": "regex" }, "default": {} }, "dependentSchemas": { "type": "object", "additionalProperties": { "$recursiveRef": "#" } }, "propertyNames": { "$recursiveRef": "#" }, "if": { "$recursiveRef": "#" }, "then": { "$recursiveRef": "#" }, "else": { "$recursiveRef": "#" }, "allOf": { "$ref": "#/$defs/schemaArray" }, "anyOf": { "$ref": "#/$defs/schemaArray" }, "oneOf": { "$ref": "#/$defs/schemaArray" }, "not": { "$recursiveRef": "#" } }, "$defs": { "schemaArray": { "type": "array", "minItems": 1, "items": { "$recursiveRef": "#" } } } }`, "https://json-schema.org/draft/2019-09/meta/validation": `{ "$schema": "https://json-schema.org/draft/2019-09/schema", "$id": "https://json-schema.org/draft/2019-09/meta/validation", "$vocabulary": { "https://json-schema.org/draft/2019-09/vocab/validation": true }, "$recursiveAnchor": true, "title": "Validation vocabulary meta-schema", "type": ["object", "boolean"], "properties": { "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" } }, "const": true, "enum": { "type": "array", "items": true }, "type": { "anyOf": [ { "$ref": "#/$defs/simpleTypes" }, { "type": "array", "items": { "$ref": "#/$defs/simpleTypes" }, "minItems": 1, "uniqueItems": true } ] } }, "$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": [] } } }`, "https://json-schema.org/draft/2019-09/meta/meta-data": `{ "$schema": "https://json-schema.org/draft/2019-09/schema", "$id": "https://json-schema.org/draft/2019-09/meta/meta-data", "$vocabulary": { "https://json-schema.org/draft/2019-09/vocab/meta-data": true }, "$recursiveAnchor": true, "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 } } }`, "https://json-schema.org/draft/2019-09/meta/format": `{ "$schema": "https://json-schema.org/draft/2019-09/schema", "$id": "https://json-schema.org/draft/2019-09/meta/format", "$vocabulary": { "https://json-schema.org/draft/2019-09/vocab/format": true }, "$recursiveAnchor": true, "title": "Format vocabulary meta-schema", "type": ["object", "boolean"], "properties": { "format": { "type": "string" } } }`, "https://json-schema.org/draft/2019-09/meta/content": `{ "$schema": "https://json-schema.org/draft/2019-09/schema", "$id": "https://json-schema.org/draft/2019-09/meta/content", "$vocabulary": { "https://json-schema.org/draft/2019-09/vocab/content": true }, "$recursiveAnchor": true, "title": "Content vocabulary meta-schema", "type": ["object", "boolean"], "properties": { "contentMediaType": { "type": "string" }, "contentEncoding": { "type": "string" }, "contentSchema": { "$recursiveRef": "#" } } }`, "https://json-schema.org/draft/2020-12/meta/core": `{ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://json-schema.org/draft/2020-12/meta/core", "$vocabulary": { "https://json-schema.org/draft/2020-12/vocab/core": true }, "$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" } } }`, "https://json-schema.org/draft/2020-12/meta/applicator": `{ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://json-schema.org/draft/2020-12/meta/applicator", "$vocabulary": { "https://json-schema.org/draft/2020-12/vocab/applicator": true }, "$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" } } } }`, "https://json-schema.org/draft/2020-12/meta/unevaluated": `{ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://json-schema.org/draft/2020-12/meta/unevaluated", "$vocabulary": { "https://json-schema.org/draft/2020-12/vocab/unevaluated": true }, "$dynamicAnchor": "meta", "title": "Unevaluated applicator vocabulary meta-schema", "type": ["object", "boolean"], "properties": { "unevaluatedItems": { "$dynamicRef": "#meta" }, "unevaluatedProperties": { "$dynamicRef": "#meta" } } }`, "https://json-schema.org/draft/2020-12/meta/validation": `{ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://json-schema.org/draft/2020-12/meta/validation", "$vocabulary": { "https://json-schema.org/draft/2020-12/vocab/validation": true }, "$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": [] } } }`, "https://json-schema.org/draft/2020-12/meta/meta-data": `{ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://json-schema.org/draft/2020-12/meta/meta-data", "$vocabulary": { "https://json-schema.org/draft/2020-12/vocab/meta-data": true }, "$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 } } }`, "https://json-schema.org/draft/2020-12/meta/format-annotation": `{ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://json-schema.org/draft/2020-12/meta/format-annotation", "$vocabulary": { "https://json-schema.org/draft/2020-12/vocab/format-annotation": true }, "$dynamicAnchor": "meta", "title": "Format vocabulary meta-schema for annotation results", "type": ["object", "boolean"], "properties": { "format": { "type": "string" } } }`, "https://json-schema.org/draft/2020-12/meta/format-assertion": `{ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://json-schema.org/draft/2020-12/meta/format-assertion", "$vocabulary": { "https://json-schema.org/draft/2020-12/vocab/format-assertion": true }, "$dynamicAnchor": "meta", "title": "Format vocabulary meta-schema for assertion results", "type": ["object", "boolean"], "properties": { "format": { "type": "string" } } }`, "https://json-schema.org/draft/2020-12/meta/content": `{ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://json-schema.org/draft/2020-12/meta/content", "$vocabulary": { "https://json-schema.org/draft/2020-12/vocab/content": true }, "$dynamicAnchor": "meta", "title": "Content vocabulary meta-schema", "type": ["object", "boolean"], "properties": { "contentEncoding": { "type": "string" }, "contentMediaType": { "type": "string" }, "contentSchema": { "$dynamicRef": "#meta" } } }`, } func clone(m map[string]position) map[string]position { mm := make(map[string]position) for k, v := range m { mm[k] = v } return mm } jsonschema-5.3.1/errors.go000066400000000000000000000070141445702222200155170ustar00rootroot00000000000000package jsonschema import ( "fmt" "strings" ) // InvalidJSONTypeError is the error type returned by ValidateInterface. // this tells that specified go object is not valid jsonType. type InvalidJSONTypeError string func (e InvalidJSONTypeError) Error() string { return fmt.Sprintf("jsonschema: invalid jsonType: %s", string(e)) } // InfiniteLoopError is returned by Compile/Validate. // this gives url#keywordLocation that lead to infinity loop. type InfiniteLoopError string func (e InfiniteLoopError) Error() string { return "jsonschema: infinite loop " + string(e) } func infiniteLoopError(stack []schemaRef, sref schemaRef) InfiniteLoopError { var path string for _, ref := range stack { if path == "" { path += ref.schema.Location } else { path += "/" + ref.path } } return InfiniteLoopError(path + "/" + sref.path) } // SchemaError is the error type returned by Compile. type SchemaError struct { // SchemaURL is the url to json-schema that filed to compile. // This is helpful, if your schema refers to external schemas SchemaURL string // Err is the error that occurred during compilation. // It could be ValidationError, because compilation validates // given schema against the json meta-schema Err error } func (se *SchemaError) Unwrap() error { return se.Err } func (se *SchemaError) Error() string { s := fmt.Sprintf("jsonschema %s compilation failed", se.SchemaURL) if se.Err != nil { return fmt.Sprintf("%s: %v", s, strings.TrimPrefix(se.Err.Error(), "jsonschema: ")) } return s } func (se *SchemaError) GoString() string { if _, ok := se.Err.(*ValidationError); ok { return fmt.Sprintf("jsonschema %s compilation failed\n%#v", se.SchemaURL, se.Err) } return se.Error() } // ValidationError is the error type returned by Validate. type ValidationError struct { KeywordLocation string // validation path of validating keyword or schema AbsoluteKeywordLocation string // absolute location of validating keyword or schema InstanceLocation string // location of the json value within the instance being validated Message string // describes error Causes []*ValidationError // nested validation errors } func (ve *ValidationError) add(causes ...error) error { for _, cause := range causes { ve.Causes = append(ve.Causes, cause.(*ValidationError)) } return ve } func (ve *ValidationError) causes(err error) error { if err := err.(*ValidationError); err.Message == "" { ve.Causes = err.Causes } else { ve.add(err) } return ve } func (ve *ValidationError) Error() string { leaf := ve for len(leaf.Causes) > 0 { leaf = leaf.Causes[0] } u, _ := split(ve.AbsoluteKeywordLocation) return fmt.Sprintf("jsonschema: %s does not validate with %s: %s", quote(leaf.InstanceLocation), u+"#"+leaf.KeywordLocation, leaf.Message) } func (ve *ValidationError) GoString() string { sloc := ve.AbsoluteKeywordLocation sloc = sloc[strings.IndexByte(sloc, '#')+1:] msg := fmt.Sprintf("[I#%s] [S#%s] %s", ve.InstanceLocation, sloc, ve.Message) for _, c := range ve.Causes { for _, line := range strings.Split(c.GoString(), "\n") { msg += "\n " + line } } return msg } func joinPtr(ptr1, ptr2 string) string { if len(ptr1) == 0 { return ptr2 } if len(ptr2) == 0 { return ptr1 } return ptr1 + "/" + ptr2 } // quote returns single-quoted string func quote(s string) string { s = fmt.Sprintf("%q", s) s = strings.ReplaceAll(s, `\"`, `"`) s = strings.ReplaceAll(s, `'`, `\'`) return "'" + s[1:len(s)-1] + "'" } jsonschema-5.3.1/example_extension_test.go000066400000000000000000000030771445702222200207760ustar00rootroot00000000000000package jsonschema_test import ( "encoding/json" "fmt" "log" "strconv" "strings" "github.com/santhosh-tekuri/jsonschema/v5" ) var powerOfMeta = jsonschema.MustCompileString("powerOf.json", `{ "properties" : { "powerOf": { "type": "integer", "exclusiveMinimum": 0 } } }`) type powerOfCompiler struct{} func (powerOfCompiler) Compile(ctx jsonschema.CompilerContext, m map[string]interface{}) (jsonschema.ExtSchema, error) { if pow, ok := m["powerOf"]; ok { n, err := pow.(json.Number).Int64() return powerOfSchema(n), err } // nothing to compile, return nil return nil, nil } type powerOfSchema int64 func (s powerOfSchema) Validate(ctx jsonschema.ValidationContext, v interface{}) error { switch v.(type) { case json.Number, float32, float64, int, int8, int32, int64, uint, uint8, uint32, uint64: pow := int64(s) n, _ := strconv.ParseInt(fmt.Sprint(v), 10, 64) for n%pow == 0 { n = n / pow } if n != 1 { return ctx.Error("powerOf", "%v not powerOf %v", v, pow) } return nil default: return nil } } func Example_extension() { c := jsonschema.NewCompiler() c.RegisterExtension("powerOf", powerOfMeta, powerOfCompiler{}) schema := `{"powerOf": 10}` instance := `100` if err := c.AddResource("schema.json", strings.NewReader(schema)); err != nil { log.Fatal(err) } sch, err := c.Compile("schema.json") if err != nil { log.Fatalf("%#v", err) } var v interface{} if err := json.Unmarshal([]byte(instance), &v); err != nil { log.Fatal(err) } if err = sch.Validate(v); err != nil { log.Fatalf("%#v", err) } // Output: } jsonschema-5.3.1/example_test.go000066400000000000000000000101751445702222200166770ustar00rootroot00000000000000package jsonschema_test import ( "encoding/hex" "encoding/json" "encoding/xml" "fmt" "io" "io/ioutil" "log" "strconv" "strings" "github.com/santhosh-tekuri/jsonschema/v5" ) func Example() { sch, err := jsonschema.Compile("testdata/person_schema.json") if err != nil { log.Fatalf("%#v", err) } data, err := ioutil.ReadFile("testdata/person.json") if err != nil { log.Fatal(err) } var v interface{} if err := json.Unmarshal(data, &v); err != nil { log.Fatal(err) } if err = sch.Validate(v); err != nil { log.Fatalf("%#v", err) } // Output: } // Example_fromString shows how to load schema from string. func Example_fromString() { schema := `{"type": "object"}` instance := `{"foo": "bar"}` sch, err := jsonschema.CompileString("schema.json", schema) if err != nil { log.Fatalf("%#v", err) } var v interface{} if err := json.Unmarshal([]byte(instance), &v); err != nil { log.Fatal(err) } if err = sch.Validate(v); err != nil { log.Fatalf("%#v", err) } // Output: } // Example_fromStrings shows how to load schema from more than one string. func Example_fromStrings() { c := jsonschema.NewCompiler() if err := c.AddResource("main.json", strings.NewReader(`{"$ref":"obj.json"}`)); err != nil { log.Fatal(err) } if err := c.AddResource("obj.json", strings.NewReader(`{"type":"object"}`)); err != nil { log.Fatal(err) } sch, err := c.Compile("main.json") if err != nil { log.Fatalf("%#v", err) } var v interface{} if err := json.Unmarshal([]byte("{}"), &v); err != nil { log.Fatal(err) } if err = sch.Validate(v); err != nil { log.Fatalf("%#v", err) } // Output: } // Example_userDefinedFormat shows how to define 'odd-number' format. func Example_userDefinedFormat() { c := jsonschema.NewCompiler() c.AssertFormat = true c.Formats["odd-number"] = func(v interface{}) bool { switch v := v.(type) { case json.Number, float32, float64, int, int8, int32, int64, uint, uint8, uint32, uint64: n, _ := strconv.ParseInt(fmt.Sprint(v), 10, 64) return n%2 != 0 default: return true } } schema := `{ "type": "integer", "format": "odd-number" }` instance := 5 if err := c.AddResource("schema.json", strings.NewReader(schema)); err != nil { log.Fatalf("%v", err) } sch, err := c.Compile("schema.json") if err != nil { log.Fatalf("%#v", err) } if err = sch.Validate(instance); err != nil { log.Fatalf("%#v", err) } // Output: } // Example_userDefinedContent shows how to define // "hex" contentEncoding and "application/xml" contentMediaType func Example_userDefinedContent() { c := jsonschema.NewCompiler() c.AssertContent = true c.Decoders["hex"] = hex.DecodeString c.MediaTypes["application/xml"] = func(b []byte) error { return xml.Unmarshal(b, new(interface{})) } schema := `{ "type": "object", "properties": { "xml" : { "type": "string", "contentEncoding": "hex", "contentMediaType": "application/xml" } } }` instance := `{"xml": "3c726f6f742f3e"}` if err := c.AddResource("schema.json", strings.NewReader(schema)); err != nil { log.Fatalf("%v", err) } sch, err := c.Compile("schema.json") if err != nil { log.Fatalf("%#v", err) } var v interface{} if err := json.Unmarshal([]byte(instance), &v); err != nil { log.Fatal(err) } if err = sch.Validate(v); err != nil { log.Fatalf("%#v", err) } // Output: } // Example_userDefinedLoader shows how to define custom schema loader. // // we are implementing a "map" protocol which servers schemas from // go map variable. func Example_userDefinedLoader() { var schemas = map[string]string{ "main.json": `{"$ref":"obj.json"}`, "obj.json": `{"type":"object"}`, } jsonschema.Loaders["map"] = func(url string) (io.ReadCloser, error) { schema, ok := schemas[strings.TrimPrefix(url, "map:///")] if !ok { return nil, fmt.Errorf("%q not found", url) } return ioutil.NopCloser(strings.NewReader(schema)), nil } sch, err := jsonschema.Compile("map:///main.json") if err != nil { log.Fatalf("%+v", err) } var v interface{} if err := json.Unmarshal([]byte("{}"), &v); err != nil { log.Fatal(err) } if err = sch.Validate(v); err != nil { log.Fatalf("%#v", err) } // Output: } jsonschema-5.3.1/extension.go000066400000000000000000000103421445702222200162150ustar00rootroot00000000000000package jsonschema // ExtCompiler compiles custom keyword(s) into ExtSchema. type ExtCompiler interface { // Compile compiles the custom keywords in schema m and returns its compiled representation. // if the schema m does not contain the keywords defined by this extension, // compiled representation nil should be returned. Compile(ctx CompilerContext, m map[string]interface{}) (ExtSchema, error) } // ExtSchema is schema representation of custom keyword(s) type ExtSchema interface { // Validate validates the json value v with this ExtSchema. // Returned error must be *ValidationError. Validate(ctx ValidationContext, v interface{}) error } type extension struct { meta *Schema compiler ExtCompiler } // RegisterExtension registers custom keyword(s) into this compiler. // // name is extension name, used only to avoid name collisions. // meta captures the metaschema for the new keywords. // This is used to validate the schema before calling ext.Compile. func (c *Compiler) RegisterExtension(name string, meta *Schema, ext ExtCompiler) { c.extensions[name] = extension{meta, ext} } // CompilerContext --- // CompilerContext provides additional context required in compiling for extension. type CompilerContext struct { c *Compiler r *resource stack []schemaRef res *resource } // Compile compiles given value at ptr into *Schema. This is useful in implementing // keyword like allOf/not/patternProperties. // // schPath is the relative-json-pointer to the schema to be compiled from parent schema. // // applicableOnSameInstance tells whether current schema and the given schema // are applied on same instance value. this is used to detect infinite loop in schema. func (ctx CompilerContext) Compile(schPath string, applicableOnSameInstance bool) (*Schema, error) { var stack []schemaRef if applicableOnSameInstance { stack = ctx.stack } return ctx.c.compileRef(ctx.r, stack, schPath, ctx.res, ctx.r.url+ctx.res.floc+"/"+schPath) } // CompileRef compiles the schema referenced by ref uri // // refPath is the relative-json-pointer to ref. // // applicableOnSameInstance tells whether current schema and the given schema // are applied on same instance value. this is used to detect infinite loop in schema. func (ctx CompilerContext) CompileRef(ref string, refPath string, applicableOnSameInstance bool) (*Schema, error) { var stack []schemaRef if applicableOnSameInstance { stack = ctx.stack } return ctx.c.compileRef(ctx.r, stack, refPath, ctx.res, ref) } // ValidationContext --- // ValidationContext provides additional context required in validating for extension. type ValidationContext struct { result validationResult validate func(sch *Schema, schPath string, v interface{}, vpath string) error validateInplace func(sch *Schema, schPath string) error validationError func(keywordPath string, format string, a ...interface{}) *ValidationError } // EvaluatedProp marks given property of object as evaluated. func (ctx ValidationContext) EvaluatedProp(prop string) { delete(ctx.result.unevalProps, prop) } // EvaluatedItem marks given index of array as evaluated. func (ctx ValidationContext) EvaluatedItem(index int) { delete(ctx.result.unevalItems, index) } // Validate validates schema s with value v. Extension must use this method instead of // *Schema.ValidateInterface method. This will be useful in implementing keywords like // allOf/oneOf // // spath is relative-json-pointer to s // vpath is relative-json-pointer to v. func (ctx ValidationContext) Validate(s *Schema, spath string, v interface{}, vpath string) error { if vpath == "" { return ctx.validateInplace(s, spath) } return ctx.validate(s, spath, v, vpath) } // Error used to construct validation error by extensions. // // keywordPath is relative-json-pointer to keyword. func (ctx ValidationContext) Error(keywordPath string, format string, a ...interface{}) *ValidationError { return ctx.validationError(keywordPath, format, a...) } // Group is used by extensions to group multiple errors as causes to parent error. // This is useful in implementing keywords like allOf where each schema specified // in allOf can result a validationError. func (ValidationError) Group(parent *ValidationError, causes ...error) error { return parent.add(causes...) } jsonschema-5.3.1/extension_test.go000066400000000000000000000024171445702222200172600ustar00rootroot00000000000000package jsonschema_test import ( "strings" "testing" "github.com/santhosh-tekuri/jsonschema/v5" ) func TestPowerOfExt(t *testing.T) { t.Run("invalidSchema", func(t *testing.T) { c := jsonschema.NewCompiler() c.RegisterExtension("powerOf", powerOfMeta, powerOfCompiler{}) if err := c.AddResource("test.json", strings.NewReader(`{"powerOf": "hello"}`)); err != nil { t.Fatal(err) } _, err := c.Compile("test.json") if err == nil { t.Fatal("error expected") } t.Log(err) }) t.Run("validSchema", func(t *testing.T) { c := jsonschema.NewCompiler() c.RegisterExtension("powerOf", powerOfMeta, powerOfCompiler{}) if err := c.AddResource("test.json", strings.NewReader(`{"powerOf": 10}`)); err != nil { t.Fatal(err) } sch, err := c.Compile("test.json") if err != nil { t.Fatal(err) } t.Run("validInstance", func(t *testing.T) { if err := sch.Validate(100); err != nil { t.Fatal(err) } }) t.Run("invalidInstance", func(t *testing.T) { if err := sch.Validate(111); err == nil { t.Fatal("validation must fail") } else { t.Logf("%#v", err) if !strings.Contains(err.(*jsonschema.ValidationError).GoString(), "111 not powerOf 10") { t.Fatal("validation error expected to contain powerOf message") } } }) }) } jsonschema-5.3.1/format.go000066400000000000000000000305121445702222200154720ustar00rootroot00000000000000package jsonschema import ( "errors" "net" "net/mail" "net/url" "regexp" "strconv" "strings" "time" ) // Formats is a registry of functions, which know how to validate // a specific format. // // New Formats can be registered by adding to this map. Key is format name, // value is function that knows how to validate that format. var Formats = map[string]func(interface{}) bool{ "date-time": isDateTime, "date": isDate, "time": isTime, "duration": isDuration, "period": isPeriod, "hostname": isHostname, "email": isEmail, "ip-address": isIPV4, "ipv4": isIPV4, "ipv6": isIPV6, "uri": isURI, "iri": isURI, "uri-reference": isURIReference, "uriref": isURIReference, "iri-reference": isURIReference, "uri-template": isURITemplate, "regex": isRegex, "json-pointer": isJSONPointer, "relative-json-pointer": isRelativeJSONPointer, "uuid": isUUID, } // isDateTime tells whether given string is a valid date representation // as defined by RFC 3339, section 5.6. // // see https://datatracker.ietf.org/doc/html/rfc3339#section-5.6, for details func isDateTime(v interface{}) bool { s, ok := v.(string) if !ok { return true } if len(s) < 20 { // yyyy-mm-ddThh:mm:ssZ return false } if s[10] != 'T' && s[10] != 't' { return false } return isDate(s[:10]) && isTime(s[11:]) } // isDate tells whether given string is a valid full-date production // as defined by RFC 3339, section 5.6. // // see https://datatracker.ietf.org/doc/html/rfc3339#section-5.6, for details func isDate(v interface{}) bool { s, ok := v.(string) if !ok { return true } _, err := time.Parse("2006-01-02", s) return err == nil } // isTime tells whether given string is a valid full-time production // as defined by RFC 3339, section 5.6. // // see https://datatracker.ietf.org/doc/html/rfc3339#section-5.6, for details func isTime(v interface{}) bool { str, ok := v.(string) if !ok { return true } // golang time package does not support leap seconds. // so we are parsing it manually here. // hh:mm:ss // 01234567 if len(str) < 9 || str[2] != ':' || str[5] != ':' { return false } isInRange := func(str string, min, max int) (int, bool) { n, err := strconv.Atoi(str) if err != nil { return 0, false } if n < min || n > max { return 0, false } return n, true } var h, m, s int if h, ok = isInRange(str[0:2], 0, 23); !ok { return false } if m, ok = isInRange(str[3:5], 0, 59); !ok { return false } if s, ok = isInRange(str[6:8], 0, 60); !ok { return false } str = str[8:] // parse secfrac if present if str[0] == '.' { // dot following more than one digit str = str[1:] var numDigits int for str != "" { if str[0] < '0' || str[0] > '9' { break } numDigits++ str = str[1:] } if numDigits == 0 { return false } } if len(str) == 0 { return false } if str[0] == 'z' || str[0] == 'Z' { if len(str) != 1 { return false } } else { // time-numoffset // +hh:mm // 012345 if len(str) != 6 || str[3] != ':' { return false } var sign int if str[0] == '+' { sign = -1 } else if str[0] == '-' { sign = +1 } else { return false } var zh, zm int if zh, ok = isInRange(str[1:3], 0, 23); !ok { return false } if zm, ok = isInRange(str[4:6], 0, 59); !ok { return false } // apply timezone offset hm := (h*60 + m) + sign*(zh*60+zm) if hm < 0 { hm += 24 * 60 } h, m = hm/60, hm%60 } // check leapsecond if s == 60 { // leap second if h != 23 || m != 59 { return false } } return true } // isDuration tells whether given string is a valid duration format // from the ISO 8601 ABNF as given in Appendix A of RFC 3339. // // see https://datatracker.ietf.org/doc/html/rfc3339#appendix-A, for details func isDuration(v interface{}) bool { s, ok := v.(string) if !ok { return true } if len(s) == 0 || s[0] != 'P' { return false } s = s[1:] parseUnits := func() (units string, ok bool) { for len(s) > 0 && s[0] != 'T' { digits := false for { if len(s) == 0 { break } if s[0] < '0' || s[0] > '9' { break } digits = true s = s[1:] } if !digits || len(s) == 0 { return units, false } units += s[:1] s = s[1:] } return units, true } units, ok := parseUnits() if !ok { return false } if units == "W" { return len(s) == 0 // P_W } if len(units) > 0 { if strings.Index("YMD", units) == -1 { return false } if len(s) == 0 { return true // "P" dur-date } } if len(s) == 0 || s[0] != 'T' { return false } s = s[1:] units, ok = parseUnits() return ok && len(s) == 0 && len(units) > 0 && strings.Index("HMS", units) != -1 } // isPeriod tells whether given string is a valid period format // from the ISO 8601 ABNF as given in Appendix A of RFC 3339. // // see https://datatracker.ietf.org/doc/html/rfc3339#appendix-A, for details func isPeriod(v interface{}) bool { s, ok := v.(string) if !ok { return true } slash := strings.IndexByte(s, '/') if slash == -1 { return false } start, end := s[:slash], s[slash+1:] if isDateTime(start) { return isDateTime(end) || isDuration(end) } return isDuration(start) && isDateTime(end) } // isHostname tells whether given string is a valid representation // for an Internet host name, as defined by RFC 1034 section 3.1 and // RFC 1123 section 2.1. // // See https://en.wikipedia.org/wiki/Hostname#Restrictions_on_valid_host_names, for details. func isHostname(v interface{}) bool { s, ok := v.(string) if !ok { return true } // entire hostname (including the delimiting dots but not a trailing dot) has a maximum of 253 ASCII characters s = strings.TrimSuffix(s, ".") if len(s) > 253 { return false } // Hostnames are composed of series of labels concatenated with dots, as are all domain names for _, label := range strings.Split(s, ".") { // Each label must be from 1 to 63 characters long if labelLen := len(label); labelLen < 1 || labelLen > 63 { return false } // labels must not start with a hyphen // RFC 1123 section 2.1: restriction on the first character // is relaxed to allow either a letter or a digit if first := s[0]; first == '-' { return false } // must not end with a hyphen if label[len(label)-1] == '-' { return false } // labels may contain only the ASCII letters 'a' through 'z' (in a case-insensitive manner), // the digits '0' through '9', and the hyphen ('-') for _, c := range label { if valid := (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || (c == '-'); !valid { return false } } } return true } // isEmail tells whether given string is a valid Internet email address // as defined by RFC 5322, section 3.4.1. // // See https://en.wikipedia.org/wiki/Email_address, for details. func isEmail(v interface{}) bool { s, ok := v.(string) if !ok { return true } // entire email address to be no more than 254 characters long if len(s) > 254 { return false } // email address is generally recognized as having two parts joined with an at-sign at := strings.LastIndexByte(s, '@') if at == -1 { return false } local := s[0:at] domain := s[at+1:] // local part may be up to 64 characters long if len(local) > 64 { return false } // domain if enclosed in brackets, must match an IP address if len(domain) >= 2 && domain[0] == '[' && domain[len(domain)-1] == ']' { ip := domain[1 : len(domain)-1] if strings.HasPrefix(ip, "IPv6:") { return isIPV6(strings.TrimPrefix(ip, "IPv6:")) } return isIPV4(ip) } // domain must match the requirements for a hostname if !isHostname(domain) { return false } _, err := mail.ParseAddress(s) return err == nil } // isIPV4 tells whether given string is a valid representation of an IPv4 address // according to the "dotted-quad" ABNF syntax as defined in RFC 2673, section 3.2. func isIPV4(v interface{}) bool { s, ok := v.(string) if !ok { return true } groups := strings.Split(s, ".") if len(groups) != 4 { return false } for _, group := range groups { n, err := strconv.Atoi(group) if err != nil { return false } if n < 0 || n > 255 { return false } if n != 0 && group[0] == '0' { return false // leading zeroes should be rejected, as they are treated as octals } } return true } // isIPV6 tells whether given string is a valid representation of an IPv6 address // as defined in RFC 2373, section 2.2. func isIPV6(v interface{}) bool { s, ok := v.(string) if !ok { return true } if !strings.Contains(s, ":") { return false } return net.ParseIP(s) != nil } // isURI tells whether given string is valid URI, according to RFC 3986. func isURI(v interface{}) bool { s, ok := v.(string) if !ok { return true } u, err := urlParse(s) return err == nil && u.IsAbs() } func urlParse(s string) (*url.URL, error) { u, err := url.Parse(s) if err != nil { return nil, err } // if hostname is ipv6, validate it hostname := u.Hostname() if strings.IndexByte(hostname, ':') != -1 { if strings.IndexByte(u.Host, '[') == -1 || strings.IndexByte(u.Host, ']') == -1 { return nil, errors.New("ipv6 address is not enclosed in brackets") } if !isIPV6(hostname) { return nil, errors.New("invalid ipv6 address") } } return u, nil } // isURIReference tells whether given string is a valid URI Reference // (either a URI or a relative-reference), according to RFC 3986. func isURIReference(v interface{}) bool { s, ok := v.(string) if !ok { return true } _, err := urlParse(s) return err == nil && !strings.Contains(s, `\`) } // isURITemplate tells whether given string is a valid URI Template // according to RFC6570. // // Current implementation does minimal validation. func isURITemplate(v interface{}) bool { s, ok := v.(string) if !ok { return true } u, err := urlParse(s) if err != nil { return false } for _, item := range strings.Split(u.RawPath, "/") { depth := 0 for _, ch := range item { switch ch { case '{': depth++ if depth != 1 { return false } case '}': depth-- if depth != 0 { return false } } } if depth != 0 { return false } } return true } // isRegex tells whether given string is a valid regular expression, // according to the ECMA 262 regular expression dialect. // // The implementation uses go-lang regexp package. func isRegex(v interface{}) bool { s, ok := v.(string) if !ok { return true } _, err := regexp.Compile(s) return err == nil } // isJSONPointer tells whether given string is a valid JSON Pointer. // // Note: It returns false for JSON Pointer URI fragments. func isJSONPointer(v interface{}) bool { s, ok := v.(string) if !ok { return true } if s != "" && !strings.HasPrefix(s, "/") { return false } for _, item := range strings.Split(s, "/") { for i := 0; i < len(item); i++ { if item[i] == '~' { if i == len(item)-1 { return false } switch item[i+1] { case '0', '1': // valid default: return false } } } } return true } // isRelativeJSONPointer tells whether given string is a valid Relative JSON Pointer. // // see https://tools.ietf.org/html/draft-handrews-relative-json-pointer-01#section-3 func isRelativeJSONPointer(v interface{}) bool { s, ok := v.(string) if !ok { return true } if s == "" { return false } if s[0] == '0' { s = s[1:] } else if s[0] >= '0' && s[0] <= '9' { for s != "" && s[0] >= '0' && s[0] <= '9' { s = s[1:] } } else { return false } return s == "#" || isJSONPointer(s) } // isUUID tells whether given string is a valid uuid format // as specified in RFC4122. // // see https://datatracker.ietf.org/doc/html/rfc4122#page-4, for details func isUUID(v interface{}) bool { s, ok := v.(string) if !ok { return true } parseHex := func(n int) bool { for n > 0 { if len(s) == 0 { return false } hex := (s[0] >= '0' && s[0] <= '9') || (s[0] >= 'a' && s[0] <= 'f') || (s[0] >= 'A' && s[0] <= 'F') if !hex { return false } s = s[1:] n-- } return true } groups := []int{8, 4, 4, 4, 12} for i, numDigits := range groups { if !parseHex(numDigits) { return false } if i == len(groups)-1 { break } if len(s) == 0 || s[0] != '-' { return false } s = s[1:] } return len(s) == 0 } jsonschema-5.3.1/format_test.go000066400000000000000000000333401445702222200165330ustar00rootroot00000000000000package jsonschema import ( "strings" "testing" ) type test struct { str string valid bool } func TestFormatsNonString(t *testing.T) { for name, check := range Formats { if !check(1) { t.Errorf("%s: want true, got false", name) } } } func TestIsDateTime(t *testing.T) { tests := []test{ {"1963-06-19T08:30:06.283185Z", true}, // with second fraction {"1963-06-19T08:30:06Z", true}, // without second fraction {"1937-01-01T12:00:27.87+00:20", true}, // with plus offset {"1990-12-31T15:59:50.123-08:00", true}, // with minus offset {"1990-02-31T15:59:60.123-08:00", false}, // invalid day {"1990-12-31T15:59:60-24:00", false}, // invalid offset {"06/19/1963 08:30:06 PST", false}, // invalid date delimiters {"1963-06-19t08:30:06.283185z", true}, // case-insensitive T and Z {"2013-350T01:01:01", false}, // invalid: only RFC3339 not all of ISO 8601 are valid {"1963-6-19T08:30:06.283185Z", false}, // invalid: non-padded month {"1963-06-1T08:30:06.283185Z", false}, // invalid: non-padded day {"1985-04-12T23:20:50.52Z", true}, {"1996-12-19T16:39:57-08:00", true}, {"1990-12-31T23:59:59Z", true}, {"1990-12-31T15:59:59-08:00", true}, } for i, test := range tests { if test.valid != isDateTime(test.str) { t.Errorf("#%d: %q, valid %t, got valid %t", i, test.str, test.valid, !test.valid) } } } func TestIsDate(t *testing.T) { tests := []test{ {"1963-06-19", true}, {"2020-01-31", true}, // valid: 31 days in January {"2020-01-32", false}, // invalid: 32 days in January {"2021-02-28", true}, // valid: 28 days in February (normal) {"2021-02-29", false}, // invalid: 29 days in February (normal) {"2020-02-29", true}, // valid: 29 days in February (leap) {"2020-02-30", false}, // invalid: 30 days in February (leap) {"2020-03-31", true}, // valid: 31 days in March {"2020-03-32", false}, // invalid: 32 days in March {"2020-04-30", true}, // valid: 30 days in April {"2020-04-31", false}, // invalid: 31 days in April {"2020-05-31", true}, // valid: 31 days in May {"2020-05-32", false}, // invalid: 32 days in May {"2020-06-30", true}, // valid: 30 days in June {"2020-06-31", false}, // invalid: 31 days in June {"2020-07-31", true}, // valid: 31 days in July {"2020-07-32", false}, // invalid: 32 days in July {"2020-08-31", true}, // valid: 31 days in August {"2020-08-32", false}, // invalid: 32 days in August {"2020-09-30", true}, // valid: 30 days in September {"2020-09-31", false}, // invalid: 31 days in September {"2020-10-31", true}, // valid: 31 days in October {"2020-10-32", false}, // invalid: 32 days in October {"2020-11-30", true}, // valid: 30 days in November {"2020-11-31", false}, // invalid: 31 days in November {"2020-12-31", true}, // valid: 31 days in December {"2020-12-32", false}, // invalid: 32 days in December {"2020-13-01", false}, // invalid month {"06/19/1963", false}, // invalid: wrong delimiters {"2013-350", false}, // invalid: only RFC3339 not all of ISO 8601 are valid {"1998-1-20", false}, // invalid: non-padded month {"1998-01-1", false}, // invalid: non-padded day } for i, test := range tests { if test.valid != isDate(test.str) { t.Errorf("#%d: %q, valid %t, got valid %t", i, test.str, test.valid, !test.valid) } } } func TestIsTime(t *testing.T) { tests := []test{ {"08:30:06.283185Z", true}, {"08:30:06 PST", false}, {"01:01:01,1111", false}, // only RFC3339 not all of ISO 8601 are valid {"23:59:60Z", true}, // with leap second {"15:59:60-08:00", true}, // with leap second with offset {"23:20:50.52Z", true}, // with second fraction {"08:30:06.283185Z", true}, // with precise second fraction {"23:20:50.Z", false}, // invalid (no digit after dot in second fraction) {"08:30:06+00:20", true}, // with plus offset {"08:30:06-08:00", true}, // with minus offset {"08:30:06z", true}, // with case-insensitive Z {"24:00:00Z", false}, // invalid hour {"00:60:00Z", false}, // invalid minute {"00:00:61Z", false}, // invalid second {"22:59:60Z", false}, // invalid leap second (wrong hour) {"23:58:60Z", false}, // invalid leap second (wrong minute) {"01:02:03+24:00", false}, // invalid time numoffset hour {"01:02:03+00:60", false}, // invalid time numoffset minute {"01:02:03Z+00:30", false}, // invalid time with both Z and numoffset {"01:29:60+01:30", true}, // leap second, positive time-offset {"12:00:00.52", false}, // no time offset with second fraction {"1২:00:00Z", false}, // invalid non-ASCII '২' (a Bengali 2) {"08:30:06#00:20", false}, // offset not starting with plus or minus {"ab:cd:efz", false}, // contains letters } for i, test := range tests { if test.valid != isTime(test.str) { t.Errorf("#%d: %q, valid %t, got valid %t", i, test.str, test.valid, !test.valid) } } } func TestIsDuration(t *testing.T) { tests := []test{ {"P4DT12H30M5S", true}, {"PT1D", false}, // invalid: days after 'T' {"P", false}, // invalid: no elements {"P1YT", false}, // invalid: no time elements after 'T' {"PT", false}, // invalid: no date or time elements {"P2D1Y", false}, // invalid: elements out of order {"P1D2H", false}, // invalid: missing time separator {"P2S", false}, // invalid: time element in the date position {"P4Y", true}, // valid: four years duration {"PT0S", true}, // valid: zero time, in seconds {"P0D", true}, // valid: zero time, in days {"P1M", true}, // valid: one month duration {"PT1M", true}, // valid: one minute duration {"PT36H", true}, // valid: one and a half days, in hours {"P1DT12H", true}, // valid: one and a half days, in days and hours {"P2W", true}, // valid: two weeks {"P1Y2W", false}, // invalid: weeks cannot be combined with other units {"P1", false}, // element without unit } for i, test := range tests { if test.valid != isDuration(test.str) { t.Errorf("#%d: %q, valid %t, got valid %t", i, test.str, test.valid, !test.valid) } } } func TestIsPeriod(t *testing.T) { dt := "1963-06-19T08:30:06Z" dur := "P4DT12H30M5S" tests := []test{ {dt + "/" + dt, true}, // period-explicit {dt + "/" + dur, true}, // period-start {dur + "/" + dt, true}, // period-end {dur + "/" + dur, false}, {dt, false}, {dur, false}, {dt + "/" + dt + "/" + dt, false}, {dt + " " + dt, false}, {dt + "-" + dt, false}, {"foo/bar", false}, {"", false}, {"/", false}, } for i, test := range tests { if test.valid != isPeriod(test.str) { t.Errorf("#%d: %q, valid %t, got valid %t", i, test.str, test.valid, !test.valid) } } } func TestIsHostname(t *testing.T) { tests := []test{ {"www.example.com", true}, {strings.Repeat("a", 63) + "." + strings.Repeat("a", 63) + "." + strings.Repeat("a", 63) + "." + strings.Repeat("a", 61), true}, {strings.Repeat("a", 63) + "." + strings.Repeat("a", 63) + "." + strings.Repeat("a", 63) + "." + strings.Repeat("a", 61) + ".", true}, {strings.Repeat("a", 63) + "." + strings.Repeat("a", 63) + "." + strings.Repeat("a", 63) + "." + strings.Repeat("a", 62) + ".", false}, // length more than 253 characters long {"www..com", false}, // empty label {"-a-host-name-that-starts-with--", false}, {"not_a_valid_host_name", false}, {"a-vvvvvvvvvvvvvvvveeeeeeeeeeeeeeeerrrrrrrrrrrrrrrryyyyyyyyyyyyyyyy-long-host-name-component", false}, {"www.example-.com", false}, // label ends with a hyphen } for i, test := range tests { if test.valid != isHostname(test.str) { t.Errorf("#%d: %q, valid %t, got valid %t", i, test.str, test.valid, !test.valid) } } } func TestIsEmail(t *testing.T) { tests := []test{ {"joe.bloggs@example.com", true}, {"2962", false}, // no "@" character {strings.Repeat("a", 244) + "@google.com", false}, // more than 254 characters long {strings.Repeat("a", 65) + "@google.com", false}, // local part more than 64 characters long {"santhosh@-google.com", false}, // invalid domain name } for i, test := range tests { if test.valid != isEmail(test.str) { t.Errorf("#%d: %q, valid %t, got valid %t", i, test.str, test.valid, !test.valid) } } } func TestIsIPV4(t *testing.T) { tests := []test{ {"192.168.0.1", true}, {"192.168.0.test", false}, // non-integer component {"127.0.0.0.1", false}, // too many components {"256.256.256.256", false}, // out-of-range values {"127.0", false}, // without 4 components {"0x7f000001", false}, // an integer } for i, test := range tests { if test.valid != isIPV4(test.str) { t.Errorf("#%d: %q, valid %t, got valid %t", i, test.str, test.valid, !test.valid) } } } func TestIsIPV6(t *testing.T) { tests := []test{ {"::1", true}, {"192.168.0.1", false}, // is IPV4 {"12345::", false}, // out-of-range values {"1:1:1:1:1:1:1:1:1:1:1:1:1:1:1:1", false}, // too many components {"::laptop", false}, // containing illegal characters } for i, test := range tests { if test.valid != isIPV6(test.str) { t.Errorf("#%d: %q, valid %t, got valid %t", i, test.str, test.valid, !test.valid) } } } func TestIsURI(t *testing.T) { tests := []test{ {"http://foo.bar/?baz=qux#quux", true}, {"//foo.bar/?baz=qux#quux", false}, // an invalid protocol-relative URI Reference {"\\\\WINDOWS\\fileshare", false}, // an invalid URI {"abc", false}, // an invalid URI though valid URI reference } for i, test := range tests { if test.valid != isURI(test.str) { t.Errorf("#%d: %q, valid %t, got valid %t", i, test.str, test.valid, !test.valid) } } } func TestIsURITemplate(t *testing.T) { tests := []test{ {"http://example.com/dictionary/{term:1}/{term}", true}, {"http://example.com/dictionary/{term:1}/{term", false}, {"http://example.com/dictionary", true}, // without variables {"dictionary/{term:1}/{term}", true}, // relative url-template } for i, test := range tests { if test.valid != isURITemplate(test.str) { t.Errorf("#%d: %q, valid %t, got valid %t", i, test.str, test.valid, !test.valid) } } } func TestIsRegex(t *testing.T) { tests := []test{ {"([abc])+\\s+$", true}, {"^(abc]", false}, // unclosed parenthesis } for i, test := range tests { if test.valid != isRegex(test.str) { t.Errorf("#%d: %q, valid %t, got valid %t", i, test.str, test.valid, !test.valid) } } } func TestIsJSONPointer(t *testing.T) { tests := []test{ {"/foo/bar~0/baz~1/%a", true}, {"/foo/baz~", false}, // ~ not escaped {"/foo//bar", true}, // empty segment {"/foo/bar/", true}, // last empty segment {"", true}, // empty {"/foo", true}, {"/foo/0", true}, {"/ ", true}, {"/a~1b", true}, {"/c%d", true}, {"/e^f", true}, {"/g|h", true}, {"/i\\j", true}, {"/k\"l", true}, {"/ ", true}, {"/m~0n", true}, {"/foo/-", true}, // used adding to the last array position {"/foo/-/bar", true}, // - used as object member name {"/~1~0~0~1~1", true}, // multiple escaped characters // escaped with fraction part {"/~1.1", true}, {"/~0.1", true}, // URI Fragment Identifier {"#", false}, {"#/", false}, {"#a", false}, // some escaped, but not all {"/~0~", false}, {"/~0/~", false}, // wrong escape character {"/~2", false}, {"/~-1", false}, {"/~~", false}, // multiple characters not escaped // isn't empty nor starts with / {"a", false}, {"0", false}, {"a/a", false}, } for i, test := range tests { if test.valid != isJSONPointer(test.str) { t.Errorf("#%d: %q, valid %t, got valid %t", i, test.str, test.valid, !test.valid) } } } func TestRelativeJSONPointer(t *testing.T) { tests := []test{ {"1", true}, // upwards RJP {"0/foo/bar", true}, // downwards RJP {"2/0/baz/1/zip", true}, // up and then down RJP, with array index {"0#", true}, // taking the member or index name {"/foo/bar", false}, // valid json-pointer, but invalid RJP {"-1/foo/bar", false}, // negative prefix {"0##", false}, // ## is not a valid json-pointer {"01/a", false}, // zero cannot be followed by other digits, plus json-pointer {"01#", false}, // zero cannot be followed by other digits, plus octothorpe {"", false}, // empty string } for i, test := range tests { if test.valid != isRelativeJSONPointer(test.str) { t.Errorf("#%d: %q, valid %t, got valid %t", i, test.str, test.valid, !test.valid) } } } func TestIsUUID(t *testing.T) { tests := []test{ {"2EB8AA08-AA98-11EA-B4AA-73B441D16380", true}, // valid: all upper-case {"2eb8aa08-aa98-11ea-b4aa-73b441d16380", true}, // valid: all lower-case {"2eb8aa08-AA98-11ea-B4Aa-73B441D16380", true}, // valid: mixed case {"00000000-0000-0000-0000-000000000000", true}, // valid: all zeroes {"2eb8aa08-aa98-11ea-b4aa-73b441d1638", false}, // invalid: wrong length {"2eb8aa08-aa98-11ea-73b441d16380", false}, // invalid: missing section {"2eb8aa08-aa98-11ea-b4ga-73b441d16380", false}, // invalid: bad characters (not hex) {"2eb8aa08aa9811eab4aa73b441d16380", false}, // invalid: no dashes {"2eb8aa08aa98-11ea-b4aa73b441d16380", false}, // too few dashes {"2eb8-aa08-aa98-11ea-b4aa73b44-1d16380", false}, // too many dashes {"2eb8aa08aa9811eab4aa73b441d16380----", false}, // dashes in the wrong spot {"98d80576-482e-427f-8434-7f86890ab222", true}, // valid: version 4 {"99c17cbb-656f-564a-940f-1a4568f03487", true}, // valid: version 5 {"99c17cbb-656f-664a-940f-1a4568f03487", true}, // valid: hypothetical version 6 {"99c17cbb-656f-f64a-940f-1a4568f03487", true}, // valid: hypothetical version 15 } for i, test := range tests { if test.valid != isUUID(test.str) { t.Errorf("#%d: %q, valid %t, got valid %t", i, test.str, test.valid, !test.valid) } } } jsonschema-5.3.1/go.mod000066400000000000000000000000711445702222200147560ustar00rootroot00000000000000module github.com/santhosh-tekuri/jsonschema/v5 go 1.19 jsonschema-5.3.1/httploader/000077500000000000000000000000001445702222200160205ustar00rootroot00000000000000jsonschema-5.3.1/httploader/httploader.go000066400000000000000000000016161445702222200205210ustar00rootroot00000000000000// Package httploader implements loader.Loader for http/https url. // // The package is typically only imported for the side effect of // registering its Loaders. // // To use httploader, link this package into your program: // // import _ "github.com/santhosh-tekuri/jsonschema/v5/httploader" package httploader import ( "fmt" "io" "net/http" "github.com/santhosh-tekuri/jsonschema/v5" ) // Client is the default HTTP Client used to Get the resource. var Client = http.DefaultClient // Load loads resource from given http(s) url. func Load(url string) (io.ReadCloser, error) { resp, err := Client.Get(url) if err != nil { return nil, err } if resp.StatusCode != http.StatusOK { _ = resp.Body.Close() return nil, fmt.Errorf("%s returned status code %d", url, resp.StatusCode) } return resp.Body, nil } func init() { jsonschema.Loaders["http"] = Load jsonschema.Loaders["https"] = Load } jsonschema-5.3.1/internal_test.go000066400000000000000000000002731445702222200170560ustar00rootroot00000000000000package jsonschema import "testing" func TestQuote(t *testing.T) { got, want := quote(`abc"def'ghi`), `'abc"def\'ghi'` if got != want { t.Fatalf("got: %s want: %s", got, want) } } jsonschema-5.3.1/loader.go000066400000000000000000000027071445702222200154550ustar00rootroot00000000000000package jsonschema import ( "fmt" "io" "net/url" "os" "path/filepath" "runtime" "strings" ) func loadFileURL(s string) (io.ReadCloser, error) { u, err := url.Parse(s) if err != nil { return nil, err } f := u.Path if runtime.GOOS == "windows" { f = strings.TrimPrefix(f, "/") f = filepath.FromSlash(f) } return os.Open(f) } // Loaders is a registry of functions, which know how to load // absolute url of specific schema. // // New loaders can be registered by adding to this map. Key is schema, // value is function that knows how to load url of that schema var Loaders = map[string]func(url string) (io.ReadCloser, error){ "file": loadFileURL, } // LoaderNotFoundError is the error type returned by Load function. // It tells that no Loader is registered for that URL Scheme. type LoaderNotFoundError string func (e LoaderNotFoundError) Error() string { return fmt.Sprintf("jsonschema: no Loader found for %s", string(e)) } // LoadURL loads document at given absolute URL. The default implementation // uses Loaders registry to lookup by schema and uses that loader. // // Users can change this variable, if they would like to take complete // responsibility of loading given URL. Used by Compiler if its LoadURL // field is nil. var LoadURL = func(s string) (io.ReadCloser, error) { u, err := url.Parse(s) if err != nil { return nil, err } loader, ok := Loaders[u.Scheme] if !ok { return nil, LoaderNotFoundError(s) } return loader(s) } jsonschema-5.3.1/output.go000066400000000000000000000043051445702222200155430ustar00rootroot00000000000000package jsonschema // Flag is output format with simple boolean property valid. type Flag struct { Valid bool `json:"valid"` } // FlagOutput returns output in flag format func (ve *ValidationError) FlagOutput() Flag { return Flag{} } // Basic --- // Basic is output format with flat list of output units. type Basic struct { Valid bool `json:"valid"` Errors []BasicError `json:"errors"` } // BasicError is output unit in basic format. type BasicError struct { KeywordLocation string `json:"keywordLocation"` AbsoluteKeywordLocation string `json:"absoluteKeywordLocation"` InstanceLocation string `json:"instanceLocation"` Error string `json:"error"` } // BasicOutput returns output in basic format func (ve *ValidationError) BasicOutput() Basic { var errors []BasicError var flatten func(*ValidationError) flatten = func(ve *ValidationError) { errors = append(errors, BasicError{ KeywordLocation: ve.KeywordLocation, AbsoluteKeywordLocation: ve.AbsoluteKeywordLocation, InstanceLocation: ve.InstanceLocation, Error: ve.Message, }) for _, cause := range ve.Causes { flatten(cause) } } flatten(ve) return Basic{Errors: errors} } // Detailed --- // Detailed is output format based on structure of schema. type Detailed struct { Valid bool `json:"valid"` KeywordLocation string `json:"keywordLocation"` AbsoluteKeywordLocation string `json:"absoluteKeywordLocation"` InstanceLocation string `json:"instanceLocation"` Error string `json:"error,omitempty"` Errors []Detailed `json:"errors,omitempty"` } // DetailedOutput returns output in detailed format func (ve *ValidationError) DetailedOutput() Detailed { var errors []Detailed for _, cause := range ve.Causes { errors = append(errors, cause.DetailedOutput()) } var message = ve.Message if len(ve.Causes) > 0 { message = "" } return Detailed{ KeywordLocation: ve.KeywordLocation, AbsoluteKeywordLocation: ve.AbsoluteKeywordLocation, InstanceLocation: ve.InstanceLocation, Error: message, Errors: errors, } } jsonschema-5.3.1/resource.go000066400000000000000000000133221445702222200160310ustar00rootroot00000000000000package jsonschema import ( "encoding/json" "fmt" "io" "net/url" "path/filepath" "runtime" "strconv" "strings" ) type resource struct { url string // base url of resource. can be empty floc string // fragment with json-pointer from root resource doc interface{} draft *Draft subresources map[string]*resource // key is floc. only applicable for root resource schema *Schema } func (r *resource) String() string { return r.url + r.floc } func newResource(url string, r io.Reader) (*resource, error) { if strings.IndexByte(url, '#') != -1 { panic(fmt.Sprintf("BUG: newResource(%q)", url)) } doc, err := unmarshal(r) if err != nil { return nil, fmt.Errorf("jsonschema: invalid json %s: %v", url, err) } url, err = toAbs(url) if err != nil { return nil, err } return &resource{ url: url, floc: "#", doc: doc, }, nil } // fillSubschemas fills subschemas in res into r.subresources func (r *resource) fillSubschemas(c *Compiler, res *resource) error { if err := c.validateSchema(r, res.doc, res.floc[1:]); err != nil { return err } if r.subresources == nil { r.subresources = make(map[string]*resource) } if err := r.draft.listSubschemas(res, r.baseURL(res.floc), r.subresources); err != nil { return err } // ensure subresource.url uniqueness url2floc := make(map[string]string) for _, sr := range r.subresources { if sr.url != "" { if floc, ok := url2floc[sr.url]; ok { return fmt.Errorf("jsonschema: %q and %q in %s have same canonical-uri", floc[1:], sr.floc[1:], r.url) } url2floc[sr.url] = sr.floc } } return nil } // listResources lists all subresources in res func (r *resource) listResources(res *resource) []*resource { var result []*resource prefix := res.floc + "/" for _, sr := range r.subresources { if strings.HasPrefix(sr.floc, prefix) { result = append(result, sr) } } return result } func (r *resource) findResource(url string) *resource { if r.url == url { return r } for _, res := range r.subresources { if res.url == url { return res } } return nil } // resolve fragment f with sr as base func (r *resource) resolveFragment(c *Compiler, sr *resource, f string) (*resource, error) { if f == "#" || f == "#/" { return sr, nil } // resolve by anchor if !strings.HasPrefix(f, "#/") { // check in given resource for _, anchor := range r.draft.anchors(sr.doc) { if anchor == f[1:] { return sr, nil } } // check in subresources that has same base url prefix := sr.floc + "/" for _, res := range r.subresources { if strings.HasPrefix(res.floc, prefix) && r.baseURL(res.floc) == sr.url { for _, anchor := range r.draft.anchors(res.doc) { if anchor == f[1:] { return res, nil } } } } return nil, nil } // resolve by ptr floc := sr.floc + f[1:] if res, ok := r.subresources[floc]; ok { return res, nil } // non-standrad location doc := r.doc for _, item := range strings.Split(floc[2:], "/") { item = strings.Replace(item, "~1", "/", -1) item = strings.Replace(item, "~0", "~", -1) item, err := url.PathUnescape(item) if err != nil { return nil, err } switch d := doc.(type) { case map[string]interface{}: if _, ok := d[item]; !ok { return nil, nil } doc = d[item] case []interface{}: index, err := strconv.Atoi(item) if err != nil { return nil, err } if index < 0 || index >= len(d) { return nil, nil } doc = d[index] default: return nil, nil } } id, err := r.draft.resolveID(r.baseURL(floc), doc) if err != nil { return nil, err } res := &resource{url: id, floc: floc, doc: doc} r.subresources[floc] = res if err := r.fillSubschemas(c, res); err != nil { return nil, err } return res, nil } func (r *resource) baseURL(floc string) string { for { if sr, ok := r.subresources[floc]; ok { if sr.url != "" { return sr.url } } slash := strings.LastIndexByte(floc, '/') if slash == -1 { break } floc = floc[:slash] } return r.url } // url helpers --- func toAbs(s string) (string, error) { // if windows absolute file path, convert to file url // because: net/url parses driver name as scheme if runtime.GOOS == "windows" && len(s) >= 3 && s[1:3] == `:\` { s = "file:///" + filepath.ToSlash(s) } u, err := url.Parse(s) if err != nil { return "", err } if u.IsAbs() { return s, nil } // s is filepath if s, err = filepath.Abs(s); err != nil { return "", err } if runtime.GOOS == "windows" { s = "file:///" + filepath.ToSlash(s) } else { s = "file://" + s } u, err = url.Parse(s) // to fix spaces in filepath return u.String(), err } func resolveURL(base, ref string) (string, error) { if ref == "" { return base, nil } if strings.HasPrefix(ref, "urn:") { return ref, nil } refURL, err := url.Parse(ref) if err != nil { return "", err } if refURL.IsAbs() { return ref, nil } if strings.HasPrefix(base, "urn:") { base, _ = split(base) return base + ref, nil } baseURL, err := url.Parse(base) if err != nil { return "", err } return baseURL.ResolveReference(refURL).String(), nil } func split(uri string) (string, string) { hash := strings.IndexByte(uri, '#') if hash == -1 { return uri, "#" } f := uri[hash:] if f == "#/" { f = "#" } return uri[0:hash], f } func (s *Schema) url() string { u, _ := split(s.Location) return u } func (s *Schema) loc() string { _, f := split(s.Location) return f[1:] } func unmarshal(r io.Reader) (interface{}, error) { decoder := json.NewDecoder(r) decoder.UseNumber() var doc interface{} if err := decoder.Decode(&doc); err != nil { return nil, err } if t, _ := decoder.Token(); t != nil { return nil, fmt.Errorf("invalid character %v after top-level value", t) } return doc, nil } jsonschema-5.3.1/schema.go000066400000000000000000000616411445702222200154510ustar00rootroot00000000000000package jsonschema import ( "bytes" "encoding/json" "fmt" "hash/maphash" "math/big" "net/url" "regexp" "sort" "strconv" "strings" "unicode/utf8" ) // A Schema represents compiled version of json-schema. type Schema struct { Location string // absolute location Draft *Draft // draft used by schema. meta *Schema vocab []string dynamicAnchors []*Schema // type agnostic validations Format string format func(interface{}) bool Always *bool // always pass/fail. used when booleans are used as schemas in draft-07. Ref *Schema RecursiveAnchor bool RecursiveRef *Schema DynamicAnchor string DynamicRef *Schema dynamicRefAnchor string Types []string // allowed types. Constant []interface{} // first element in slice is constant value. note: slice is used to capture nil constant. Enum []interface{} // allowed values. enumError string // error message for enum fail. captured here to avoid constructing error message every time. Not *Schema AllOf []*Schema AnyOf []*Schema OneOf []*Schema If *Schema Then *Schema // nil, when If is nil. Else *Schema // nil, when If is nil. // object validations MinProperties int // -1 if not specified. MaxProperties int // -1 if not specified. Required []string // list of required properties. Properties map[string]*Schema PropertyNames *Schema RegexProperties bool // property names must be valid regex. used only in draft4 as workaround in metaschema. PatternProperties map[*regexp.Regexp]*Schema AdditionalProperties interface{} // nil or bool or *Schema. Dependencies map[string]interface{} // map value is *Schema or []string. DependentRequired map[string][]string DependentSchemas map[string]*Schema UnevaluatedProperties *Schema // array validations MinItems int // -1 if not specified. MaxItems int // -1 if not specified. UniqueItems bool Items interface{} // nil or *Schema or []*Schema AdditionalItems interface{} // nil or bool or *Schema. PrefixItems []*Schema Items2020 *Schema // items keyword reintroduced in draft 2020-12 Contains *Schema ContainsEval bool // whether any item in an array that passes validation of the contains schema is considered "evaluated" MinContains int // 1 if not specified MaxContains int // -1 if not specified UnevaluatedItems *Schema // string validations MinLength int // -1 if not specified. MaxLength int // -1 if not specified. Pattern *regexp.Regexp ContentEncoding string decoder func(string) ([]byte, error) ContentMediaType string mediaType func([]byte) error ContentSchema *Schema // number validators Minimum *big.Rat ExclusiveMinimum *big.Rat Maximum *big.Rat ExclusiveMaximum *big.Rat MultipleOf *big.Rat // annotations. captured only when Compiler.ExtractAnnotations is true. Title string Description string Default interface{} Comment string ReadOnly bool WriteOnly bool Examples []interface{} Deprecated bool // user defined extensions Extensions map[string]ExtSchema } func (s *Schema) String() string { return s.Location } func newSchema(url, floc string, draft *Draft, doc interface{}) *Schema { // fill with default values s := &Schema{ Location: url + floc, Draft: draft, MinProperties: -1, MaxProperties: -1, MinItems: -1, MaxItems: -1, MinContains: 1, MaxContains: -1, MinLength: -1, MaxLength: -1, } if doc, ok := doc.(map[string]interface{}); ok { if ra, ok := doc["$recursiveAnchor"]; ok { if ra, ok := ra.(bool); ok { s.RecursiveAnchor = ra } } if da, ok := doc["$dynamicAnchor"]; ok { if da, ok := da.(string); ok { s.DynamicAnchor = da } } } return s } func (s *Schema) hasVocab(name string) bool { if s == nil { // during bootstrap return true } if name == "core" { return true } for _, url := range s.vocab { if url == "https://json-schema.org/draft/2019-09/vocab/"+name { return true } if url == "https://json-schema.org/draft/2020-12/vocab/"+name { return true } } return false } // Validate validates given doc, against the json-schema s. // // the v must be the raw json value. for number precision // unmarshal with json.UseNumber(). // // returns *ValidationError if v does not confirm with schema s. // returns InfiniteLoopError if it detects loop during validation. // returns InvalidJSONTypeError if it detects any non json value in v. func (s *Schema) Validate(v interface{}) (err error) { return s.validateValue(v, "") } func (s *Schema) validateValue(v interface{}, vloc string) (err error) { defer func() { if r := recover(); r != nil { switch r := r.(type) { case InfiniteLoopError, InvalidJSONTypeError: err = r.(error) default: panic(r) } } }() if _, err := s.validate(nil, 0, "", v, vloc); err != nil { ve := ValidationError{ KeywordLocation: "", AbsoluteKeywordLocation: s.Location, InstanceLocation: vloc, Message: fmt.Sprintf("doesn't validate with %s", s.Location), } return ve.causes(err) } return nil } // validate validates given value v with this schema. func (s *Schema) validate(scope []schemaRef, vscope int, spath string, v interface{}, vloc string) (result validationResult, err error) { validationError := func(keywordPath string, format string, a ...interface{}) *ValidationError { return &ValidationError{ KeywordLocation: keywordLocation(scope, keywordPath), AbsoluteKeywordLocation: joinPtr(s.Location, keywordPath), InstanceLocation: vloc, Message: fmt.Sprintf(format, a...), } } sref := schemaRef{spath, s, false} if err := checkLoop(scope[len(scope)-vscope:], sref); err != nil { panic(err) } scope = append(scope, sref) vscope++ // populate result switch v := v.(type) { case map[string]interface{}: result.unevalProps = make(map[string]struct{}) for pname := range v { result.unevalProps[pname] = struct{}{} } case []interface{}: result.unevalItems = make(map[int]struct{}) for i := range v { result.unevalItems[i] = struct{}{} } } validate := func(sch *Schema, schPath string, v interface{}, vpath string) error { vloc := vloc if vpath != "" { vloc += "/" + vpath } _, err := sch.validate(scope, 0, schPath, v, vloc) return err } validateInplace := func(sch *Schema, schPath string) error { vr, err := sch.validate(scope, vscope, schPath, v, vloc) if err == nil { // update result for pname := range result.unevalProps { if _, ok := vr.unevalProps[pname]; !ok { delete(result.unevalProps, pname) } } for i := range result.unevalItems { if _, ok := vr.unevalItems[i]; !ok { delete(result.unevalItems, i) } } } return err } if s.Always != nil { if !*s.Always { return result, validationError("", "not allowed") } return result, nil } if len(s.Types) > 0 { vType := jsonType(v) matched := false for _, t := range s.Types { if vType == t { matched = true break } else if t == "integer" && vType == "number" { num, _ := new(big.Rat).SetString(fmt.Sprint(v)) if num.IsInt() { matched = true break } } } if !matched { return result, validationError("type", "expected %s, but got %s", strings.Join(s.Types, " or "), vType) } } var errors []error if len(s.Constant) > 0 { if !equals(v, s.Constant[0]) { switch jsonType(s.Constant[0]) { case "object", "array": errors = append(errors, validationError("const", "const failed")) default: errors = append(errors, validationError("const", "value must be %#v", s.Constant[0])) } } } if len(s.Enum) > 0 { matched := false for _, item := range s.Enum { if equals(v, item) { matched = true break } } if !matched { errors = append(errors, validationError("enum", s.enumError)) } } if s.format != nil && !s.format(v) { var val = v if v, ok := v.(string); ok { val = quote(v) } errors = append(errors, validationError("format", "%v is not valid %s", val, quote(s.Format))) } switch v := v.(type) { case map[string]interface{}: if s.MinProperties != -1 && len(v) < s.MinProperties { errors = append(errors, validationError("minProperties", "minimum %d properties allowed, but found %d properties", s.MinProperties, len(v))) } if s.MaxProperties != -1 && len(v) > s.MaxProperties { errors = append(errors, validationError("maxProperties", "maximum %d properties allowed, but found %d properties", s.MaxProperties, len(v))) } if len(s.Required) > 0 { var missing []string for _, pname := range s.Required { if _, ok := v[pname]; !ok { missing = append(missing, quote(pname)) } } if len(missing) > 0 { errors = append(errors, validationError("required", "missing properties: %s", strings.Join(missing, ", "))) } } for pname, sch := range s.Properties { if pvalue, ok := v[pname]; ok { delete(result.unevalProps, pname) if err := validate(sch, "properties/"+escape(pname), pvalue, escape(pname)); err != nil { errors = append(errors, err) } } } if s.PropertyNames != nil { for pname := range v { if err := validate(s.PropertyNames, "propertyNames", pname, escape(pname)); err != nil { errors = append(errors, err) } } } if s.RegexProperties { for pname := range v { if !isRegex(pname) { errors = append(errors, validationError("", "patternProperty %s is not valid regex", quote(pname))) } } } for pattern, sch := range s.PatternProperties { for pname, pvalue := range v { if pattern.MatchString(pname) { delete(result.unevalProps, pname) if err := validate(sch, "patternProperties/"+escape(pattern.String()), pvalue, escape(pname)); err != nil { errors = append(errors, err) } } } } if s.AdditionalProperties != nil { if allowed, ok := s.AdditionalProperties.(bool); ok { if !allowed && len(result.unevalProps) > 0 { errors = append(errors, validationError("additionalProperties", "additionalProperties %s not allowed", result.unevalPnames())) } } else { schema := s.AdditionalProperties.(*Schema) for pname := range result.unevalProps { if pvalue, ok := v[pname]; ok { if err := validate(schema, "additionalProperties", pvalue, escape(pname)); err != nil { errors = append(errors, err) } } } } result.unevalProps = nil } for dname, dvalue := range s.Dependencies { if _, ok := v[dname]; ok { switch dvalue := dvalue.(type) { case *Schema: if err := validateInplace(dvalue, "dependencies/"+escape(dname)); err != nil { errors = append(errors, err) } case []string: for i, pname := range dvalue { if _, ok := v[pname]; !ok { errors = append(errors, validationError("dependencies/"+escape(dname)+"/"+strconv.Itoa(i), "property %s is required, if %s property exists", quote(pname), quote(dname))) } } } } } for dname, dvalue := range s.DependentRequired { if _, ok := v[dname]; ok { for i, pname := range dvalue { if _, ok := v[pname]; !ok { errors = append(errors, validationError("dependentRequired/"+escape(dname)+"/"+strconv.Itoa(i), "property %s is required, if %s property exists", quote(pname), quote(dname))) } } } } for dname, sch := range s.DependentSchemas { if _, ok := v[dname]; ok { if err := validateInplace(sch, "dependentSchemas/"+escape(dname)); err != nil { errors = append(errors, err) } } } case []interface{}: if s.MinItems != -1 && len(v) < s.MinItems { errors = append(errors, validationError("minItems", "minimum %d items required, but found %d items", s.MinItems, len(v))) } if s.MaxItems != -1 && len(v) > s.MaxItems { errors = append(errors, validationError("maxItems", "maximum %d items required, but found %d items", s.MaxItems, len(v))) } if s.UniqueItems { if len(v) <= 20 { outer1: for i := 1; i < len(v); i++ { for j := 0; j < i; j++ { if equals(v[i], v[j]) { errors = append(errors, validationError("uniqueItems", "items at index %d and %d are equal", j, i)) break outer1 } } } } else { m := make(map[uint64][]int) var h maphash.Hash outer2: for i, item := range v { h.Reset() hash(item, &h) k := h.Sum64() if err != nil { panic(err) } arr, ok := m[k] if ok { for _, j := range arr { if equals(v[j], item) { errors = append(errors, validationError("uniqueItems", "items at index %d and %d are equal", j, i)) break outer2 } } } arr = append(arr, i) m[k] = arr } } } // items + additionalItems switch items := s.Items.(type) { case *Schema: for i, item := range v { if err := validate(items, "items", item, strconv.Itoa(i)); err != nil { errors = append(errors, err) } } result.unevalItems = nil case []*Schema: for i, item := range v { if i < len(items) { delete(result.unevalItems, i) if err := validate(items[i], "items/"+strconv.Itoa(i), item, strconv.Itoa(i)); err != nil { errors = append(errors, err) } } else if sch, ok := s.AdditionalItems.(*Schema); ok { delete(result.unevalItems, i) if err := validate(sch, "additionalItems", item, strconv.Itoa(i)); err != nil { errors = append(errors, err) } } else { break } } if additionalItems, ok := s.AdditionalItems.(bool); ok { if additionalItems { result.unevalItems = nil } else if len(v) > len(items) { errors = append(errors, validationError("additionalItems", "only %d items are allowed, but found %d items", len(items), len(v))) } } } // prefixItems + items for i, item := range v { if i < len(s.PrefixItems) { delete(result.unevalItems, i) if err := validate(s.PrefixItems[i], "prefixItems/"+strconv.Itoa(i), item, strconv.Itoa(i)); err != nil { errors = append(errors, err) } } else if s.Items2020 != nil { delete(result.unevalItems, i) if err := validate(s.Items2020, "items", item, strconv.Itoa(i)); err != nil { errors = append(errors, err) } } else { break } } // contains + minContains + maxContains if s.Contains != nil && (s.MinContains != -1 || s.MaxContains != -1) { matched := 0 var causes []error for i, item := range v { if err := validate(s.Contains, "contains", item, strconv.Itoa(i)); err != nil { causes = append(causes, err) } else { matched++ if s.ContainsEval { delete(result.unevalItems, i) } } } if s.MinContains != -1 && matched < s.MinContains { errors = append(errors, validationError("minContains", "valid must be >= %d, but got %d", s.MinContains, matched).add(causes...)) } if s.MaxContains != -1 && matched > s.MaxContains { errors = append(errors, validationError("maxContains", "valid must be <= %d, but got %d", s.MaxContains, matched)) } } case string: // minLength + maxLength if s.MinLength != -1 || s.MaxLength != -1 { length := utf8.RuneCount([]byte(v)) if s.MinLength != -1 && length < s.MinLength { errors = append(errors, validationError("minLength", "length must be >= %d, but got %d", s.MinLength, length)) } if s.MaxLength != -1 && length > s.MaxLength { errors = append(errors, validationError("maxLength", "length must be <= %d, but got %d", s.MaxLength, length)) } } if s.Pattern != nil && !s.Pattern.MatchString(v) { errors = append(errors, validationError("pattern", "does not match pattern %s", quote(s.Pattern.String()))) } // contentEncoding + contentMediaType if s.decoder != nil || s.mediaType != nil { decoded := s.ContentEncoding == "" var content []byte if s.decoder != nil { b, err := s.decoder(v) if err != nil { errors = append(errors, validationError("contentEncoding", "value is not %s encoded", s.ContentEncoding)) } else { content, decoded = b, true } } if decoded && s.mediaType != nil { if s.decoder == nil { content = []byte(v) } if err := s.mediaType(content); err != nil { errors = append(errors, validationError("contentMediaType", "value is not of mediatype %s", quote(s.ContentMediaType))) } } if decoded && s.ContentSchema != nil { contentJSON, err := unmarshal(bytes.NewReader(content)) if err != nil { errors = append(errors, validationError("contentSchema", "value is not valid json")) } else { err := validate(s.ContentSchema, "contentSchema", contentJSON, "") if err != nil { errors = append(errors, err) } } } } case json.Number, float32, float64, int, int8, int32, int64, uint, uint8, uint32, uint64: // lazy convert to *big.Rat to avoid allocation var numVal *big.Rat num := func() *big.Rat { if numVal == nil { numVal, _ = new(big.Rat).SetString(fmt.Sprint(v)) } return numVal } f64 := func(r *big.Rat) float64 { f, _ := r.Float64() return f } if s.Minimum != nil && num().Cmp(s.Minimum) < 0 { errors = append(errors, validationError("minimum", "must be >= %v but found %v", f64(s.Minimum), v)) } if s.ExclusiveMinimum != nil && num().Cmp(s.ExclusiveMinimum) <= 0 { errors = append(errors, validationError("exclusiveMinimum", "must be > %v but found %v", f64(s.ExclusiveMinimum), v)) } if s.Maximum != nil && num().Cmp(s.Maximum) > 0 { errors = append(errors, validationError("maximum", "must be <= %v but found %v", f64(s.Maximum), v)) } if s.ExclusiveMaximum != nil && num().Cmp(s.ExclusiveMaximum) >= 0 { errors = append(errors, validationError("exclusiveMaximum", "must be < %v but found %v", f64(s.ExclusiveMaximum), v)) } if s.MultipleOf != nil { if q := new(big.Rat).Quo(num(), s.MultipleOf); !q.IsInt() { errors = append(errors, validationError("multipleOf", "%v not multipleOf %v", v, f64(s.MultipleOf))) } } } // $ref + $recursiveRef + $dynamicRef validateRef := func(sch *Schema, refPath string) error { if sch != nil { if err := validateInplace(sch, refPath); err != nil { var url = sch.Location if s.url() == sch.url() { url = sch.loc() } return validationError(refPath, "doesn't validate with %s", quote(url)).causes(err) } } return nil } if err := validateRef(s.Ref, "$ref"); err != nil { errors = append(errors, err) } if s.RecursiveRef != nil { sch := s.RecursiveRef if sch.RecursiveAnchor { // recursiveRef based on scope for _, e := range scope { if e.schema.RecursiveAnchor { sch = e.schema break } } } if err := validateRef(sch, "$recursiveRef"); err != nil { errors = append(errors, err) } } if s.DynamicRef != nil { sch := s.DynamicRef if s.dynamicRefAnchor != "" && sch.DynamicAnchor == s.dynamicRefAnchor { // dynamicRef based on scope for i := len(scope) - 1; i >= 0; i-- { sr := scope[i] if sr.discard { break } for _, da := range sr.schema.dynamicAnchors { if da.DynamicAnchor == s.DynamicRef.DynamicAnchor && da != s.DynamicRef { sch = da break } } } } if err := validateRef(sch, "$dynamicRef"); err != nil { errors = append(errors, err) } } if s.Not != nil && validateInplace(s.Not, "not") == nil { errors = append(errors, validationError("not", "not failed")) } for i, sch := range s.AllOf { schPath := "allOf/" + strconv.Itoa(i) if err := validateInplace(sch, schPath); err != nil { errors = append(errors, validationError(schPath, "allOf failed").add(err)) } } if len(s.AnyOf) > 0 { matched := false var causes []error for i, sch := range s.AnyOf { if err := validateInplace(sch, "anyOf/"+strconv.Itoa(i)); err == nil { matched = true } else { causes = append(causes, err) } } if !matched { errors = append(errors, validationError("anyOf", "anyOf failed").add(causes...)) } } if len(s.OneOf) > 0 { matched := -1 var causes []error for i, sch := range s.OneOf { if err := validateInplace(sch, "oneOf/"+strconv.Itoa(i)); err == nil { if matched == -1 { matched = i } else { errors = append(errors, validationError("oneOf", "valid against schemas at indexes %d and %d", matched, i)) break } } else { causes = append(causes, err) } } if matched == -1 { errors = append(errors, validationError("oneOf", "oneOf failed").add(causes...)) } } // if + then + else if s.If != nil { err := validateInplace(s.If, "if") // "if" leaves dynamic scope scope[len(scope)-1].discard = true if err == nil { if s.Then != nil { if err := validateInplace(s.Then, "then"); err != nil { errors = append(errors, validationError("then", "if-then failed").add(err)) } } } else { if s.Else != nil { if err := validateInplace(s.Else, "else"); err != nil { errors = append(errors, validationError("else", "if-else failed").add(err)) } } } // restore dynamic scope scope[len(scope)-1].discard = false } for _, ext := range s.Extensions { if err := ext.Validate(ValidationContext{result, validate, validateInplace, validationError}, v); err != nil { errors = append(errors, err) } } // unevaluatedProperties + unevaluatedItems switch v := v.(type) { case map[string]interface{}: if s.UnevaluatedProperties != nil { for pname := range result.unevalProps { if pvalue, ok := v[pname]; ok { if err := validate(s.UnevaluatedProperties, "unevaluatedProperties", pvalue, escape(pname)); err != nil { errors = append(errors, err) } } } result.unevalProps = nil } case []interface{}: if s.UnevaluatedItems != nil { for i := range result.unevalItems { if err := validate(s.UnevaluatedItems, "unevaluatedItems", v[i], strconv.Itoa(i)); err != nil { errors = append(errors, err) } } result.unevalItems = nil } } switch len(errors) { case 0: return result, nil case 1: return result, errors[0] default: return result, validationError("", "").add(errors...) // empty message, used just for wrapping } } type validationResult struct { unevalProps map[string]struct{} unevalItems map[int]struct{} } func (vr validationResult) unevalPnames() string { pnames := make([]string, 0, len(vr.unevalProps)) for pname := range vr.unevalProps { pnames = append(pnames, quote(pname)) } return strings.Join(pnames, ", ") } // jsonType returns the json type of given value v. // // It panics if the given value is not valid json value func jsonType(v interface{}) string { switch v.(type) { case nil: return "null" case bool: return "boolean" case json.Number, float32, float64, int, int8, int32, int64, uint, uint8, uint32, uint64: return "number" case string: return "string" case []interface{}: return "array" case map[string]interface{}: return "object" } panic(InvalidJSONTypeError(fmt.Sprintf("%T", v))) } // equals tells if given two json values are equal or not. func equals(v1, v2 interface{}) bool { v1Type := jsonType(v1) if v1Type != jsonType(v2) { return false } switch v1Type { case "array": arr1, arr2 := v1.([]interface{}), v2.([]interface{}) if len(arr1) != len(arr2) { return false } for i := range arr1 { if !equals(arr1[i], arr2[i]) { return false } } return true case "object": obj1, obj2 := v1.(map[string]interface{}), v2.(map[string]interface{}) if len(obj1) != len(obj2) { return false } for k, v1 := range obj1 { if v2, ok := obj2[k]; ok { if !equals(v1, v2) { return false } } else { return false } } return true case "number": num1, _ := new(big.Rat).SetString(fmt.Sprint(v1)) num2, _ := new(big.Rat).SetString(fmt.Sprint(v2)) return num1.Cmp(num2) == 0 default: return v1 == v2 } } func hash(v interface{}, h *maphash.Hash) { switch v := v.(type) { case nil: h.WriteByte(0) case bool: h.WriteByte(1) if v { h.WriteByte(1) } else { h.WriteByte(0) } case json.Number, float32, float64, int, int8, int32, int64, uint, uint8, uint32, uint64: h.WriteByte(2) num, _ := new(big.Rat).SetString(fmt.Sprint(v)) h.Write(num.Num().Bytes()) h.Write(num.Denom().Bytes()) case string: h.WriteByte(3) h.WriteString(v) case []interface{}: h.WriteByte(4) for _, item := range v { hash(item, h) } case map[string]interface{}: h.WriteByte(5) props := make([]string, 0, len(v)) for prop := range v { props = append(props, prop) } sort.Slice(props, func(i, j int) bool { return props[i] < props[j] }) for _, prop := range props { hash(prop, h) hash(v[prop], h) } default: panic(InvalidJSONTypeError(fmt.Sprintf("%T", v))) } } // escape converts given token to valid json-pointer token func escape(token string) string { token = strings.ReplaceAll(token, "~", "~0") token = strings.ReplaceAll(token, "/", "~1") return url.PathEscape(token) } jsonschema-5.3.1/schema_test.go000066400000000000000000000613221445702222200165040ustar00rootroot00000000000000package jsonschema_test import ( "bytes" "crypto/tls" "encoding/json" "errors" "fmt" "io" "io/ioutil" "net/http" "net/http/httptest" "net/url" "os" "path" "path/filepath" "runtime" "strings" "testing" "github.com/santhosh-tekuri/jsonschema/v5" _ "github.com/santhosh-tekuri/jsonschema/v5/httploader" ) var skipTests = map[string]map[string][]string{ "TestDraft4/optional/zeroTerminatedFloats.json": { "some languages do not distinguish between different types of numeric value": {}, // this behavior is changed in new drafts }, "TestDraft4/optional/ecmascript-regex.json": { "ECMA 262 \\s matches whitespace": { "Line tabulation matches", // \s does not match vertical tab "latin-1 non-breaking-space matches", // \s does not match unicode whitespace "zero-width whitespace matches", // \s does not match unicode whitespace "paragraph separator matches (line terminator)", // \s does not match unicode whitespace "EM SPACE matches (Space_Separator)", // \s does not match unicode whitespace }, "ECMA 262 \\S matches everything but whitespace": { "Line tabulation does not match", // \S matches unicode whitespace "latin-1 non-breaking-space does not match", // \S matches unicode whitespace "zero-width whitespace does not match", // \S matches unicode whitespace "paragraph separator does not match (line terminator)", // \S matches unicode whitespace "EM SPACE does not match (Space_Separator)", // \S matches unicode whitespace }, "ECMA 262 regex escapes control codes with \\c and upper letter": {}, // \cX is not supported "ECMA 262 regex escapes control codes with \\c and lower letter": {}, // \cX is not supported "patterns always use unicode semantics with pattern": {}, // invalid regex "\\p{Letter}cole" "pattern with non-ASCII digits": {}, // invalid regex "^\\p{digit}+$" "patternProperties with non-ASCII digits": {}, // invalid regex "^\\p{digit}+$" "patterns always use unicode semantics with patternProperties": {}, // invalid regex "\\p{Letter}cole" }, // "TestDraft6/optional/ecmascript-regex.json": { "ECMA 262 \\s matches whitespace": { "Line tabulation matches", // \s does not match vertical tab "latin-1 non-breaking-space matches", // \s does not match unicode whitespace "zero-width whitespace matches", // \s does not match unicode whitespace "paragraph separator matches (line terminator)", // \s does not match unicode whitespace "EM SPACE matches (Space_Separator)", // \s does not match unicode whitespace }, "ECMA 262 \\S matches everything but whitespace": { "Line tabulation does not match", // \S matches unicode whitespace "latin-1 non-breaking-space does not match", // \S matches unicode whitespace "zero-width whitespace does not match", // \S matches unicode whitespace "paragraph separator does not match (line terminator)", // \S matches unicode whitespace "EM SPACE does not match (Space_Separator)", // \S matches unicode whitespace }, "ECMA 262 regex escapes control codes with \\c and upper letter": {}, // \cX is not supported "ECMA 262 regex escapes control codes with \\c and lower letter": {}, // \cX is not supported "patterns always use unicode semantics with pattern": {}, // invalid regex "\\p{Letter}cole" "pattern with non-ASCII digits": {}, // invalid regex "^\\p{digit}+$" "patternProperties with non-ASCII digits": {}, // invalid regex "^\\p{digit}+$" "patterns always use unicode semantics with patternProperties": {}, // invalid regex "\\p{Letter}cole" }, // "TestDraft7/optional/format/idn-hostname.json": {}, // idn-hostname format is not implemented "TestDraft7/optional/format/idn-email.json": {}, // idn-email format is not implemented "TestDraft7/optional/ecmascript-regex.json": { "ECMA 262 \\s matches whitespace": { "Line tabulation matches", // \s does not match vertical tab "latin-1 non-breaking-space matches", // \s does not match unicode whitespace "zero-width whitespace matches", // \s does not match unicode whitespace "paragraph separator matches (line terminator)", // \s does not match unicode whitespace "EM SPACE matches (Space_Separator)", // \s does not match unicode whitespace }, "ECMA 262 \\S matches everything but whitespace": { "Line tabulation does not match", // \S matches unicode whitespace "latin-1 non-breaking-space does not match", // \S matches unicode whitespace "zero-width whitespace does not match", // \S matches unicode whitespace "paragraph separator does not match (line terminator)", // \S matches unicode whitespace "EM SPACE does not match (Space_Separator)", // \S matches unicode whitespace }, "ECMA 262 regex escapes control codes with \\c and upper letter": {}, // \cX is not supported "ECMA 262 regex escapes control codes with \\c and lower letter": {}, // \cX is not supported "patterns always use unicode semantics with pattern": {}, // invalid regex "\\p{Letter}cole" "pattern with non-ASCII digits": {}, // invalid regex "^\\p{digit}+$" "patternProperties with non-ASCII digits": {}, // invalid regex "^\\p{digit}+$" "patterns always use unicode semantics with patternProperties": {}, // invalid regex "\\p{Letter}cole" }, // "TestDraft2019/optional/format/idn-hostname.json": {}, // idn-hostname format is not implemented "TestDraft2019/optional/format/idn-email.json": {}, // idn-email format is not implemented "TestDraft2019/optional/ecmascript-regex.json": { "ECMA 262 \\s matches whitespace": { "Line tabulation matches", // \s does not match vertical tab "latin-1 non-breaking-space matches", // \s does not match unicode whitespace "zero-width whitespace matches", // \s does not match unicode whitespace "paragraph separator matches (line terminator)", // \s does not match unicode whitespace "EM SPACE matches (Space_Separator)", // \s does not match unicode whitespace }, "ECMA 262 \\S matches everything but whitespace": { "Line tabulation does not match", // \S matches unicode whitespace "latin-1 non-breaking-space does not match", // \S matches unicode whitespace "zero-width whitespace does not match", // \S matches unicode whitespace "paragraph separator does not match (line terminator)", // \S matches unicode whitespace "EM SPACE does not match (Space_Separator)", // \S matches unicode whitespace }, "ECMA 262 regex escapes control codes with \\c and upper letter": {}, // \cX is not supported "ECMA 262 regex escapes control codes with \\c and lower letter": {}, // \cX is not supported "patterns always use unicode semantics with pattern": {}, // invalid regex "\\p{Letter}cole" "pattern with non-ASCII digits": {}, // invalid regex "^\\p{digit}+$" "patternProperties with non-ASCII digits": {}, // invalid regex "^\\p{digit}+$" "patterns always use unicode semantics with patternProperties": {}, // invalid regex "\\p{Letter}cole" }, // "TestDraft2020/optional/format/idn-hostname.json": {}, // idn-hostname format is not implemented "TestDraft2020/optional/format/idn-email.json": {}, // idn-email format is not implemented "TestDraft2020/optional/ecmascript-regex.json": { "ECMA 262 \\s matches whitespace": { "Line tabulation matches", // \s does not match vertical tab "latin-1 non-breaking-space matches", // \s does not match unicode whitespace "zero-width whitespace matches", // \s does not match unicode whitespace "paragraph separator matches (line terminator)", // \s does not match unicode whitespace "EM SPACE matches (Space_Separator)", // \s does not match unicode whitespace }, "ECMA 262 \\S matches everything but whitespace": { "Line tabulation does not match", // \S matches unicode whitespace "latin-1 non-breaking-space does not match", // \S matches unicode whitespace "zero-width whitespace does not match", // \S matches unicode whitespace "paragraph separator does not match (line terminator)", // \S matches unicode whitespace "EM SPACE does not match (Space_Separator)", // \S matches unicode whitespace }, "ECMA 262 regex escapes control codes with \\c and upper letter": {}, // \cX is not supported "ECMA 262 regex escapes control codes with \\c and lower letter": {}, // \cX is not supported "patterns always use unicode semantics with pattern": {}, // invalid regex "\\p{Letter}cole" "\\a is not an ECMA 262 control escape": {}, // \a is valid control sequence in go-regex "pattern with non-ASCII digits": {}, // invalid regex "^\\p{digit}+$" "patternProperties with non-ASCII digits": {}, // invalid regex "^\\p{digit}+$" "patterns always use unicode semantics with patternProperties": {}, // invalid regex "\\p{Letter}cole" }, } func TestDraft4(t *testing.T) { testFolder(t, "testdata/JSON-Schema-Test-Suite/tests/draft4", jsonschema.Draft4) } func TestDraft6(t *testing.T) { testFolder(t, "testdata/JSON-Schema-Test-Suite/tests/draft6", jsonschema.Draft6) } func TestDraft7(t *testing.T) { testFolder(t, "testdata/JSON-Schema-Test-Suite/tests/draft7", jsonschema.Draft7) } func TestDraft2019(t *testing.T) { testFolder(t, "testdata/JSON-Schema-Test-Suite/tests/draft2019-09", jsonschema.Draft2019) } func TestDraft2020(t *testing.T) { testFolder(t, "testdata/JSON-Schema-Test-Suite/tests/draft2020-12", jsonschema.Draft2020) } func TestExtra(t *testing.T) { t.Run("draft7", func(t *testing.T) { testFolder(t, "testdata/tests/draft7", jsonschema.Draft7) }) t.Run("draft2020", func(t *testing.T) { testFolder(t, "testdata/tests/draft2020", jsonschema.Draft2020) }) } type testGroup struct { Description string Schema json.RawMessage Tests []struct { Description string Data json.RawMessage Valid bool Skip *string } } func TestMain(m *testing.M) { server1 := &http.Server{Addr: "localhost:1234", Handler: http.FileServer(http.Dir("testdata/JSON-Schema-Test-Suite/remotes"))} go func() { if err := server1.ListenAndServe(); err != http.ErrServerClosed { panic(err) } }() server2 := &http.Server{Addr: "localhost:1235", Handler: http.FileServer(http.Dir("testdata/remotes"))} go func() { if err := server2.ListenAndServe(); err != http.ErrServerClosed { panic(err) } }() os.Exit(m.Run()) } func testFolder(t *testing.T, folder string, draft *jsonschema.Draft) { fis, err := ioutil.ReadDir(folder) if err != nil { t.Fatal(err) } for _, fi := range fis { if fi.IsDir() { t.Run(fi.Name(), func(t *testing.T) { testFolder(t, path.Join(folder, fi.Name()), draft) }) continue } if path.Ext(fi.Name()) != ".json" { continue } t.Run(fi.Name(), func(t *testing.T) { skip := skipTests[t.Name()] if skip != nil && len(skip) == 0 { t.Skip() } f, err := os.Open(path.Join(folder, fi.Name())) if err != nil { t.Fatal(err) } defer f.Close() var tg []struct { Description string Schema json.RawMessage Tests []struct { Description string Data interface{} Valid bool } } dec := json.NewDecoder(f) dec.UseNumber() if err = dec.Decode(&tg); err != nil { t.Fatal(err) } for _, group := range tg { t.Run(group.Description, func(t *testing.T) { skip := skip[group.Description] if skip != nil && len(skip) == 0 { t.Skip() } c := jsonschema.NewCompiler() c.Draft = draft if strings.Index(folder, "optional") != -1 { c.AssertFormat = true c.AssertContent = true } if err := c.AddResource("schema.json", bytes.NewReader(group.Schema)); err != nil { t.Fatal(err) } schema, err := c.Compile("schema.json") if err != nil { t.Fatalf("%#v", err) } for _, test := range group.Tests { t.Run(test.Description, func(t *testing.T) { for _, desc := range skip { if test.Description == desc { t.Skip() } } err = schema.Validate(test.Data) valid := err == nil if !valid { if _, ok := err.(*jsonschema.ValidationError); ok { for _, line := range strings.Split(err.(*jsonschema.ValidationError).GoString(), "\n") { t.Logf("%s", line) } } else { t.Fatalf("got: %#v, want: *jsonschema.ValidationError", err) } } if test.Valid != valid { t.Fatalf("valid: got %v, want %v", valid, test.Valid) } }) } }) } }) } } func TestMustCompile(t *testing.T) { t.Run("invalid", func(t *testing.T) { defer func() { if r := recover(); r == nil { t.Error("panic expected") } }() jsonschema.MustCompile("testdata/invalid_schema.json") }) t.Run("valid", func(t *testing.T) { defer func() { if r := recover(); r != nil { t.Error("panic not expected") t.Log(r) } }() jsonschema.MustCompile("testdata/person_schema.json") }) } func TestInvalidSchema(t *testing.T) { t.Run("invalid json", func(t *testing.T) { if err := jsonschema.NewCompiler().AddResource("test.json", strings.NewReader("{")); err == nil { t.Error("error expected") } else { t.Logf("%v", err) } }) t.Run("multiple json", func(t *testing.T) { if err := jsonschema.NewCompiler().AddResource("test.json", strings.NewReader("{}{}")); err == nil { t.Error("error expected") } else { t.Logf("%v", err) } }) httpURL, httpsURL, cleanup := runHTTPServers() defer cleanup() invalidTests := []struct { description string url string }{ {"syntax error", "testdata/syntax_error.json"}, {"missing filepath", "testdata/missing.json"}, {"missing fileurl", toFileURL("testdata/missing.json")}, {"missing httpurl", httpURL + "/missing.json"}, {"missing httpsurl", httpsURL + "/missing.json"}, } for _, test := range invalidTests { t.Run(test.description, func(t *testing.T) { if _, err := jsonschema.Compile(test.url); err == nil { t.Error("expected error") } else { t.Logf("%v", err) } }) } type test struct { Description string Schema json.RawMessage Fragment string } data, err := ioutil.ReadFile("testdata/invalid_schemas.json") if err != nil { t.Fatal(err) } var tests []test if err = json.Unmarshal(data, &tests); err != nil { t.Fatal(err) } for _, test := range tests { t.Run(test.Description, func(t *testing.T) { c := jsonschema.NewCompiler() url := "test.json" if err := c.AddResource(url, bytes.NewReader(test.Schema)); err != nil { t.Fatal(err) } if len(test.Fragment) > 0 { url += test.Fragment } if _, err = c.Compile(url); err == nil { t.Error("error expected") } else { t.Logf("%#v", err) } }) } } func TestCompileURL(t *testing.T) { httpURL, httpsURL, cleanup := runHTTPServers() defer cleanup() validTests := []struct { schema, doc string }{ //{"testdata/customer_schema.json#/0", "testdata/customer.json"}, //{toFileURL("testdata/customer_schema.json") + "#/0", "testdata/customer.json"}, //{httpURL + "/customer_schema.json#/0", "testdata/customer.json"}, //{httpsURL + "/customer_schema.json#/0", "testdata/customer.json"}, {toFileURL("testdata/empty schema.json"), "testdata/empty schema.json"}, {httpURL + "/empty schema.json", "testdata/empty schema.json"}, {httpsURL + "/empty schema.json", "testdata/empty schema.json"}, } for i, test := range validTests { t.Run(test.schema, func(t *testing.T) { s, err := jsonschema.Compile(test.schema) if err != nil { t.Errorf("valid #%d: %v", i, err) return } f, err := os.Open(test.doc) if err != nil { t.Errorf("valid #%d: %v", i, err) return } err = s.Validate(f) _ = f.Close() if err != nil { t.Errorf("valid #%d: %v", i, err) } }) } } func TestInvalidJsonTypeError(t *testing.T) { compiler := jsonschema.NewCompiler() err := compiler.AddResource("test.json", strings.NewReader(`{ "type": "string"}`)) if err != nil { t.Fatalf("addResource failed. reason: %v\n", err) } schema, err := compiler.Compile("test.json") if err != nil { t.Fatalf("schema compilation failed. reason: %v\n", err) } v := struct{ name string }{"hello world"} err = schema.Validate(v) switch err.(type) { case jsonschema.InvalidJSONTypeError: // passed: struct is not valid json type default: t.Fatalf("got %v. want InvalidJSONTypeErr", err) } } func TestInfiniteLoopError(t *testing.T) { t.Run("compile", func(t *testing.T) { compiler := jsonschema.NewCompiler() _, err := compiler.Compile("testdata/loop-compile.json") if err == nil { t.Fatal("error expected") } switch err := err.(*jsonschema.SchemaError).Err.(type) { case jsonschema.InfiniteLoopError: suffix := "testdata/loop-compile.json#/$ref/$ref/not/$ref/allOf/0/$ref/anyOf/0/$ref/oneOf/0/$ref/dependencies/prop/$ref/dependentSchemas/prop/$ref/then/$ref/else/$ref" if !strings.HasSuffix(string(err), suffix) { t.Errorf(" got: %s", string(err)) t.Errorf("want-suffix: %s", suffix) } default: t.Fatalf("got %#v. want InfiniteLoopTypeErr", err) } }) t.Run("validate", func(t *testing.T) { compiler := jsonschema.NewCompiler() schema, err := compiler.Compile("testdata/loop-validate.json") if err != nil { t.Fatal(err) } err = schema.Validate(decodeString(t, `{"prop": 1}`)) switch err := err.(type) { case jsonschema.InfiniteLoopError: suffix := "testdata/loop-validate.json#/$ref/$ref/not/$ref/allOf/0/$ref/anyOf/0/$ref/oneOf/0/$ref/dependencies/prop/$ref/dependentSchemas/prop/$ref/then/$ref/else/$dynamicRef/$ref" if !strings.HasSuffix(string(err), suffix) { t.Errorf(" got: %s", string(err)) t.Errorf("want-suffix: %s", suffix) } default: t.Fatalf("got %#v. want InfiniteLoopTypeErr", err) } }) } func TestExtractAnnotations(t *testing.T) { str := `{ "title": "this is title", "description": "this is description", "$comment": "this is comment", "format": "date-time", "examples": ["2019-04-09T21:54:56.052Z"], "readOnly": true, "writeOnly": true, "deprecated": true }` t.Run("false", func(t *testing.T) { compiler := jsonschema.NewCompiler() err := compiler.AddResource("test.json", strings.NewReader(str)) if err != nil { t.Fatalf("addResource failed. reason: %v\n", err) } schema, err := compiler.Compile("test.json") if err != nil { t.Fatalf("schema compilation failed. reason: %v\n", err) } if schema.Title != "" { t.Error("title should not be extracted") } if schema.Description != "" { t.Error("description should not be extracted") } if schema.Comment != "" { t.Error("comment should not be extracted") } if len(schema.Examples) != 0 { t.Error("examples should not be extracted") } if schema.ReadOnly { t.Error("readOnly should not be extracted") } if schema.WriteOnly { t.Error("writeOnly should not be extracted") } if schema.Deprecated { t.Error("Deprecated should not be extracted") } }) t.Run("true", func(t *testing.T) { compiler := jsonschema.NewCompiler() compiler.ExtractAnnotations = true err := compiler.AddResource("test.json", strings.NewReader(str)) if err != nil { t.Fatalf("addResource failed. reason: %v\n", err) } schema, err := compiler.Compile("test.json") if err != nil { t.Fatalf("schema compilation failed. reason: %v\n", err) } if schema.Title != "this is title" { t.Errorf("title: got %q, want %q", schema.Title, "this is title") } if schema.Description != "this is description" { t.Errorf("description: got %q, want %q", schema.Description, "this is description") } if schema.Comment != "this is comment" { t.Errorf("$comment: got %q, want %q", schema.Comment, "this is comment") } if schema.Examples[0] != "2019-04-09T21:54:56.052Z" { t.Errorf("example: got %q, want %q", schema.Examples[0], "2019-04-09T21:54:56.052Z") } if !schema.ReadOnly { t.Error("readOnly should be extracted") } if !schema.WriteOnly { t.Error("writeOnly should be extracted") } if !schema.Deprecated { t.Error("Deprecated should be extracted") } }) } func toFileURL(path string) string { path, err := filepath.Abs(path) if err != nil { panic(err) } path = filepath.ToSlash(path) if runtime.GOOS == "windows" { path = "/" + path } u, err := url.Parse("file://" + path) if err != nil { panic(err) } return u.String() } // TestPanic tests https://github.com/santhosh-tekuri/jsonschema/issues/18 func TestPanic(t *testing.T) { schema_d := ` { "type": "object", "properties": { "myid": { "type": "integer" }, "otype": { "$ref": "defs.json#someid" } } } ` defs_d := ` { "definitions": { "stt": { "$schema": "http://json-schema.org/draft-07/schema#", "$id": "#someid", "type": "object", "enum": [ { "name": "stainless" }, { "name": "zinc" } ] } } } ` c := jsonschema.NewCompiler() c.Draft = jsonschema.Draft7 if err := c.AddResource("schema.json", strings.NewReader(schema_d)); err != nil { t.Fatal(err) } if err := c.AddResource("defs.json", strings.NewReader(defs_d)); err != nil { t.Fatal(err) } if _, err := c.Compile("schema.json"); err != nil { t.Fatal(err) } } func TestNonStringFormat(t *testing.T) { jsonschema.Formats["even-number"] = func(v interface{}) bool { switch v := v.(type) { case int: return v%2 == 0 default: return false } } schema := `{"type": "integer", "format": "even-number"}` c := jsonschema.NewCompiler() c.AssertFormat = true if err := c.AddResource("schema.json", strings.NewReader(schema)); err != nil { t.Fatal(err) } s, err := c.Compile("schema.json") if err != nil { t.Fatal(err) } if err = s.Validate(5); err == nil { t.Fatal("error expected") } if err = s.Validate(6); err != nil { t.Fatalf("%#v", err) } } func TestCompiler_LoadURL(t *testing.T) { const ( base = `{ "type": "string" }` schema = `{ "allOf": [{ "$ref": "base.json" }, { "maxLength": 3 }] }` ) c := jsonschema.NewCompiler() c.LoadURL = func(s string) (io.ReadCloser, error) { switch s { case "map:///base.json": return ioutil.NopCloser(strings.NewReader(base)), nil case "map:///schema.json": return ioutil.NopCloser(strings.NewReader(schema)), nil default: return nil, errors.New("unsupported schema") } } s, err := c.Compile("map:///schema.json") if err != nil { t.Fatal(err) } if err = s.Validate("foo"); err != nil { t.Fatal(err) } if err = s.Validate("long"); err == nil { t.Fatal("error expected") } } func TestFilePathSpaces(t *testing.T) { if _, err := jsonschema.Compile("testdata/person schema.json"); err != nil { t.Fatal(err) } } func TestSchemaDraftFeild(t *testing.T) { var schemas = map[string]string{ "main.json": `{"$schema": "https://json-schema.org/draft/2020-12/schema", "$ref":"obj.json"}`, "obj.json": `{"$schema": "https://json-schema.org/draft/2019-09/schema", "type":"object"}`, } jsonschema.Loaders["map"] = func(url string) (io.ReadCloser, error) { schema, ok := schemas[strings.TrimPrefix(url, "map:///")] if !ok { return nil, fmt.Errorf("%q not found", url) } return ioutil.NopCloser(strings.NewReader(schema)), nil } sch, err := jsonschema.Compile("map:///main.json") if err != nil { t.Fatalf("%+v", err) } if sch.Draft != jsonschema.Draft2020 { t.Errorf("got: %s, want: %s", sch.Draft, jsonschema.Draft2020) } if sch.Ref.Draft != jsonschema.Draft2019 { t.Errorf("got: %s, want: %s", sch.Ref.Draft, jsonschema.Draft2019) } } func runHTTPServers() (httpURL, httpsURL string, cleanup func()) { tr := http.DefaultTransport.(*http.Transport) if tr.TLSClientConfig == nil { tr.TLSClientConfig = &tls.Config{} } tr.TLSClientConfig.InsecureSkipVerify = true handler := http.FileServer(http.Dir("testdata")) httpServer := httptest.NewServer(handler) httpsServer := httptest.NewTLSServer(handler) return httpServer.URL, httpsServer.URL, func() { httpServer.Close() httpsServer.Close() } } func decodeString(t *testing.T, s string) interface{} { t.Helper() return decodeReader(t, strings.NewReader(s)) } func decodeReader(t *testing.T, r io.Reader) interface{} { t.Helper() decoder := json.NewDecoder(r) decoder.UseNumber() var doc interface{} if err := decoder.Decode(&doc); err != nil { t.Fatal("invalid json:", err) } return doc } jsonschema-5.3.1/testdata/000077500000000000000000000000001445702222200154635ustar00rootroot00000000000000jsonschema-5.3.1/testdata/JSON-Schema-Test-Suite/000077500000000000000000000000001445702222200214365ustar00rootroot00000000000000jsonschema-5.3.1/testdata/customer.json000066400000000000000000000003601445702222200202160ustar00rootroot00000000000000{ "shipping_address": { "street_address": "1600 Pennsylvania Avenue NW", "city": "Washington", "state": "DC" }, "billing_address": { "street_address": "1st Street SE", "city": "Washington", "state": "DC" } } jsonschema-5.3.1/testdata/customer_schema.json000066400000000000000000000003721445702222200215410ustar00rootroot00000000000000[ { "$schema": "http://json-schema.org/draft-04/schema#", "type": "object", "properties": { "billing_address": { "$ref": "definitions.json#/address" }, "shipping_address": { "$ref": "definitions.json#/address" } } } ] jsonschema-5.3.1/testdata/definitions.json000066400000000000000000000004051445702222200206700ustar00rootroot00000000000000{ "address": { "type": "object", "properties": { "street_address": { "type": "string" }, "city": { "type": "string" }, "state": { "type": "string" } }, "required": ["street_address", "city", "state"] } } jsonschema-5.3.1/testdata/empty schema.json000066400000000000000000000000021445702222200207250ustar00rootroot00000000000000{}jsonschema-5.3.1/testdata/invalid_schema.json000066400000000000000000000000151445702222200213200ustar00rootroot00000000000000{ "type": 1 }jsonschema-5.3.1/testdata/invalid_schemas.json000066400000000000000000000127141445702222200215140ustar00rootroot00000000000000[ { "description": "must be object", "schema": 1 }, { "description": "format must be string", "schema": { "format": 1 } }, { "description": "pattern must be string", "schema": { "pattern": 1 } }, { "description": "pattern must be regex", "schema": { "pattern": "(" } }, { "description": "$ref must be string", "schema": { "$ref": 1 } }, { "description": "patternProperties keys must be regex", "schema": { "patternProperties": { "(": {} } } }, { "description": "patternProperties keys must be regex", "schema": { "$schema": "http://json-schema.org/draft-04/schema#", "patternProperties": { "(": {} } } }, { "description": "invalid $ref json-pointer", "schema": { "$ref": "#/definition/employee%1" } }, { "description": "invalid $ref url", "schema": { "$ref": "http:/localhost/test.json" } }, { "description": "invalid $ref ptr-1", "schema": { "$ref": "#/definition/employee" } }, { "description": "invalid $ref ptr-2", "schema": { "$ref": "#/abcd/employee", "abcd": true } }, { "description": "$ref to invalid schema", "schema": { "$ref": "#/definition/employee", "definitions": { "employee": { "type": 1 } } } }, { "description": "draft3 is not supported", "schema": { "$schema": "http://json-schema.org/draft-03/schema#" } }, { "description": "does not validate with latest draft", "schema": { "$schema": "http://json-schema.org/schema#", "contains": 1 } }, { "description": "$ref schema with syntax error", "schema": { "$ref": "testdata/syntax_error.json#" } }, { "description": "multipleOf must be greater than zero", "schema": { "multipleOf": 0 } }, { "description": "not compile fail", "schema": { "not": { "$ref": "#/unknown" } } }, { "description": "allOf compile fail", "schema": { "allOf": [ { "$ref": "#/unknown" } ] } }, { "description": "anyOf compile fail", "schema": { "anyOf": [ { "$ref": "#/unknown" } ] } }, { "description": "oneOf compile fail", "schema": { "oneOf": [ { "$ref": "#/unknown" } ] } }, { "description": "properties compile fail", "schema": { "properties": { "p1": { "$ref": "#/unknown" } } } }, { "description": "patternProperties compile fail", "schema": { "patternProperties": { "p1": { "$ref": "#/unknown" } } } }, { "description": "additionalProperties compile fail", "schema": { "additionalProperties": { "$ref": "#/unknown" } } }, { "description": "items compile fail", "schema": { "items": { "$ref": "#/unknown" } } }, { "description": "item compile fail", "schema": { "items": [ { "$ref": "#/unknown" } ] } }, { "description": "additionalItems compile fail", "schema": { "items": [{}], "additionalItems": { "$ref": "#/unknown" } } }, { "description": "dependencies compile fail", "schema": { "dependencies": { "p1": { "$ref": "#/unknown" } } } }, { "description": "contains compile fail", "schema": { "contains": { "$ref": "#/unknown" } } }, { "description": "propertyNames compile fail", "schema": { "propertyNames": { "$ref": "#/unknown" } } }, { "description": "a:jsonpointer urlescape", "schema": { "propertyNames": { "$ref": "#/unknown%" } } }, { "description": "b:jsonpointer urlescape", "schema": {}, "fragment": "#/unknown%" }, { "description": "jsonpointer array index", "schema": { "refs": [{}], "propertyNames": { "$ref": "#/refs/unknown" } } }, { "description": "jsonpointer array index outofrange", "schema": { "refs": [{}], "propertyNames": { "$ref": "#/refs/5" } } }, { "description": "schemaRef with wrong jsonpointer", "schema": [{}], "fragment": "#/1" }, { "description": "schemaRef", "schema": [1], "fragment": "#/0" }, { "description": "schemaRef wrong jsonpointer", "schema": [{"$ref": "#/5"}], "fragment": "#/0" }, { "description": "unrecognized vocab", "schema": { "$vocabulary": { "https://github.com/santhosh-tekuri": true } } }, { "description": "$schema must be absolute uri", "schema": { "$schema": "meta.json" } }, { "description": "$schema must be string", "schema": { "$schema": 1 } }, { "description": "$ref with non existing anchor", "schema": { "$ref": "#abcd" } }, { "description": "repeating canonical url", "schema": { "$defs": { "one": { "$id": "http://localhost/test.json" }, "two": { "$id": "http://localhost/test.json" } } } } ] jsonschema-5.3.1/testdata/loop-compile.json000066400000000000000000000021771445702222200207640ustar00rootroot00000000000000{ "$ref": "#/$defs/ref", "$defs": { "ref": { "$ref": "#/$defs/not" }, "not": { "not": { "$ref": "#/$defs/allOf" } }, "allOf": { "allOf": [{ "$ref": "#/$defs/anyOf" }] }, "anyOf": { "anyOf": [{ "$ref": "#/$defs/oneOf" }] }, "oneOf": { "oneOf": [{ "$ref": "#/$defs/dependencies" }] }, "dependencies": { "dependencies": { "prop": { "$ref": "#/$defs/dependentSchemas" } } }, "dependentSchemas": { "dependentSchemas": { "prop": { "$ref": "#/$defs/then" } } }, "then": { "if": true, "then": { "$ref": "#/$defs/else" } }, "else": { "if": true, "else": { "$ref": "#" } } } } jsonschema-5.3.1/testdata/loop-validate.json000066400000000000000000000026731445702222200211260ustar00rootroot00000000000000{ "$ref": "#/$defs/ref", "$defs": { "ref": { "$ref": "#/$defs/not" }, "not": { "not": { "$ref": "#/$defs/allOf" } }, "allOf": { "allOf": [{ "$ref": "#/$defs/anyOf" }] }, "anyOf": { "anyOf": [{ "$ref": "#/$defs/oneOf" }] }, "oneOf": { "oneOf": [{ "$ref": "#/$defs/dependencies" }] }, "dependencies": { "dependencies": { "prop": { "$ref": "#/$defs/dependentSchemas" } } }, "dependentSchemas": { "dependentSchemas": { "prop": { "$ref": "#/$defs/then" } } }, "then": { "if": true, "then": { "$ref": "#/$defs/else" } }, "else": { "if": false, "else": { "$defs": { "xyz": { "$dynamicAnchor": "mno" }, "abc": { "$dynamicAnchor": "mno", "$ref": "#" } }, "$dynamicRef": "#/$defs/else/else/$defs/abc" } } } } jsonschema-5.3.1/testdata/person schema.json000066400000000000000000000003331445702222200211040ustar00rootroot00000000000000{ "type": "object", "properties": { "firstName": { "type": "string" }, "lastName": { "type": "string" } }, "required": ["firstName", "lastName"] } jsonschema-5.3.1/testdata/person.json000066400000000000000000000001001445702222200176530ustar00rootroot00000000000000{ "firstName": "Santhosh Kumar", "lastName": "Tekuri" } jsonschema-5.3.1/testdata/person_schema.json000066400000000000000000000003331445702222200212030ustar00rootroot00000000000000{ "type": "object", "properties": { "firstName": { "type": "string" }, "lastName": { "type": "string" } }, "required": ["firstName", "lastName"] } jsonschema-5.3.1/testdata/remotes/000077500000000000000000000000001445702222200171415ustar00rootroot00000000000000jsonschema-5.3.1/testdata/remotes/metaschema-no-vocabulary.json000066400000000000000000000001771445702222200247270ustar00rootroot00000000000000{ "$schema": "https://json-schema.org/draft/2020-12/schema#", "$ref": "https://json-schema.org/draft/2020-12/schema" } jsonschema-5.3.1/testdata/syntax_error.json000066400000000000000000000000011445702222200211040ustar00rootroot00000000000000{jsonschema-5.3.1/testdata/tests/000077500000000000000000000000001445702222200166255ustar00rootroot00000000000000jsonschema-5.3.1/testdata/tests/draft2019/000077500000000000000000000000001445702222200202415ustar00rootroot00000000000000jsonschema-5.3.1/testdata/tests/draft2019/format.json000066400000000000000000000005101445702222200224200ustar00rootroot00000000000000[ { "description": "format must not be asserted", "schema": { "type": "string", "format": "time" }, "tests": [ { "description": "invalid time", "data": "abcdef", "valid": true } ] } ] jsonschema-5.3.1/testdata/tests/draft2020/000077500000000000000000000000001445702222200202315ustar00rootroot00000000000000jsonschema-5.3.1/testdata/tests/draft2020/anchor.json000066400000000000000000000020521445702222200223750ustar00rootroot00000000000000[ { "description": "same $anchor with different base uri", "schema": { "$id": "http://localhost:1234/root", "$defs": { "A": { "$id": "child1", "allOf": [ { "$id": "child2", "$anchor": "my_anchor", "type": "number" }, { "$anchor": "my_anchor", "type": "string" } ] } }, "$ref": "child1#my_anchor" }, "tests": [ { "description": "$ref should resolve to /$defs/A/allOf/2", "data": "a", "valid": true }, { "description": "$ref should not resolve to /$defs/A/allOf/1", "data": 1, "valid": false } ] } ] jsonschema-5.3.1/testdata/tests/draft2020/optional/000077500000000000000000000000001445702222200220565ustar00rootroot00000000000000jsonschema-5.3.1/testdata/tests/draft2020/optional/content.json000066400000000000000000000104431445702222200244250ustar00rootroot00000000000000[ { "description": "validation of string-encoded content based on media type", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", "contentMediaType": "application/json" }, "tests": [ { "description": "a valid JSON document", "data": "{\"foo\": \"bar\"}", "valid": true }, { "description": "an invalid JSON document; validates true", "data": "{:}", "valid": false }, { "description": "ignores non-strings", "data": 100, "valid": true } ] }, { "description": "validation of binary string-encoding", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", "contentEncoding": "base64" }, "tests": [ { "description": "a valid base64 string", "data": "eyJmb28iOiAiYmFyIn0K", "valid": true }, { "description": "an invalid base64 string (% is not a valid character); validates true", "data": "eyJmb28iOi%iYmFyIn0K", "valid": false }, { "description": "ignores non-strings", "data": 100, "valid": true } ] }, { "description": "validation of binary-encoded media type documents", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", "contentMediaType": "application/json", "contentEncoding": "base64" }, "tests": [ { "description": "a valid base64-encoded JSON document", "data": "eyJmb28iOiAiYmFyIn0K", "valid": true }, { "description": "a validly-encoded invalid JSON document; validates true", "data": "ezp9Cg==", "valid": false }, { "description": "an invalid base64 string that is valid JSON; validates true", "data": "{}", "valid": false }, { "description": "ignores non-strings", "data": 100, "valid": true } ] }, { "description": "validation of binary-encoded media type documents with schema", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", "contentMediaType": "application/json", "contentEncoding": "base64", "contentSchema": { "required": ["foo"], "properties": { "foo": { "type": "string" } } } }, "tests": [ { "description": "a valid base64-encoded JSON document", "data": "eyJmb28iOiAiYmFyIn0K", "valid": true }, { "description": "another valid base64-encoded JSON document", "data": "eyJib28iOiAyMCwgImZvbyI6ICJiYXoifQ==", "valid": true }, { "description": "an invalid base64-encoded JSON document; validates true", "data": "eyJib28iOiAyMH0=", "valid": false }, { "description": "an empty object as a base64-encoded JSON document; validates true", "data": "e30=", "valid": false }, { "description": "an empty array as a base64-encoded JSON document", "data": "W10=", "valid": true }, { "description": "a validly-encoded invalid JSON document; validates true", "data": "ezp9Cg==", "valid": false }, { "description": "an invalid base64 string that is valid JSON; validates true", "data": "{}", "valid": false }, { "description": "ignores non-strings", "data": 100, "valid": true } ] } ] jsonschema-5.3.1/testdata/tests/draft2020/ref.json000066400000000000000000000022461445702222200217040ustar00rootroot00000000000000[ { "description": "$ref must be resolved against the current base", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", "$ref": "#/$defs/foo/$defs/b", "$defs": { "foo":{ "$id": "http://localhost:1234/draft2020-12/ref/foo", "$defs": { "a": { "type": "string" }, "b": { "$defs": { "a": { "type": "number" } }, "$ref": "#/$defs/a" } } } } }, "tests": [ { "description": "$ref resolving against current base", "data": "foo", "valid": true }, { "description": "$ref resolving against current schema", "data": 1, "valid": false } ] } ] jsonschema-5.3.1/testdata/tests/draft2020/vocabulary.json000066400000000000000000000017211445702222200232740ustar00rootroot00000000000000[ { "description": "metaschema without $vocabulary should consider validation vocab", "schema": { "$schema": "http://localhost:1235/metaschema-no-vocabulary.json", "minimum": 10 }, "tests": [ { "description": "minimum must be checked", "data": 5, "valid": false } ] }, { "description": "format-assertion vocab must assert format", "schema": { "$schema": "http://localhost:1234/draft2020-12/format-assertion-true.json", "format": "ipv4" }, "tests": [ { "description": "ipv4 should be valid", "data": "1.1.1.1", "valid": true }, { "description": "non-ipv4 should be invalid", "data": "ABCDEFGH", "valid": false } ] } ] jsonschema-5.3.1/testdata/tests/draft7/000077500000000000000000000000001445702222200200145ustar00rootroot00000000000000jsonschema-5.3.1/testdata/tests/draft7/format.json000066400000000000000000000005051445702222200221770ustar00rootroot00000000000000[ { "description": "format must be asserted", "schema": { "type": "string", "format": "time" }, "tests": [ { "description": "valid time", "data": "12:00:00Z", "valid": true } ] } ] jsonschema-5.3.1/testdata/tests/draft7/ref.json000066400000000000000000000022561445702222200214700ustar00rootroot00000000000000[ { "description": "absolute ref to $id with fragment", "schema": { "$id": "http://example.com/schema-relative-uri-defs1.json", "properties": { "foo": { "$id": "schema-relative-uri-defs2.json", "definitions": { "inner": { "properties": { "bar": { "type": "string" } } } }, "allOf": [ { "$ref": "http://example.com/schema-relative-uri-defs2.json#/definitions/inner" } ] } }, "allOf": [ { "$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 } ] } ] jsonschema-5.3.1/testdata/tests/draft7/schema.json000066400000000000000000000011321445702222200221440ustar00rootroot00000000000000[ { "description": "http trailing hash", "schema": { "$schema": "http://json-schema.org/draft-07/schema#" }, "tests": [] }, { "description": "http no trailing hash", "schema": { "$schema": "http://json-schema.org/draft-07/schema" }, "tests": [] }, { "description": "https trailing hash", "schema": { "$schema": "https://json-schema.org/draft-07/schema#" }, "tests": [] }, { "description": "https no trailing hash", "schema": { "$schema": "https://json-schema.org/draft-07/schema" }, "tests": [] } ]