Migrate to santhosh-tekuri/jsonschema (#168)

* Migrate to santhosh-tekuri/jsonschema
This commit is contained in:
Yann Hamon 2023-01-23 19:22:20 +01:00 committed by GitHub
parent 84afe70659
commit ee7c498580
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
65 changed files with 4651 additions and 9106 deletions

View file

@ -0,0 +1,4 @@
.vscode
.idea
*.swp
jv

175
vendor/github.com/santhosh-tekuri/jsonschema/v5/LICENSE generated vendored Normal file
View file

@ -0,0 +1,175 @@
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.

View file

@ -0,0 +1,215 @@
# jsonschema v5.1.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.io](https://codecov.io/github/santhosh-tekuri/jsonschema/coverage.svg?branch=master)](https://codecov.io/github/santhosh-tekuri/jsonschema?branch=master)
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/v5/cmd/jv@latest`
```bash
jv [-draft INT] [-output FORMAT] [-assertformat] [-assertcontent] <json-schema> [<json-doc>]...
-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 `<json-doc>` arguments are passed, it simply validates the `<json-schema>`.
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
## Validating YAML Documents
since yaml supports non-string keys, such yaml documents are rendered as invalid json documents.
yaml parser returns `map[interface{}]interface{}` for object, whereas json parser returns `map[string]interface{}`.
this package accepts only `map[string]interface{}`, so we need to manually convert them to `map[string]interface{}`
https://play.golang.org/p/Hhax3MrtD8r
the above example shows how to validate yaml document with jsonschema.
the conversion explained above is implemented by `toStringKeys` function

View file

@ -0,0 +1,771 @@
package 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)
// AssertFormat for specifications >= draft2019-09.
AssertFormat bool
// 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), 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.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, 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 := range vocab.(map[string]interface{}) {
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
}
}
}
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") {
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)
s.decoder, _ = Decoders[s.ContentEncoding]
}
if mediaType, ok := m["contentMediaType"]; ok {
s.ContentMediaType = mediaType.(string)
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
}

View file

@ -0,0 +1,29 @@
package 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)
}

49
vendor/github.com/santhosh-tekuri/jsonschema/v5/doc.go generated vendored Normal file
View file

@ -0,0 +1,49 @@
/*
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

1432
vendor/github.com/santhosh-tekuri/jsonschema/v5/draft.go generated vendored Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,129 @@
package 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] + "'"
}

View file

@ -0,0 +1,116 @@
package 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...)
}

View file

@ -0,0 +1,567 @@
package 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
}

View file

@ -0,0 +1,38 @@
// 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
}

View file

@ -0,0 +1,60 @@
package 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)
}

View file

@ -0,0 +1,77 @@
package 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,
}
}

View file

@ -0,0 +1,280 @@
package 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
}

View file

@ -0,0 +1,826 @@
package jsonschema
import (
"bytes"
"encoding/json"
"fmt"
"math/big"
"net/url"
"regexp"
"strconv"
"strings"
"unicode/utf8"
)
// A Schema represents compiled version of json-schema.
type Schema struct {
Location string // absolute location
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
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, doc interface{}) *Schema {
// fill with default values
s := &Schema{
Location: url + floc,
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 {
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))
}
}
}
}
// 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 sch.DynamicAnchor != "" {
// 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
}
}
// 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)
}