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.

Diese Seite gibt es noch nicht auf Deutsch, deshalb steht sie hier auf Englisch. Zur englischen Seite

Die generierte Referenz von std/json-schema zeigt jede öffentliche Deklaration mit Signatur, Doc-Kommentar und Beispielen.

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 toKeywords
any valuetype, enum, const
numbersmultipleOf, maximum, exclusiveMaximum, minimum, exclusiveMinimum
stringsmaxLength, minLength, pattern
arraysmaxItems, minItems, uniqueItems, maxContains, minContains, prefixItems, items, contains
objectsmaxProperties, minProperties, required, dependentRequired, dependentSchemas, properties, patternProperties, additionalProperties, propertyNames
subschemasallOf, 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, so 1.0 is an integer. Numbers are Float64, as in JsonValue.
  • enum, const and uniqueItems compare by value, not by spelling: 1 and 1.0 are one value, 0 and false are two, and objects are equal whatever the order of their members.
  • maxLength and minLength count characters, not bytes. pattern finds its pattern anywhere in the string.
  • items is for the elements after those of prefixItems. contains needs minContains matching elements (1 by default, and 0 makes it always pass) and at most maxContains.
  • additionalProperties is for the members that neither properties nor a pattern of patternProperties names.

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 $schema other than https://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, unevaluatedItems and unevaluatedProperties, which need references and the annotations of a whole schema.
  • A pattern outside RE2. pattern and the names of patternProperties are 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.

  • std/json - JsonValue, the document this validates and the tree a schema is read from.
  • std/regex - the RE2 patterns of pattern and patternProperties.
  • std/xml - another format with a tree.
  • The standard library - the other packages.
Esc