Documentation
¶
Overview ¶
Package jsonschema validates JSON documents against families of JSON Schema documents (the Open Charge Alliance's OCPP and OSCP schemas) with github.com/santhosh-tekuri/jsonschema/v6, and reports each failure as a JSON pointer, the schema keyword and a short message of our own.
A Set holds a family of schema documents that refer to each other by relative file name ("phase-indicator.json") or JSON pointer ("#/definitions/X", "other.json#/definitions/Y"). References are resolved against the referring document's $id; when no document carries the resolved $id, the document added under that whole URL is used, then the one added under the same file name (some published sets name a file differently from its $id). Each document is validated by the draft its $schema names (draft-04 to 2020-12; draft-07 when it names none).
What this package adds to the library: the documents are kept in memory as added (corrections can be applied to a decoded copy before Add), every format of JSON Schema 2020-12 is checked by our own checkers with our messages (formats.go, uri.go, idna.go), patterns are ECMA-262 regular expressions (ecma.go), failures are listed in a stable order (see Translate), and CheckSchemas walks the schemas before use to refuse keywords outside a dialect (dialect.go).
format is an assertion in drafts 4, 6 and 7 and an annotation in drafts 2019-09 and 2020-12, as those drafts say; Options.AssertFormat asserts it in every draft.
Instances are decoded with json.Number (Decode), so integers keep their precision and "integer" accepts any number whose fraction is zero.
Example ¶
A family of two schema documents that refer to each other by file name; every failure comes back as pointer, keyword and message, in order.
package main
import (
"errors"
"fmt"
"github.com/cB-Sigma/jsonschema"
)
func main() {
set := jsonschema.NewSet(jsonschema.Options{})
_ = set.Add("meter.json", []byte(`{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"id": {"type": "string", "minLength": 1},
"reading": {"$ref": "reading.json"}
},
"required": ["id", "reading"]
}`))
_ = set.Add("reading.json", []byte(`{
"type": "object",
"properties": {"kWh": {"type": "number", "minimum": 0}, "at": {"type": "string", "format": "date-time"}},
"required": ["kWh"]
}`))
fmt.Println(set.ValidateJSON("meter.json", []byte(`{"id": "m1", "reading": {"kWh": 12.5}}`)))
err := set.ValidateJSON("meter.json", []byte(`{"id": "", "reading": {"kWh": -1, "at": "yesterday"}}`))
var v jsonschema.ValidationError
if errors.As(err, &v) {
for _, e := range v {
fmt.Println(e.Path, e.Keyword)
}
}
}
Output: <nil> /id minLength /reading/at format /reading/kWh minimum
Index ¶
- Constants
- func Decode(data []byte) (any, error)
- func Keywords() []string
- func Keywords2020() []string
- type CheckContext
- type Dialect
- type Error
- type Keyword
- type Options
- type OutputFormat
- type OutputUnit
- type Regexp
- type Set
- func (s *Set) Add(name string, raw []byte) error
- func (s *Set) AddValue(name string, root any) error
- func (s *Set) CheckSchemas(name string, pointers ...string) ([]string, error)
- func (s *Set) Compile(name string) error
- func (s *Set) Names() []string
- func (s *Set) Output(name string, instance any, f OutputFormat) (OutputUnit, error)
- func (s *Set) Validate(name string, instance any) error
- func (s *Set) ValidateGo(name string, v any) error
- func (s *Set) ValidateJSON(name string, data []byte) error
- type Style
- type ValidationError
Examples ¶
Constants ¶
const MaxSteps = 1 << 22
MaxSteps bounds the work of the backtracking matcher for one MatchString. A pattern that needs more steps does not match.
Variables ¶
This section is empty.
Functions ¶
func Decode ¶
Decode parses JSON the way Validate expects instances (numbers as json.Number); trailing data is an error.
func Keywords ¶
func Keywords() []string
Keywords returns, sorted, the draft-07 keywords (and $schema, $id, $comment, title, description, default, examples, definitions, readOnly, writeOnly and contentMediaType/contentEncoding, which carry no check) that CheckSchemas accepts by default. The slice is the caller's.
func Keywords2020 ¶ added in v0.3.0
func Keywords2020() []string
Keywords2020 returns, sorted, the keywords of drafts 2019-09 and 2020-12 that are not in Keywords. Without a dialect's Allowed, CheckSchemas accepts them in documents of those drafts. The slice is the caller's.
Types ¶
type CheckContext ¶
type CheckContext struct {
// contains filtered or unexported fields
}
CheckContext is what a keyword's Check sees while CheckSchemas walks.
func (*CheckContext) Fail ¶
func (c *CheckContext) Fail(format string, a ...any)
Fail reports a problem with the keyword's value.
func (*CheckContext) FailAt ¶
func (c *CheckContext) FailAt(pointer, format string, a ...any)
FailAt reports a problem at another pointer of the document.
func (*CheckContext) Pointer ¶
func (c *CheckContext) Pointer() string
Pointer is the JSON pointer of the keyword in its document.
type Dialect ¶
type Dialect struct {
// Keywords adds keywords to those CheckSchemas accepts, with a check
// of their value.
Keywords map[string]Keyword
// Allowed says which keywords CheckSchemas accepts beside those in
// Keywords; nil accepts the package's Keywords list.
Allowed func(keyword string) bool
// ObjectSchemasOnly makes CheckSchemas refuse boolean schemas (other
// than additionalProperties: false/true, which is a keyword form).
ObjectSchemasOnly bool
}
A Dialect is what CheckSchemas accepts in a family of schemas: the keywords allowed, and checks of a keyword's value. Validation itself is the library's (see jsonschema.go); a dialect only refuses, before use, schemas the validator would not check as their authors meant. Every field is optional. A Dialect must not be changed once a Set uses it; the Check functions may then run on several goroutines at once.
func OCA ¶
func OCA() *Dialect
OCA is the dialect of the Open Charge Alliance's OCPP schemas: OCPP 1.6 (draft-04), OCPP 2.0.1 and 2.1 (draft-06). What they use is a subset of draft-07 with the same meaning, plus annotations: "id" (the draft-04 document identifier), "javaType" and "comment". Any other keyword is refused by CheckSchemas, so a later schema that relies on a keyword this package does not implement cannot be checked only halfway. The draft-04 boolean form of exclusiveMinimum/exclusiveMaximum means something else in draft-07 and is refused too.
type Error ¶
type Error struct {
// Path is the JSON pointer of the failing value in the instance.
Path string
// Keyword is the schema keyword that failed ("false" for a false
// schema, "json" for a document that is not JSON).
Keyword string
// Message is our wording of the failure, without the path. It may
// change in any release; branch on Keyword and Path instead.
Message string
}
Error is one failed check.
type Keyword ¶
type Keyword struct {
// Check runs when CheckSchemas meets the keyword in a schema, to
// report a value of the wrong shape.
Check func(c *CheckContext, schema map[string]any, value any)
}
Keyword is the dialect's treatment of one keyword.
type Options ¶ added in v0.2.0
type Options struct {
// Dialect, when set, is applied by CheckSchemas: keywords outside it
// are refused and its keyword checks run. nil makes CheckSchemas
// accept the keywords Keywords lists (and Keywords2020 in drafts
// 2019-09 and later). The set keeps the pointer; the caller must not
// change the dialect afterwards.
Dialect *Dialect
// AssertFormat makes "format" an assertion in every draft. Without
// it, format is an assertion in drafts 4, 6 and 7, and an annotation
// in drafts 2019-09 and 2020-12 unless the document's meta-schema
// requires the format-assertion vocabulary (2020-12) or the format
// vocabulary (2019-09). Unknown formats are never asserted.
AssertFormat bool
// AssertContent makes contentEncoding (base64) and contentMediaType
// (application/json) assertions, and contentSchema with them (2019-09
// and later). Without it they are annotations.
AssertContent bool
// GoRegexp reads "pattern", "patternProperties" and format "regex"
// with Go's regexp syntax (RE2), as v0.1 did, instead of ECMA-262
// (CompileRegexp).
GoRegexp bool
// DefaultDraft is the meta-schema URL of the draft that documents
// without $schema are read by, one of the URLs of drafts 4, 6, 7,
// 2019-09 and 2020-12 ("https://json-schema.org/draft/2020-12/schema").
// Empty means draft-07, as in v0.1 and v0.2. Another value makes
// Compile fail.
DefaultDraft string
// Draft4Integer refuses, in draft-04 documents, a number written with
// a fraction or an exponent (1.0, 1e2) where "type" allows integer but
// not number, as draft-04 defines integer. Without it a number whose
// value is whole is an integer in every draft.
Draft4Integer bool
}
Options choose how a Set made by NewSet validates. The zero value gives the defaults each field describes.
type OutputFormat ¶ added in v0.3.0
type OutputFormat int
OutputFormat is one of the four output structures of JSON Schema 2019-09 and 2020-12 (core specification, section 12.4 of 2020-12).
const ( // OutputFlag is the result alone: {"valid": false}. OutputFlag OutputFormat = iota + 1 // OutputBasic is a flat list: the failures of an invalid instance, or // the annotations of a valid one. OutputBasic // OutputDetailed is a tree that follows the schema, with only the // failing (or, when valid, annotating) branches, and a unit with a // single child replaced by that child. OutputDetailed // OutputVerbose is the whole tree: every subschema applied to every // instance location, valid or not. OutputVerbose )
type OutputUnit ¶ added in v0.3.0
type OutputUnit struct {
Valid bool
// KeywordLocation is the JSON pointer of the keyword or subschema in
// the schema, through any $ref, $dynamicRef or $recursiveRef followed.
KeywordLocation string
// AbsoluteKeywordLocation is the same place as an absolute URI: the
// enclosing schema resource's $id (a document without one is
// "mem:///" and its name in the set) and a pointer in it.
AbsoluteKeywordLocation string
// InstanceLocation is the JSON pointer of the value in the instance.
InstanceLocation string
// Error is our message for a failed keyword (empty on a unit whose
// failure is its Errors).
Error string
// Annotation is the value a keyword annotates the instance with, as
// JSON; nil when the unit carries none.
Annotation json.RawMessage
// Errors are the nested units that failed; Annotations the nested
// units that passed.
Errors []OutputUnit
Annotations []OutputUnit
// contains filtered or unexported fields
}
OutputUnit is one unit of the specification's output.
func (OutputUnit) MarshalJSON ¶ added in v0.3.0
func (u OutputUnit) MarshalJSON() ([]byte, error)
MarshalJSON writes the unit with the specification's property names; a flag result is {"valid": ...} alone.
func (OutputUnit) String ¶ added in v0.3.0
func (u OutputUnit) String() string
String is the unit as indented JSON, for logs and tests.
type Regexp ¶ added in v0.2.0
type Regexp struct {
// contains filtered or unexported fields
}
Regexp is an ECMA-262 regular expression, read with the u flag (code points, strict escapes), as JSON Schema's "pattern", "patternProperties" and format "regex" use them.
A pattern without lookaround, back references, multiline anchors or counted repeats above 1000 is translated to Go's regexp (RE2) syntax and matched in linear time; the ECMA-262 meaning of \s, ".", "$", \cX, empty classes and the rest is kept by the translation. Any other pattern is matched by a backtracking matcher that follows ECMA-262's matching rules, lookbehind included; a match that takes more than a fixed number of steps (see MaxSteps) is reported as no match.
func CompileRegexp ¶ added in v0.2.0
CompileRegexp parses an ECMA-262 regular expression (with the u flag) and refuses one the specification refuses: unknown escapes such as \a, inline flag groups such as (?i), Python's (?P<name>), ranges out of order, a quantifier with nothing to repeat, references to groups that do not exist, and unknown Unicode properties.
func (*Regexp) MatchString ¶ added in v0.2.0
MatchString reports whether the pattern matches anywhere in s.
type Set ¶
type Set struct {
// contains filtered or unexported fields
}
Set is a family of schema documents. It is safe for concurrent use once every document is added.
func NewSet ¶
NewSet returns an empty set with the options; the zero Options assert format in drafts 4, 6 and 7 only (see Options.AssertFormat) and refuse no keyword in CheckSchemas.
Example (Options) ¶
In a 2020-12 document format is an annotation unless the set asserts it; patterns are ECMA-262.
package main
import (
"fmt"
"github.com/cB-Sigma/jsonschema"
)
func main() {
schema := []byte(`{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "string",
"format": "hostname",
"pattern": "^(?!-)[a-z0-9.-]+$"
}`)
for _, assert := range []bool{false, true} {
set := jsonschema.NewSet(jsonschema.Options{AssertFormat: assert})
_ = set.Add("host.json", schema)
fmt.Println(assert, set.ValidateJSON("host.json", []byte(`"gw..example"`)))
}
}
Output: false <nil> true /: gw..example is not a host name
func (*Set) AddValue ¶
AddValue adds a schema document that is already decoded (map[string]any, []any, string, bool, nil and json.Number values), for instance one read from YAML.
func (*Set) CheckSchemas ¶
CheckSchemas walks the schemas at the given JSON pointers of document name, and every schema they contain or reference, before any validation: keywords the dialect does not allow are refused, each keyword's Check runs, references must resolve, and a schema must not reach itself through $ref, allOf, anyOf, oneOf, not, if, then or else alone (validating it would never reach a value). Errors carry pointers into the documents. It returns the pointers of the schemas it visited in document name.
func (*Set) Compile ¶
Compile compiles the named document (and what it references) once, so that a schema the validator refuses is reported before any validation; later validations reuse the result. The first call freezes the set: Add fails after it.
func (*Set) Output ¶ added in v0.3.0
func (s *Set) Output(name string, instance any, f OutputFormat) (OutputUnit, error)
Output validates an instance (as Validate does) and returns the result in one of the specification's output formats. The result's Valid is always the outcome of Validate. A schema that cannot be compiled is an error.
How the tree is built: each subschema is evaluated against the part of the instance it applies to, failures come from the library's evaluation, and annotations are read from the schema documents. A subschema reached through $dynamicRef or $recursiveRef is shown at its static target; whether it passed comes from the evaluation in context.
Example ¶
The specification's basic output format: each failure with its keyword location, absolute keyword location and instance location.
package main
import (
"fmt"
"github.com/cB-Sigma/jsonschema"
)
func main() {
set := jsonschema.NewSet(jsonschema.Options{})
_ = set.Add("evse.json", []byte(`{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://example.com/evse",
"properties": {"evseId": {"type": "integer", "minimum": 0}}
}`))
inst, _ := jsonschema.Decode([]byte(`{"evseId": -1}`))
out, _ := set.Output("evse.json", inst, jsonschema.OutputBasic)
fmt.Println(out.Valid)
for _, e := range out.Errors {
fmt.Println(e.KeywordLocation, e.AbsoluteKeywordLocation, e.InstanceLocation, e.Error)
}
}
Output: false /properties/evseId/minimum https://example.com/evse#/properties/evseId/minimum /evseId must be >= 0.00000000
func (*Set) Validate ¶
Validate checks an instance decoded with Decode (or any value built of map[string]any, []any, string, bool, nil, json.Number and float64). A failed check is a ValidationError; a schema that cannot be compiled is another error.
func (*Set) ValidateGo ¶ added in v0.3.0
ValidateGo validates any Go value (a struct, a map, a slice) as the JSON that encoding/json makes of it, so struct tags and MarshalJSON methods apply. A value that cannot be encoded is a ValidationError with keyword "json".
type Style ¶
type Style struct {
// Bound prints the limit of minimum, maximum and the exclusive bounds
// (default: eight decimals); Multiple prints multipleOf's divisor
// (default: a fraction such as 1/100).
Bound, Multiple func(*big.Rat) string
// Pattern starts the message of a failed pattern (default "does not
// match "); the pattern follows.
Pattern string
// ClosestBranch adds, after an anyOf or oneOf that no branch passes,
// the failures of the branch with the fewest, each message starting
// "(closest branch) ".
ClosestBranch bool
// Max caps the failures listed (0: no cap).
Max int
// Last lists keywords whose failures at a location come after every
// other failure there and below it, in this order.
Last []string
}
Style is how Translate words its messages. The zero value is the wording of a Set.
type ValidationError ¶
type ValidationError []Error
ValidationError lists every failed check of one validation. Callers reach it with errors.As (a value, not a pointer).
func Translate ¶
func Translate(err error, instance any, style Style, schemaAt func(loc string) map[string]any) ValidationError
Translate turns a validation failure of github.com/santhosh-tekuri/jsonschema/v6 into a ValidationError: one Error per failed check, with our messages, listed depth first by instance location (members by name, items by index), and at one location in keyword order: type, enum, const, the object, array, string and number keywords, then dependencies, contains, anyOf, oneOf and not. Failures under allOf, $ref, if/then/else and propertyNames are listed where they occur; anyOf and oneOf report one failure each (with the closest branch if the style asks). schemaAt, when not nil, finds a schema object by the location the library reports, to name multiple types in the schema's order. Any other error is returned as nil.
Translate reads the error values of major version 6 of that library: it is for callers that compile schemas with v6 themselves, and stays tied to v6 for the whole of this module's v1.
func (ValidationError) Error ¶
func (v ValidationError) Error() string
Error joins the failures with "; ". Unlike the package's other errors it carries no "jsonschema:" prefix, so that it can be shown to the sender of the document as it is.