Skip to content

Repository files navigation

JSONSchema

Build Status

This is an implementation of JSON Schema for the Pharo language. It is used to define the structure and values of a JSON document, to validate a value against that structure, and to read/write values through it. A schema can also be externalized to JSON so it can be consumed by a third party.

The framework is a single package, JSONSchema-Core, with NeoJSON as its only dependency.

Contents

Installation

Load it into Pharo via Metacello:

Metacellonewrepository:'github://zweidenker/JSONSchema';
baseline:#JSONSchema;
load.

This loads the default group (JSONSchema-Core + its tests). The baseline also declares a separate Testsuite group that adds the generated tests of the official, cross-language JSON-Schema-Test-Suite; load it with load: #('Testsuite') if you want to run them.

Quick start

There are two ways to get a schema: build it with the Pharo DSL, or load it from a JSON Schema document. Either way you get a JSONSchema object you can validate values with.

"Load a schema from a JSON Schema document..."
schema :=JSONSchemafromString:'{ "type": "object", "properties": { "name": { "type": "string" }, "age": { "type": "integer", "minimum": 0 } }, "required": [ "name" ]}'.
"...and validate a parsed value against it."
value :=NeoJSONReaderfromString:'{ "name": "John Doe", "age": 42 }'.
schema validate: value. "returns the schema; raises a JSONSchemaError if invalid"

Defining a schema in Pharo

The class side of JSONSchema offers a small, readable DSL for the primitive types:

JSONSchema string.
JSONSchema number.
JSONSchema integer.
JSONSchema boolean.
JSONSchema any. "accepts anything"JSONSchemastringWithFormat:'date-time'. "a typed string with a format"JSONSchema string enum:#('red' 'green' 'blue').

An object schema is written with the literal array syntax and asJSONSchema:

schema := {
#name->JSONSchema string.
#dateAndTime-> (JSONSchemastringWithFormat:'date-time').
#numberOfPets->JSONSchema number } asJSONSchema.

which describes the following JSON:

{
"name": "John Doe",
"dateAndTime": "1970-01-01T14:00:00+00:00",
"numberOfPets": 3
}

Schemas can be nested to any depth — a property value is just another schema:

schema := {
#name->JSONSchema string.
#address-> {
#street->JSONSchema string.
#number->JSONSchema number } } asJSONSchema.

Arrays are defined with JSONSchemaArray:

JSONSchemaArraynewitems:JSONSchema integer. "an array of integers"

Loading a schema from JSON

Given a JSON Schema document as a string, instantiate a schema with:

schema :=JSONSchemafromString: schemaString.

This is the counterpart of serializing a schema and understands the supported keywords below.

Validating a value

validate: is the core operation. It takes an already parsed value (a Dictionary, Array, String, number, Boolean or nil — for example the result of NeoJSONReader fromString:) and checks it against the schema:

schema :=JSONSchemafromString:'{ "type": "integer", "minimum": 10 }'.
schema validate:42. "OK — returns the schema"
schema validate:3. "raises JSONConstraintError: 3 must be greater than or equal to 10"

On success validate: returns the schema. On failure it signals an exception, so wrap it if you want a boolean result:

isValid := [ schema validate: value. true ]
on:JSONSchemaErrordo: [ :error | false ].

All validation errors are subclasses of JSONSchemaError, so you can catch them at the granularity you need:

ExceptionRaised when
JSONTypeErrorthe value has the wrong JSON type
JSONConstraintErrora constraint fails (range, length, pattern, format, enum, const, composition, …)
JSONSchemaMissingRequiredPropertya required property is absent
JSONInvalidPropertyErrora property is not allowed
JSONFormatErrora value does not match its declared format

Reading and writing values

Beyond plain validation a schema can materialize a value from JSON and write it back out. Reading validates and converts formatted values (e.g. a date-time string becomes a DateAndTime):

value := schema readString: jsonString.

For the object schema above, value has name as a String, dateAndTime as a DateAndTime and numberOfPets as a SmallInteger.

Writing produces JSON and guarantees that only valid JSON leaves your system:

jsonString := schema write: value.

Serializing a schema

A schema is itself serializable to a JSON Schema document:

schema jsonString. "compact"
schema jsonStringPretty. "indented"

For the { name, numberOfPets, dateAndTime } schema this yields:

{
"type": "object",
"properties": {
"name": { "type": "string" },
"numberOfPets": { "type": "number" },
"dateAndTime": { "type": "string", "format": "date-time" }
}
}

Constraints

Constraints refine a type. When building schemas in Pharo they are reachable through accessors on the schema object, e.g. the numeric interval:

numberSchema :=JSONSchema number.
numberSchema interval
minimum:1;
exclusiveMaximum:100. "1 <= value < 100"

The same constraints are available as keywords when loading a schema from JSON — see the table below.

Supported keywords

The core covers the Draft-04 keyword set plus several keywords from later drafts. All of the following are honored by fromString: and enforced by validate::

GroupKeywords
Type / valuetype (a single name or an array of names), enum, const
Numbersminimum, maximum, exclusiveMinimum, exclusiveMaximum, multipleOf
StringsminLength, maxLength, pattern, format
Arraysitems (single schema or tuple), additionalItems, minItems, maxItems, uniqueItems, contains
Objectsproperties, required, patternProperties, propertyNames, dependencies, minProperties, maxProperties
CompositionallOf, anyOf, oneOf, not
Conditionalif / then / else
References$ref to a local pointer (#/definitions/…, #/$defs/…)
Boolean schemasa bare true (accept anything) or false (reject everything) as a whole schema

Supported formats

format is validated for these string formats:

date, date-time, time, email, idn-email, hostname, idn-hostname, ipv4, ipv6, uri, uri-reference, iri, iri-reference, uri-template, json-pointer, relative-json-pointer, regex.

Unknown or unsupported format names are tolerated (ignored), matching the JSON Schema default that format is an annotation unless a validator exists for it.

Adding a custom format

Create a subclass of JSONFormat (or of JSONBasicStringFormat for string formats) and implement three class-side methods: formatName, basicConvertString: and a validateString: that signals a JSONSchemaError for a malformed value. Formats are looked up by name, so the new one is picked up automatically wherever it is referenced:

JSONFormatSlugclass>> formatName [
^'slug' ]
JSONFormatSlugclass>>basicConvertString: aString [
^ aString ]
JSONFormatSlugclass>>validateString: aString [
(aString allSatisfy: [ :c | c isLowercase or: [ c isDigit or: [ c = $- ] ] ])
ifFalse: [ JSONConstraintErrorsignal: aString printString, ' is not a valid slug' ] ]

{ "type": "string", "format": "slug" } will now reject values that are not slugs.

What is not (yet) supported

This is intentionally not a full implementation of the specification. Known gaps:

  • Remote / external $ref — only local pointers within the same document are resolved.
  • The 2020-12 vocabulary machinery$dynamicRef / $dynamicAnchor, unevaluatedProperties / unevaluatedItems, $vocabulary.
  • contentEncoding / contentMediaType.
  • Some ECMA-262 regex edge semantics differ from Pharo's regex engine.

If you need something that is missing you are welcome to open a pull request, or a ticket in this repository.

About

Implementation of JSON schema for pharo

Topics

Resources

Stars

17 stars

Watchers

4 watching

Forks

Releases

Packages

Contributors

Languages