1 of 65

Maintaining JSON Schema at Scale

Jason Desrosiers

2 of 65

Intro / Who am I

  • Active in the JSON Schema Community since 2013
  • Core contributor to the JSON Schema Specification and Documentation
  • Top answerer in StackOverflow for the "jsonschema" tag
  • Experience with large scale APIs using JSON Hyper-Schema
  • Author of Hyperjump JSON Schema Validator and https://json-shema.hyperjump.io
  • Author of the OpenAPI 3.1 meta-schemas

3 of 65

Treat Schemas as Code

  • Single Responsibility
    • One schema pre concept
    • Reasonably sized schemas
  • DRY
    • Reuse schemas by referencing them
  • Consistent Code Style
  • Understand Performance Characteristics
    • Avoid patterns that have unnecessarily slow performance
  • Understand Error Response Behaviors
    • Avoid patterns that give poor error messages
  • Test Coverage

4 of 65

Schema Identification and Referencing

5 of 65

Schema Identification and Referencing

  • Schemas are identified by URIs using $id

{� "$schema": "https://json-schema.org/draft/2020-12/schema",� "$id": "https://example.com/schemas/address",�� "type": "object",� "properties": {� }�}

{� "$schema": "https://json-schema.org/draft/2020-12/schema",� "$id": "https://example.com/schemas/customer",�� "type": "object",� "properties": {� "givenName": { "$ref": "#/$defs/name" },� "surname": { "$ref": "#/$defs/name" },� "address": { "$ref": "/schemas/address" }� },�� "$defs": {� "name": { "type": "string", "minimum": 1, "maximum": 128 }� }�}

6 of 65

Schema Identification and Referencing

  • Schemas are identified by URIs using $id
  • Schemas are referenced by their identifier using $ref
  • $ref resolves against the schema's identifier
    • /schemas/address resolves against,
    • https://example.com/schemas/customer
    • to get https://example.com/schemas/address

{� "$schema": "https://json-schema.org/draft/2020-12/schema",� "$id": "https://example.com/schemas/address",�� "type": "object",� "properties": {� }�}

{� "$schema": "https://json-schema.org/draft/2020-12/schema",� "$id": "https://example.com/schemas/customer",�� "type": "object",� "properties": {� "givenName": { "$ref": "#/$defs/name" },� "surname": { "$ref": "#/$defs/name" },� "address": { "$ref": "/schemas/address" }� },�� "$defs": {� "name": { "type": "string", "minimum": 1, "maximum": 128 }� }�}

7 of 65

Schema Identification and Referencing

  • Schemas are identified by URIs using $id
  • Schemas are referenced by their identifier using $ref
  • $ref resolves against the schema's identifier
    • /schemas/address resolves against,
    • https://example.com/schemas/customer
    • to get https://example.com/schemas/address
  • Same behavior as an HTML document in your browser
    • $id behaves like <base/>
    • $ref behaves like <a/>

{� "$schema": "https://json-schema.org/draft/2020-12/schema",� <base href="https://example.com/schemas/address">�� "type": "object",� "properties": {� }�}

{� "$schema": "https://json-schema.org/draft/2020-12/schema",� <base href="https://example.com/schemas/customer">�� "type": "object",� "properties": {� "givenName": { "$ref": <a href="#/$defs/name"> },� "surname": { "$ref": <a href="#/$defs/name"> },� "address": { "$ref": <a href="/schemas/address"> }� },�� "$defs": {� "name": { "type": "string", "minimum": 1, "maximum": 128 }� }�}

8 of 65

Schema Identification and Referencing

  • Use $defs for schema local reuse
    • $defs is short for "definitions"
    • URI fragments are interpreted as JSON Pointers (RFC-6901) that point to a sub-schema.

{� "$schema": "https://json-schema.org/draft/2020-12/schema",� "$id": "https://example.com/schemas/address",�� "type": "object",� "properties": {� }�}

{� "$schema": "https://json-schema.org/draft/2020-12/schema",� "$id": "https://example.com/schemas/customer",�� "type": "object",� "properties": {� "givenName": { "$ref": "#/$defs/name" },� "surname": { "$ref": "#/$defs/name" },� "address": { "$ref": "/schemas/address" }� },�� "$defs": {� "name": { "type": "string", "minimum": 1, "maximum": 128 }� }�}

9 of 65

Schema Identification and Referencing

  • Use $defs for schema local reuse
    • $defs is short for "definitions"
    • URI fragments are interpreted as JSON Pointers (RFC-6901) that point to a sub-schema.
  • Use $anchor for sub-schemas intended to be referenced from other schemas

{� "$schema": "https://json-schema.org/draft/2020-12/schema",� "$id": "https://example.com/schemas/common",�� "$defs": {� "non-empty-string": {� "$anchor": "non-empty-string",� "type": "string",� "minimum": 1 }� }�}

{� "$schema": "https://json-schema.org/draft/2020-12/schema",� "$id": "https://example.com/schemas/customer",�� "type": "object",� "properties": {� "givenName": {� "$ref": "/schemas/common#non-empty-string"� },� "surname": {� "$ref": "/schemas/common#non-empty-string"� },� "address": { "$ref": "/schemas/address" }� }�}

10 of 65

Schema Identification and Referencing

  • Use $defs for schema local reuse
    • $defs is short for "definitions"
    • URI fragments are interpreted as JSON Pointers (RFC-6901) that point to a sub-schema.
  • Use $anchor for sub-schemas intended to be referenced from other schemas�
  • $anchor is analogous to <a id=""> in HTML

{� "$schema": "https://json-schema.org/draft/2020-12/schema",� "$id": "https://example.com/schemas/common",�� "$defs": {� "non-empty-string": {� <a id="non-empty-string"/>,� "type": "string",� "minimum": 1� }� }�}

{� "$schema": "https://json-schema.org/draft/2020-12/schema",� "$id": "https://example.com/schemas/customer",�� "type": "object",� "properties": {� "givenName": {� "$ref": <a href="/schemas/common#non-empty-string"/>� },� "surname": {� "$ref": <a href="/schemas/common#non-empty-string"/>� },� "address": { "$ref": "/schemas/address" }� }�}

11 of 65

Schema Identification and Referencing

  • $ids are not necessarily network accessible

import JsonSchema from "@hyperjump/json-schema";��const customerSchema = JsonSchema.get("https://example.com/schemas/customer"); ❌ Not found�const isValid = JsonSchema.validate(customerSchema, customer).valid;

12 of 65

Schema Identification and Referencing

  • $ids are not necessarily network accessible

import JsonSchema from "@hyperjump/json-schema";��JsonSchema.add({ ... address schema ... });�JsonSchema.add({ ... customer schema ... });��const customerSchema = JsonSchema.get("https://example.com/schemas/customer"); ✅�const isValid = JsonSchema.validate(customerSchema, customer).valid;

13 of 65

Schema Identification and Referencing

  • Breakdown and organize schemas the same way you would code�
  • Schema identifiers should match the filesystem path except without extensions
    • https://example.com/schemas/address
    • {localpath}/schemas/address.schema.json
  • Alternative: Store schemas next to the code they represent

📁 schemas� |- 🗎 address.schema.json� |- 🗎 customer.schema.json� |- 📁 nested� | - 🗎 another.schema.json

14 of 65

Schema Bundling

  • Embed schemas in $defs with their $id

{� "$schema": "https://json-schema.org/draft/2020-12/schema",� "$id": "https://example.com/schemas/customer",�� "type": "object",� "properties": {� "firstName": { "$ref": "#/$defs/name" },� "lastName": { "$ref": "#/$defs/name" },� "address": { "$ref": "/schemas/address" }� },�� "$defs": {� "name": { "type": "string", "minimum": 1, "maximum": 128 },� "https://example.com/schemas/address": {� "$schema": "https://json-schema.org/draft/2020-12/schema",� "$id": "https://example.com/schemas/address",�� "type": "object",� "properties": {� "postalCode": { "$ref": "#/$defs/postalCode" }� },�� "$defs": {� "postalCode": { "pattern": "^[0-9]{5}(-[0-9]{4})?$" }� }� } }�}

15 of 65

Schema Bundling

  • Embed schemas in $defs with their $id
  • Don't modify $refs

{� "$schema": "https://json-schema.org/draft/2020-12/schema",� "$id": "https://example.com/schemas/customer",�� "type": "object",� "properties": {� "firstName": { "$ref": "#/$defs/name" },� "lastName": { "$ref": "#/$defs/name" },� "address": { "$ref": "/schemas/address" }� },�� "$defs": {� "name": { "type": "string", "minimum": 1, "maximum": 128 },� "https://example.com/schemas/address": {� "$schema": "https://json-schema.org/draft/2020-12/schema",� "$id": "https://example.com/schemas/address",�� "type": "object",� "properties": {� "postalCode": { "$ref": "#/$defs/postalCode" }� },�� "$defs": {� "postalCode": { "pattern": "^[0-9]{5}(-[0-9]{4})?$" }� }� }� }�}

16 of 65

Schema Bundling

  • Embed schemas in $defs with their $id
  • Don't modify $refs�
  • Embedded schemas don't need to be the use the same JSON Schema dialect as the parent schema

{� "$schema": "https://json-schema.org/draft/2020-12/schema",� "$id": "https://example.com/schemas/customer",�� "type": "object",� "properties": {� "firstName": { "$ref": "#/$defs/name" },� "lastName": { "$ref": "#/$defs/name" },� "address": { "$ref": "/schemas/address" }� },�� "$defs": {� "name": { "type": "string", "minimum": 1, "maximum": 128 },� "https://example.com/schemas/address": {� "$schema": "https://json-schema.org/draft/2020-12/schema",� "$id": "https://example.com/schemas/address",�� "type": "object",� "properties": {� "postalCode": { "$ref": "#/$defs/postalCode" }� },�� "$defs": {� "postalCode": { "pattern": "^[0-9]{5}(-[0-9]{4})?$" }� }� }� }�}

17 of 65

Schema Bundling

  • Embed schemas in $defs with their $id
  • Don't modify $refs�
  • Embedded schemas don't need to be the use the same JSON Schema dialect as the parent schema
  • OpenAPI
    • Same except embed schemas in /components/schemas instead of $defs
    • 🚩 discriminator needs to be to unique

{� "components": {� "schemas": {� "Customer": {� "$schema": "https://json-schema.org/draft/2020-12/schema",� "$id": "https://example.com/schemas/customer",�� "type": "object",� "properties": {� "firstName": { "$ref": "#/$defs/name" },� "lastName": { "$ref": "#/$defs/name" },� "address": { "$ref": "/schemas/address" }� },�� "$defs": {� "name": { "type": "string", "minimum": 1, "maximum": 128 }� },� "Address": {� "$schema": "https://json-schema.org/draft/2020-12/schema",� "$id": "https://example.com/schemas/address",�� "type": "object",� "properties": {� "postalCode": { "$ref": "#/$defs/postalCode" }� },�� "$defs": {� "postalCode": { "pattern": "^[0-9]{5}(-[0-9]{4})?$" }� }� }� }� }�}

18 of 65

Code Style

19 of 65

Readable Schemas

  • What does this schema do?

{� "$schema": "https://json-schema.org/draft/2020-12/schema",� "$id": "https://example.com/schemas/address",� "type": "object",� "properties": {� "streetAddress": { "type": "string" },� "state": { "type": "string" },� "country": { "type": "string" }� },� "required": ["streetAddress"],� "if": {� "type": "object",� "properties": {� "country": { "const": "United States of America" }� },� "required": ["country"]� },� "then": { "required": ["state"] }�}

20 of 65

Readable Schemas

  • Schema sections
    • meta-data
    • main
    • definitions

{� "$schema": "https://json-schema.org/draft/2020-12/schema",� "$id": "https://example.com/schemas/address",

"type": "object",� "properties": {� "streetAddress": { "type": "string"},� "state": { "type": "string" },� "country": { "type": "string" }� },� "required": ["streetAddress", "country"],� "allOf": [{ "$ref": "#/$defs/usa-requires-state" }],

"$defs":� "usa-requires-state": {� "if": {� "type": "object",� "properties": {� "country": { "const": "United States of America" }� },� "required": ["country"]� },� "then": { "required": ["state"] }� }� }�}

21 of 65

Readable Schemas

  • Schema sections
    • meta-data
    • main
    • definitions

{� "$schema": "https://json-schema.org/draft/2020-12/schema",� "$id": "https://example.com/schemas/address",

"type": "object",� "properties": {� "streetAddress": { "type": "string"},� "state": { "type": "string" },� "country": { "type": "string" }� },� "required": ["streetAddress", "country"],� "allOf": [{ "$ref": "#/$defs/usa-requires-state" }],

"$defs":� "usa-requires-state": {� "if": {� "type": "object",� "properties": {� "country": { "const": "United States of America" }� },� "required": ["country"]� },� "then": { "required": ["state"] }� }� }�}

22 of 65

Readable Schemas

  • Schema sections
    • meta-data
    • main
    • definitions

{� "$schema": "https://json-schema.org/draft/2020-12/schema",� "$id": "https://example.com/schemas/address",

"type": "object",� "properties": {� "streetAddress": { "type": "string"},� "state": { "type": "string" },� "country": { "type": "string" }� },� "required": ["streetAddress", "country"],� "allOf": [{ "$ref": "#/$defs/usa-requires-state" }],

"$defs":� "usa-requires-state": {� "if": {� "type": "object",� "properties": {� "country": { "const": "United States of America" }� },� "required": ["country"]� },� "then": { "required": ["state"] }� }� }�}

23 of 65

Readable Schemas

  • Schema sections
    • meta-data
    • main
    • definitions

{� "$schema": "https://json-schema.org/draft/2020-12/schema",� "$id": "https://example.com/schemas/address",

"type": "object",� "properties": {� "streetAddress": { "type": "string"},� "state": { "type": "string" },� "country": { "type": "string" }� },� "required": ["streetAddress", "country"],� "allOf": [{ "$ref": "#/$defs/usa-requires-state" }],

"$defs":� "usa-requires-state": {� "if": {� "type": "object",� "properties": {� "country": { "const": "United States of America" }� },� "required": ["country"]� },� "then": { "required": ["state"] }� }� }�}

24 of 65

🚩Anti-patterns🚩

25 of 65

🚩oneOf🚩 - Union Types

26 of 65

🚩oneOf🚩 - Union Types

{� "oneOf": [� { "$ref": "#/$defs/car" },� { "$ref": "#/$defs/banana" },� { "$ref": "#/$defs/toaster" },� { "$ref": "#/$defs/wagon" },� { "$ref": "#/$defs/cellphone" },� { "$ref": "#/$defs/tshirt" },� { "$ref": "#/$defs/pillow" },� { "$ref": "#/$defs/sandals" },� { "$ref": "#/$defs/spoon" },� { "$ref": "#/$defs/rocket" },� { "$ref": "#/$defs/rope" },� { "$ref": "#/$defs/golfball" },� { "$ref": "#/$defs/dog" },� { "$ref": "#/$defs/mug" },� { "$ref": "#/$defs/boat" },� { "$ref": "#/$defs/peach" }� ],����

�������� "$defs": {� "car": {� "type": "object",� "properties": {� "make": { "type": "string" },� "model": { "type": "string" },� "year": { "type": "string" }� },� "required": ["make", "model", "year"]� },�� "banana": {� "type": "object",� "properties": {� "species": { "type": "string" },� "color": { "type": "string" },� "nutrition": { "type": "string" }� },� "required": ["species", "color"]� },

27 of 65

🚩oneOf🚩 - Union Types

"oneOf": [� { "$ref": "#/$defs/car" },� { "$ref": "#/$defs/banana" }�],��"$defs": {� "car": {� "type": "object",� "properties": {� "make": { "type": "string" },� "model": { "type": "string" },� "year": { "type": "string" }� },� "required": ["make", "model", "year"]� },�� "banana": {� "type": "object",� "properties": {� "species": { "type": "string" },� "color": { "type": "string" },� "nutrition": { "type": "string" }� },� "required": ["species", "color"]� }�}

{� make: "Subaru",� model: 42,� year: "2020"�}

#/$defs/car/properties/model/type -> Value at `/model` must be a string, but a number was found.

#/$defs/banana/required -> Value at `` is missing required properties "species" and "color".

28 of 65

🚩oneOf🚩 - Union Types

"oneOf": [� { "$ref": "#/$defs/car" },� { "$ref": "#/$defs/banana" }�],��"$defs": {� "car": {� "type": "object",� "properties": {� "make": { "type": "string" },� "model": { "type": "string" },� "year": { "type": "string" }� },� "required": ["make", "model", "year"]� },�� "banana": {� "type": "object",� "properties": {� "species": { "type": "string" },� "color": { "type": "string" },� "nutrition": { "type": "string" }� },� "required": ["species", "color"]� }�}

{� make: "Subaru",� model: 42,� year: "2020"�}

#/$defs/car/properties/model/type -> Value at `/model` must be a string, but a number was found.

#/$defs/banana/required -> Value at `` is missing required properties "species" and "color".

29 of 65

🚩oneOf🚩 - Union Types

"oneOf": [� { "$ref": "#/$defs/car" },� { "$ref": "#/$defs/banana" }�],��"$defs": {� "car": {� "type": "object",� "properties": {� "make": { "type": "string" },� "model": { "type": "string" },� "year": { "type": "string" }� },� "required": ["make", "model", "year"]� },�� "banana": {� "type": "object",� "properties": {� "species": { "type": "string" },� "color": { "type": "string" },� "nutrition": { "type": "string" }� },� "required": ["species", "color"]� }�}

{� make: "Subaru",� model: 42,� year: "2020"�}

#/$defs/car/properties/model/type -> Value at `/model` must be a string, but a number was found.

#/$defs/banana/required -> Value at `` is missing required properties "species" and "color".

30 of 65

🚩oneOf🚩 - Union Types

"components": {� "schemas": {� "Main": {� "discriminator": { "propertyName": "type" },� "oneOf": [� { "$ref": "#/components/schemas/car" },� { "$ref": "#/components/schemas/banana" }� ]� },�� "Car": {� "type": "object",� "properties": {� "type": { "const": "Car" },� "make": { "type": "string" },� "model": { "type": "string" },� "year": { "type": "string" }� },� "required": ["type", "make", "model", "year"]� },�� "Banana": {� "type": "object",� "properties": {� "type": { "const": "Banana" },� "species": { "type": "string" },� "color": { "type": "string" },� "nutrition": { "type": "string" }� },� "required": ["type", "species", "color"]� }� }�}

{� type": "Car", make: "Subaru",� model: 42,� year: "2020"�}

❌‌ #/components/schemas/Car/properties/model/type -> Value at `/model` must be a string, but a number was found.

31 of 65

🚩oneOf🚩 - Union Types

"components": {� "schemas": {� "Main": {� "discriminator": { "propertyName": "type" },� "oneOf": [� { "$ref": "#/components/schemas/car" },� { "$ref": "#/components/schemas/banana" }� ]� },�� "Car": {� "type": "object",� "properties": {� "type": { "const": "Car" },� "make": { "type": "string" },� "model": { "type": "string" },� "year": { "type": "string" }� },� "required": ["type", "make", "model", "year"]� },�� "Banana": {� "type": "object",� "properties": {� "type": { "const": "Banana" },� "species": { "type": "string" },� "color": { "type": "string" },� "nutrition": { "type": "string" }� },� "required": ["type", "species", "color"]� }� }�}

{� type": "Car",� make: "Subaru",� model: 42,� year: "2020"�}

❌‌ #/components/schemas/Car/properties/model/type -> Value at `/model` must be a string, but a number was found.

32 of 65

✅ oneOf ✅ - Union Types

"allOf": [� {� "if": {� "type": "object",� "properties": { "type": { "const": "car" } },� "required": ["type"]� },� "then": { "$ref": "#/$defs/car" }� },� {� "if": {� "type": "object",� "properties": { "type": { "const": "banana" } },� "required": ["type"]� },� "then": { "$ref": "#/$defs/banana" }� }�],��"$defs": {� "car": {� "type": "object",� "properties": {� "type": { "const": "car" },� "make": { "type": "string" },� "model": { "type": "string" },� "year": { "type": "string" }� },� "required": ["type", "make", "model", "year"]� },

{� "type": "car",� "make": "Subaru",� "model": 42,� "year": "2020"�}

#/$defs/car/properties/model/type -> Value at `/model` must be a string, but a number was found.

�� "banana": {� "type": "object",� "properties": {� "type": { "const": "banana" },� "species": { "type": "string" },� "color": { "type": "string" },� "nutrition": { "type": "string" }� },� "required": ["type", "species", "color"]� }�}

33 of 65

✅ oneOf ✅ - Union Types

"allOf": [� {� "if": {� "type": "object",� "properties": { "type": { "const": "car" } },� "required": ["type"]� },� "then": { "$ref": "#/$defs/car" }� },� {� "if": {� "type": "object",� "properties": { "type": { "const": "car" } },� "required": ["type"]� },� "then": { "$ref": "#/$defs/banana" }� }�],��"$defs": {� "car": {� "type": "object",� "properties": {� "type": { "const": "car" },� "make": { "type": "string" },� "model": { "type": "string" },� "year": { "type": "string" }� },� "required": ["type", "make", "model", "year"]� },

�� "banana": {� "type": "object",� "properties": {� "type": { "const": "banana" },� "species": { "type": "string" },� "color": { "type": "string" },� "nutrition": { "type": "string" }� },� "required": ["type", "species", "color"]� }�}

{� "type": "car",� "make": "Subaru",� "model": 42,� "year": "2020"�}

#/$defs/car/properties/model/type -> Value at `/model` must be a string, but a number was found.

34 of 65

✅ oneOf ✅ - Mutually Exclusive Required Properties

35 of 65

✅ oneOf ✅ - Mutually Exclusive Required Properties

  • Only use oneOf for simple mutually exclusive constraints.
  • oneOf is not an analog of intersection types

{� "oneOf": [� { "required": ["rock"] },� { "required": ["paper"] },� { "required": ["scissors"] }� ]�}

36 of 65

🚩additionalProperties🚩 - Polymorphism

37 of 65

🚩additionalProperties🚩 - Polymorphism

const cat: Cat = {� genus: "Felis",� species: "catus",� numberOfLives: 9�};��const dog: Dog = {� genus: "Canis",� species: "familiaris",� breed: "Labrador Retriever"�};��getScientificName(cat); // "Felis catus"�getScientificName(dog); // "Canis familiaris"

interface Animal {� genus: string;� species: string;�}��interface Cat extends Animal {� numberOfLives: number;�}��interface Dog extends Animal {� breed: string;�}��function getScientificName(a: Animal) {� return `${a.genus} ${a.species}`;�}

38 of 65

🚩additionalProperties🚩 - Polymorphism

  • Schemas can be "extended" using composition with the $ref keyword.
  • A service that takes an animal can take a cat or a dog

{� "$id": "/schemas/animal",�� "type": "object",� "properties": {� "genus": { "type": "string" },� "species": { "type": "string" }� },� "required": ["genus", "species"]�}

{� "$id": "/schemas/cat",�� "$ref": "/schemas/animal",� "properties": {� "numberOfLives": { "type": "integer" }� },� "required": ["numberOfLives"]�}�

39 of 65

🚩additionalProperties🚩 - Polymorphism

  • "additionalProperties": false is like adding final to a Java class. It can't be extended.

{� "$id": "/schemas/animal",�� "type": "object",� "properties": {� "genus": { "type": "string" },� "species": { "type": "string" }� },� "required": ["genus", "species"],� "additionalProperties": false�}

{� "$id": "/schemas/cat",�� "$ref": "/schemas/animal",� "properties": {� "numberOfLives": { "type": "integer" }� },� "required": ["numberOfLives"]�}�

40 of 65

🚩additionalProperties🚩 - Polymorphism

  • "additionalProperties": false is like adding final to a Java class. It can't be extended.

{� "$id": "/schemas/animal",�� "type": "object",� "properties": {� "genus": { "type": "string" },� "species": { "type": "string" }� },� "required": ["genus", "species"]�}

{� "$id": "/schemas/cat",�� "$ref": "/schemas/animal",� "properties": {� "numberOfLives": { "type": "integer" }� },� "required": ["numberOfLives"],� "additionalProperties": false�}�

41 of 65

✅ additionalProperties ✅ - Polymorphism

  • unevaluatedProperties is like additionalProperties except it takes properties declared in sub-schemas into account

{� "$id": "/schemas/animal",�� "type": "object",� "properties": {� "genus": { "type": "string" },� "species": { "type": "string" }� },� "required": ["genus", "species"]�}

{� "$id": "/schemas/cat",�� "$ref": "/schemas/animal",� "properties": {� "numberOfLives": { "type": "integer" }� },� "required": ["numberOfLives"],� "unevaluatedProperties": false�}�

42 of 65

✅ additionalProperties ✅ - Polymorphism

  • Provide both an open and a closed version of each schema with unevaluatedProperties.
  • Reference the open version
    • { "$ref": "/schemas/animal" }
  • Reference the closed version
    • { "$ref": "/schemas/animal#strict" }

{� "$id": "/schemas/animal",�� "type": "object",� "properties": {� "genus": { "type": "string" },� "species": { "type": "string" }� },� "required": ["genus", "species"],�� "$defs": {� "strict": {� "$anchor": "strict",� "$ref": "#",� "unevaluatedProperties": false� }� }�}

43 of 65

✅ additionalProperties ✅ - Polymorphism

  • Provide both an open and a closed version of each schema with unevaluatedProperties.
  • Reference the open version
    • { "$ref": "/schemas/animal" }
  • Reference the closed version
    • { "$ref": "/schemas/animal#strict" }

{� "$id": "/schemas/animal",�� "type": "object",� "properties": {� "genus": { "type": "string" },� "species": { "type": "string" }� },� "required": ["genus", "species"],�� "$defs": {� "strict": {� "$anchor": "strict",� "$ref": "#",� "unevaluatedProperties": false }� }�}

44 of 65

🚩additionalProperties🚩 - Object Maps

45 of 65

🚩additionalProperties🚩 - Object Maps

  • An object with arbitrary keys and uniform values.
  • /scores is an object map

{� "type": "object",� "properties": {� "event": { "type": "string" },� "level": { "type": "string" },� "scores": {� "type": "object",� "additionalProperties": {� "type": "number"� } }� }�}

{� "event": "Uneven Bars",� "level": "5",� "scores": {� "Milana": 9.5,� "Alexis": 9.3,� "Angelina": 9.7,� "Loraine": 8.9� }�}

46 of 65

🚩additionalProperties🚩 - Object Maps

{� "type": "object",� "properties": {� "event": { "type": "string" },� "level": { "type": "string" },� "scores": {� "type": "object",� "patternProperties": {� "\\*$": { "minimum": 9.0 }� },� "additionalProperties": {� "type": "number"� }� }� }�}

  • An object with arbitrary keys and uniform values.
  • /scores is an object map

{� "event": "Uneven Bars",� "level": "5",� "scores": {� "Milana*": 9.5,� "Alexis*": 9.3,� "Angelina*": 9.7,� "Loraine": 8.9� }�}

47 of 65

🚩additionalProperties🚩 - Object Maps

{� "type": "object",� "properties": {� "event": { "type": "string" },� "level": { "type": "string" },� "scores": {� "type": "object",� "patternProperties": {� "\\*$": { "minimum": 9.0 }� },� "additionalProperties": {� "type": "number"� }� }� }�}

  • An object with arbitrary keys and uniform values.
  • /scores is an object map

{� "event": "Uneven Bars",� "level": "5",� "scores": {� "Milana*": "9.5",� "Alexis*": 9.3,� "Angelina*": 9.7,� "Loraine": 8.9� }�}

48 of 65

✅ additionalProperties ✅ - Object Maps

{� "type": "object",� "properties": {� "event": { "type": "string" },� "level": { "type": "string" },� "scores": {� "type": "object",� "patternProperties": {� "": { "type": "number" },� "\\*$": { "minimum": 9.0 }� }� }� }�}

  • Use patternProperties instead of additionalProperties

{� "event": "Uneven Bars",� "level": "5",� "scores": {� "Milana*": 9.5,� "Alexis*": 9.3,� "Angelina*": 9.7,� "Loraine": 8.9� }�}

49 of 65

Template Schema Pattern

50 of 65

Template Schema Pattern - Dynamic References

{� "$id": "/schemas/extended",�� "$ref": "/schemas/base",� "properties": {� "bbb": { "$ref": "#foo" }� },�� "$defs": {� "foo": {� "$anchor": "foo",� "type": "number"� }� }�}

{� "$id": "/schemas/base",�� "type": "object",� "properties": {� "aaa": { "$ref": "#foo" }� },�� "$defs": {� "foo": {� "$anchor": "foo",� "type": "string"� }� }�}

51 of 65

Template Schema Pattern - Dynamic References

{� "$id": "/schemas/extended",�� "$ref": "/schemas/base",� "properties": {� "bbb": { "$dynamicRef": "#foo" }� },�� "$defs": {� "foo": {� "$dynamicAnchor": "foo",� "type": "number"� }� }�}

{� "$id": "/schemas/base",�� "type": "object",� "properties": {� "aaa": { "$dynamicRef": "#foo" }� },�� "$defs": {� "foo": {� "$dynamicAnchor": "foo",� "type": "string"� }� }�}

52 of 65

Template Schema Pattern - Dynamic References

{� "$id": "/schemas/extended",�� "$ref": "/schemas/base",� "properties": {� "bbb": { "$dynamicRef": "#foo" }� },�� "$defs": {� "foo": {� "$dynamicAnchor": "foo",� "type": "number"� }� }�}

{� "$id": "/schemas/base",�� "type": "object",� "properties": {� "aaa": { "$dynamicRef": "#foo" }� },�� "$defs": {� "foo": {� "$dynamicAnchor": "foo",� "type": "string"� }� }�}

53 of 65

Template Schema Pattern - Dynamic References

{� "$id": "/schemas/extended",�� "$ref": "/schemas/base",� "properties": {� "bbb": { "$dynamicRef": "#foo" }� },�� "$defs": {� "foo": {� "$dynamicAnchor": "foo",� "type": "number"� }� }�}

{� "$id": "/schemas/base",�� "type": "object",� "properties": {� "aaa": { "$dynamicRef": "#foo" }� },�� "$defs": {� "foo": {� "$dynamicAnchor": "foo",� "type": "string"� }� }�}

54 of 65

Template Schema Pattern - Dynamic References

{� "$id": "/schemas/extended",�� "$ref": "/schemas/base",� "properties": {� "bbb": { "$dynamicRef": "#foo" }� },�� "$defs": {� "foo": {� "$dynamicAnchor": "foo",� "type": "number" }� }�}

{� "$id": "/schemas/base",�� "type": "object",� "properties": {� "aaa": { "$dynamicRef": "#foo" }� },�� "$defs": {� "foo": {� "$dynamicAnchor": "foo",� "type": "string"� }� }�}

55 of 65

Template Schema Pattern - Dynamic References

{� "$id": "/schemas/extended",�� "$ref": "/schemas/base",� "properties": {� "bbb": { "$dynamicRef": "#foo" }� },�� "$defs": {� "foo": {� "$dynamicAnchor": "foo",� "type": "number"� }� }�}

{� "$id": "/schemas/base",�� "type": "object",� "properties": {� "aaa": { "$dynamicRef": "#foo" }� },�� "$defs": {� "foo": {� "$dynamicAnchor": "foo",� "type": "string"� }� }�}

56 of 65

Template Schema Pattern - List

{� "$id": "/schemas/car",�� "type": "object",� "properties": {� "make": { "type": "string" },� "model": { "type": "string" },� "year": { "type": "string" }� },� "required": ["make", "model", "year"]�}

{� "$id": "/schema/car-list",�� "type": "object",� "properties": {� "listItems": {� "type": "array",� "items": { "$ref": "/schema/car" }� },� "nextPage": { "type": "integer" },� "prevPage": { "type": "integer" },� "perPage": { "type": "integer" },� "page": { "type": "integer" }� }�}

57 of 65

Template Schema Pattern - List

{� "$id": "/schema/car-list",�� "$ref": "/schema/list",� "properties": {� "listItems": {� "items": { "$ref": "/schema/car" }� }� }�}

{� "$id": "/schema/list",�� "type": "object",� "properties": {� "listItems": { "type": "array" },� "nextPage": { "type": "integer" },� "prevPage": { "type": "integer" },� "perPage": { "type": "integer" },� "page": { "type": "integer" }� }�}

58 of 65

Template Schema Pattern - List

{� "$id": "/schema/car-list",�� "$ref": "/schema/list",� "properties": {� "listItems": {� "items": { "$ref": "/schema/car" }� }� }�}

{� "$id": "/schema/list",�� "type": "object",� "properties": {� "listItems": { "type": "array" },� "nextPage": { "type": "integer" },� "prevPage": { "type": "integer" },� "perPage": { "type": "integer" },� "page": { "type": "integer" }� }�}

59 of 65

Template Schemas Pattern - List

{� "$id": "/schema/car-list",�� "$ref": "/schema/list",�� "$defs": {� "list-items": {� "$dynamicAnchor": "list",� "$ref": "/schema/car"� }� }�}

{� "$id": "/schema/list",�� "type": "object",� "properties": {� "listItems": {� "type": "array",� "items": { "$dynamicRef": "#list" }� },� "nextPage": { "type": "integer" },� "prevPage": { "type": "integer" },� "perPage": { "type": "integer" },� "page": { "type": "integer" }� },�� "$defs": {� "default-list-item": {� "$dynamicAnchor": "list"� }� }�}

60 of 65

Template Schemas Pattern - List

{� "$id": "/schema/car-list",�� "$ref": "/schema/list",�� "$defs": {� "list-items": {� "$dynamicAnchor": "list",� "$ref": "/schema/car"� }� }�}

{� "$id": "/schema/list",�� "type": "object",� "properties": {� "listItems": {� "type": "array",� "items": { "$dynamicRef": "#list" }� },� "nextPage": { "type": "integer" },� "prevPage": { "type": "integer" },� "perPage": { "type": "integer" },� "page": { "type": "integer" }� },�� "$defs": {� "default-list-item": {� "$dynamicAnchor": "list"� }� }�}

61 of 65

Template Schemas Pattern - List

{� "$id": "/schema/car-list",�� "$ref": "/schema/list",�� "$defs": {� "list-items": {� "$dynamicAnchor": "list",� "$ref": "/schema/car"� }� }�}

{� "$id": "/schema/list",�� "type": "object",� "properties": {� "listItems": {� "type": "array",� "items": { "$dynamicRef": "#list" }� },� "nextPage": { "type": "integer" },� "prevPage": { "type": "integer" },� "perPage": { "type": "integer" },� "page": { "type": "integer" }� },�� "$defs": {� "default-list-item": {� "$dynamicAnchor": "list"� }� }�}

62 of 65

Template Schemas Pattern - List

{� "$id": "/schema/car-list",�� "$ref": "/schema/list",�� "$defs": {� "list-items": {� "$dynamicAnchor": "list",� "$ref": "/schema/car"� }� }�}

{� "$id": "/schema/list",�� "type": "object",� "properties": {� "listItems": {� "type": "array",� "items": { "$dynamicRef": "#list" }� },� "nextPage": { "type": "integer" },� "prevPage": { "type": "integer" },� "perPage": { "type": "integer" },� "page": { "type": "integer" }� },�� "$defs": {� "default-list-item": {� "$dynamicAnchor": "list"� }� }�}

63 of 65

Cheatsheet

  • A schema should describe one thing
  • Only use JSON Pointers with $defs
  • Only reference external sub-schemas that have an $anchor
  • Use $defs to avoid local schema duplication
  • Use $defs to give names to complex assertions
  • Use whitespace to keep schemas readable
  • Use if/then for union type instead of oneOf
  • Provide both an open and a closed version of your schemas using unevaluatedProperties
  • Use patternProperties for object maps instead of additionalProperties
  • Use the Template Schema Pattern to extend schemas without coupling

64 of 65

References

65 of 65

Jason Desrosiers

jason@hyperjump.io

https://github.com/jdesrosiers

@jasondesrosiers