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.
- Installation
- Quick start
- Defining a schema in Pharo
- Loading a schema from JSON
- Validating a value
- Reading and writing values
- Serializing a schema
- Constraints
- Supported keywords
- Supported formats
- Adding a custom format
- What is not (yet) supported
Load it into Pharo via Metacello:
Metacello new
repository: '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.
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 := JSONSchema fromString: '{
"type": "object",
"properties": {
"name": { "type": "string" },
"age": { "type": "integer", "minimum": 0 }
},
"required": [ "name" ]
}'.
"...and validate a parsed value against it."
value := NeoJSONReader fromString: '{ "name": "John Doe", "age": 42 }'.
schema validate: value. "returns the schema; raises a JSONSchemaError if invalid"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"
JSONSchema stringWithFormat: '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 -> (JSONSchema stringWithFormat: '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:
JSONSchemaArray new items: JSONSchema integer. "an array of integers"Given a JSON Schema document as a string, instantiate a schema with:
schema := JSONSchema fromString: schemaString.This is the counterpart of serializing a schema and understands the supported keywords below.
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 := JSONSchema fromString: '{ "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: JSONSchemaError
do: [ :error | false ].All validation errors are subclasses of JSONSchemaError, so you can catch them at the granularity you need:
| Exception | Raised when |
|---|---|
JSONTypeError |
the value has the wrong JSON type |
JSONConstraintError |
a constraint fails (range, length, pattern, format, enum, const, composition, …) |
JSONSchemaMissingRequiredProperty |
a required property is absent |
JSONInvalidPropertyError |
a property is not allowed |
JSONFormatError |
a value does not match its declared format |
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.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 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.
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::
| Group | Keywords |
|---|---|
| Type / value | type (a single name or an array of names), enum, const |
| Numbers | minimum, maximum, exclusiveMinimum, exclusiveMaximum, multipleOf |
| Strings | minLength, maxLength, pattern, format |
| Arrays | items (single schema or tuple), additionalItems, minItems, maxItems, uniqueItems, contains |
| Objects | properties, required, patternProperties, propertyNames, dependencies, minProperties, maxProperties |
| Composition | allOf, anyOf, oneOf, not |
| Conditional | if / then / else |
| References | $ref to a local pointer (#/definitions/…, #/$defs/…) |
| Boolean schemas | a bare true (accept anything) or false (reject everything) as a whole schema |
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.
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:
JSONFormatSlug class >> formatName [
^ 'slug' ]
JSONFormatSlug class >> basicConvertString: aString [
^ aString ]
JSONFormatSlug class >> validateString: aString [
(aString allSatisfy: [ :c | c isLowercase or: [ c isDigit or: [ c = $- ] ] ])
ifFalse: [ JSONConstraintError signal: aString printString, ' is not a valid slug' ] ]{ "type": "string", "format": "slug" } will now reject values that are not slugs.
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.