std/json-schema
JsonSchema reads a JSON Schema of draft 2020-12 and validates a JsonValue against its in-place keywords, with one ValidationError per failed keyword and its place as a JSON Pointer; what it cannot check yet is refused by name.
The generated reference of std/json-schema has every public construct with its signature, its doc comment and its examples.
std/json-schema is JSON Schema, draft 2020-12, written in TorbScript. JsonSchema.read reads a schema from JSON text
into a value, and validate checks a std/json JsonValue against it. This first step is the in-place
keywords: those that look at the value in front of them or hand a part of it to a subschema. Whatever the package
does not check yet it refuses when the schema is read, with an error that names it, so a schema that was read is a
schema that is checked in full.
Import#
use JsonSchema, Validation, ValidationError, JsonSchemaError from "std/json-schema"A schema is read once and applied to any number of documents. The failures are listed, one for each keyword the document fails, each with the place of the value in the document:
use JsonSchema from "std/json-schema"
use Json from "std/json"
const schemaText = """
\{ "type": "object", "required": ["name", "tags"], "additionalProperties": false,
"properties": \{
"name": \{ "type": "string", "minLength": 1 \},
"age": \{ "type": "integer", "minimum": 0 \},
"tags": \{ "type": "array", "items": \{ "type": "string" \}, "uniqueItems": true \}
\} \}
"""
const documentText = """
\{ "name": "", "age": -3, "tags": ["a", 2, "a"], "admin": true \}
"""
match (JsonSchema.read(schemaText), Json().parse(documentText)) {
(Ok(schema), Ok(document)) => {
const result = schema.validate document
print result.valid
for problem in result.errors {
print "{problem.instanceLocation}: {problem.keyword}"
}
}
(Fail(problem), _) => print "Schema refused: {problem}"
(_, Fail(problem)) => print "Not JSON: {problem}"
}It prints:
false
/name: minLength
/age: minimum
/tags: uniqueItems
/tags/1: type
/admin: false/admin is the keyword false: the schema's additionalProperties: false is the schema false for that member.
instanceLocation is a JSON Pointer (RFC 6901) into the document: the empty
string for the document itself, /tags/1 for the second tag, and ~1 and ~0 where a name has a / or a ~.
The keywords#
Every keyword is validated as 2020-12 says.
| Applies to | Keywords |
|---|---|
| any value | type, enum, const |
| numbers | multipleOf, maximum, exclusiveMaximum, minimum, exclusiveMinimum |
| strings | maxLength, minLength, pattern |
| arrays | maxItems, minItems, uniqueItems, maxContains, minContains, prefixItems, items, contains |
| objects | maxProperties, minProperties, required, dependentRequired, dependentSchemas, properties, patternProperties, additionalProperties, propertyNames |
| subschemas | allOf, anyOf, oneOf, not, if with then and else |
A schema may also be true, which accepts everything, or false, which accepts nothing and reports the keyword
false. title, description, default, examples, deprecated, readOnly, writeOnly and $comment are read
and kept in the fields of the schema and evaluate nothing, and a keyword the package does not know is ignored, as the
specification says.
Some details that are easy to get wrong, and that the cases of the JSON Schema test suite pin down:
type: "integer"accepts every number without a fractional part, so1.0is an integer. Numbers areFloat64, as inJsonValue.enum,constanduniqueItemscompare by value, not by spelling:1and1.0are one value,0andfalseare two, and objects are equal whatever the order of their members.maxLengthandminLengthcount characters, not bytes.patternfinds its pattern anywhere in the string.itemsis for the elements after those ofprefixItems.containsneedsminContainsmatching elements (1 by default, and0makes it always pass) and at mostmaxContains.additionalPropertiesis for the members that neitherpropertiesnor a pattern ofpatternPropertiesnames.
What is refused#
Reading a schema is strict, so that nothing is silently left unchecked. Each of these is a JsonSchemaError that names
what it refused and where it stands in the schema:
/properties/a/$ref: `$ref` is not supported yet: references and the unevaluated keywords are a later step
$schema: the schema is of `http://json-schema.org/draft-07/schema#`, and only draft 2020-12 is read
/pattern: the pattern `^(?=a)` is not a regular expression RE2 runs- Another dialect. A
$schemaother thanhttps://json-schema.org/draft/2020-12/schema, at the root or in any subschema. The earlier drafts will be read into the same model in a later step. - A keyword of a later step.
$ref,$dynamicRef,unevaluatedItemsandunevaluatedProperties, which need references and the annotations of a whole schema. - A pattern outside RE2.
patternand the names ofpatternPropertiesare compiled with std/regex, so a lookahead or a backreference is refused when the schema is read, never at validation.
A keyword with a value of the wrong kind ("minLength": "three") is refused the same way, with its place.
Declarations#
JsonSchema#
type JsonSchema {
var boolean: Bool? = None
var schema: String? = None
var title: String? = None
var description: String? = None
var defaultValue: JsonValue? = None
var examples: List<JsonValue> = []
var deprecated: Bool? = None
var readOnly: Bool? = None
var writeOnly: Bool? = None
var comment: String? = None
var types: List<String> = []
var enumeration: List<JsonValue>? = None
var constant: JsonValue? = None
var multipleOf: Float64? = None
var maximum: Float64? = None
var exclusiveMaximum: Float64? = None
var minimum: Float64? = None
var exclusiveMinimum: Float64? = None
var maxLength: Int? = None
var minLength: Int? = None
var pattern: Regex? = None
var maxItems: Int? = None
var minItems: Int? = None
var uniqueItems: Bool? = None
var maxContains: Int? = None
var minContains: Int? = None
var prefixItems: List<JsonSchema>? = None
var items: JsonSchema? = None
var contains: JsonSchema? = None
var maxProperties: Int? = None
var minProperties: Int? = None
var required: List<String>? = None
var dependentRequired: Map<String, List<String>>? = None
var dependentSchemas: Map<String, JsonSchema>? = None
var properties: Map<String, JsonSchema>? = None
var patternProperties: List<PatternSchema>? = None
var additionalProperties: JsonSchema? = None
var propertyNames: JsonSchema? = None
var allOf: List<JsonSchema>? = None
var anyOf: List<JsonSchema>? = None
var oneOf: List<JsonSchema>? = None
var notSchema: JsonSchema? = None
var ifSchema: JsonSchema? = None
var thenSchema: JsonSchema? = None
var elseSchema: JsonSchema? = None
static fn read(text: String): Result<Self, JsonSchemaError>
static fn parse(value: JsonValue): Result<Self, JsonSchemaError>
}
extend JsonSchema {
fn validate(instance: JsonValue): Validation
}A schema is a value. Its fields are the keywords, None (or empty) where the schema does not have one, and a keyword
whose name the language uses has another name: enumeration for enum, constant for const, types for type,
defaultValue for default, notSchema for not, ifSchema, thenSchema and elseSchema for if, then and
else, schema for $schema and comment for $comment. boolean is set for the schemas true and false, which
have no keywords. read reads JSON text and parse a document that is already a JsonValue; both answer a
JsonSchemaError for what they refuse.
validate checks a document and answers a Validation; validating never fails, a document that does not
fit is a result.
Validation and ValidationError#
type Validation with Show, Equals {
valid: Bool
errors: List<ValidationError>
static fn of(errors: List<ValidationError>): Self
fn show(): String
}
type ValidationError with Show, Equals {
keyword: String
schemaLocation: String
instanceLocation: String
message: String
fn show(): String
}A Validation is valid when errors is empty, and show() is valid or one line for each error. A ValidationError is
one failed keyword: keyword is its name ("minLength"), schemaLocation the JSON Pointer of the keyword in the
schema (/properties/name/minLength) and instanceLocation the pointer of the value in the document.
A keyword that tests the value itself reports its own failure. anyOf and oneOf report themselves and are followed
by the failures of their branches, so a reader sees why each branch was not taken; allOf, properties, items,
if, then and else only pass on the failures of their subschemas, and not reports itself when its subschema is
valid.
JsonSchemaError#
type JsonSchemaError with Show, Error {
message: String
location: String = ""
fn show(): String
}The refusal of a schema: message says what, location is the JSON Pointer of the keyword in the schema, and show()
puts the two together.
PatternSchema#
type PatternSchema {
pattern: Regex
schema: JsonSchema
}One member of patternProperties: the compiled pattern and the schema of the members whose name it finds.
Related#
- std/json -
JsonValue, the document this validates and the tree a schema is read from. - std/regex - the RE2 patterns of
patternandpatternProperties. - std/xml - another format with a tree.
- The standard library - the other packages.