@criteria/json-schema
TypeScript icon, indicating that this package has built-in type declarations

0.9.1 • Public • Published

JSON Schema

TypeScript type definitions and functions for the JSON Schema specification.

Table of Contents

Overview

This package provides TypeScript type definitions and functions for working with JSON Schema documents. It is intended to be used by tooling authors, to build JSON Schema tools in TypeScript across various drafts of the specification easily.

This package also contains functions to dereference JSON Schema documents into plain JavaScript objects according to the reference resolution rules of the specification.

The following drafts are currently supported:

Getting Started

Install JSON Schema using npm:

npm install @criteria/json-schema

Let's define a simple JSON schema in code to get started:

import { JSONSchema } from '@criteria/json-schema/draft-2020-12'

const schema: JSONSchema = {
  type: 'object',
  title: 'person',
  properties: {
    name: {
      $ref: '#/$defs/requiredString'
    },
    email: {
      $ref: '#/$defs/requiredString'
    }
  },
  $defs: {
    requiredString: {
      title: 'requiredString',
      type: 'string',
      minLength: 1
    }
  }
}

Right now if we tried to inspect the type of the name or email properties we would get undefined:

console.log(schema.properties.name.type)
// undefined

We can use the dereferenceJSONSchema() function to transform our schema object with $ref values into plain JavaScript objects without any references:

import { JSONSchema, DereferencedJSONSchema, dereferenceJSONSchema } from '@criteria/json-schema/draft-2020-12'

const schema: JSONSchema = {
  type: 'object',
  title: 'person',
  properties: {
    name: {
      $ref: '#/definitions/requiredString'
    },
    email: {
      $ref: '#/definitions/requiredString'
    }
  },
  $defs: {
    requiredString: {
      title: 'requiredString',
      type: 'string',
      minLength: 1
    }
  }
}

const dereferencedSchema: DereferencedJSONSchema = dereferenceJSONSchema(schema)

console.log((dereferencedSchema.properties.name as DereferencedJSONSchemaObject).type)
// string

console.log((dereferencedSchema.properties.name as DereferencedJSONSchemaObject).minLength)
// 1

The return type of dereferenceJSONSchema(schema) is DereferencedJSONSchema instead of JSONSchema. This is a TypeScript type alias that replaces $ref keywords with their dereferenced values.

NOTE: In JSON Schema Draft 2020-12 true and false are valid JSON schemas, equivalent to {} and {"not": {}} respectively. This example uses type assertions to specify that the type of dereferencedSchema.properties.name is DereferencedJSONSchemaObject because we know the deferenced schemas are not boolean values. If we were loading this schema from an external source, you should check whether the schema or any subschema is a boolean value before trying to access any keywords.

Installation

npm install @criteria/json-schema

Usage

Importing type definitions

Type names are suffixed with the draft that they correspond to:

import { JSONSchemaDraft04, JSONSchemaDraft2020_12 } from '@criteria/json-schema'

const fooSchema: JSONSchemaDraft04 = {
  $schema: 'http://json-schema.org/draft-04/schema#'
}

const barSchema: JSONSchemaDraft2020_12 = {
  $schema: 'https://json-schema.org/draft/2020-12/schema'
}

If you only want to import types for a specific draft, you can import more concise types from the draft-specific module:

import { JSONSchema } from '@criteria/json-schema/draft-04'

const schema: JSONSchema = {
  $schema: 'http://json-schema.org/draft-04/schema#'
}
import { JSONSchema } from '@criteria/json-schema/draft-2020-12'

const schema: JSONSchema = {
  $schema: 'https://json-schema.org/draft/2020-12/schema'
}

The exclusiveMinimum and exclusiveMaximum keywords are an example of type differences between Draft 04 and Draft 2020-12. In Draft 04 they were boolean values, whereas in Draft 2020-12 they are now numbers to be consistent with the principle of keyword independence. Mixing these up is caught by the TypeScript compiler:

import { JSONSchemaDraft2020_12 } from '@criteria/json-schema'

const schema: JSONSchemaDraft2020_12 = {
  type: 'number',
  maximum: 100,
  exclusiveMaximum: true
}
// Type 'boolean' is not assignable to type 'number'.

Dereferencing schemas

Dereferencing refers to the process of transforming a JSON schema by replacing occurences of $ref with the actual subschema being referenced.

The dereferenceJSONSchema(schema) functions provided by this package observe the following behaviour:

  • The schema is treated as immutable. The return value is a copy of the input data.
  • Object identity is maintained. The dereferenced schema is the same JavaScript instance as the value that was referenced.
  • Circular references are preserved. Recursive or circular references in the input schema will be replicated in the dereferenced output.

The following example demonstrates this behaviour:

import { dereferenceJSONSchema } from '@criteria/json-schema/draft-2020-12'

const inputSchema: JSONSchema = {
  type: 'object',
  title: 'person',
  properties: {
    name: {
      $ref: '#/definitions/requiredString'
    },
    children: {
      type: 'array',
      items: {
        $ref: '#'
      }
    }
  },
  $defs: {
    requiredString: {
      title: 'requiredString',
      type: 'string',
      minLength: 1
    }
  }
}

const dereferencedSchema = dereferenceJSONSchema(inputSchema)

console.log(dereferencedSchema === inputSchema)
// false, input data is not mutated

console.log(dereferencedSchema.properties.name === dereferencedSchema.$defs.requiredString)
// true, object identity is maintained

console.log(dereferencedSchema.properties.children.items === dereferencedSchema)
// true, circular references are supported

A $ref property may contain an absolute URI or a URI reference that is resolved against the containing schema's base URI. The way $ref values are resolved is unique to each draft.

In all drafts, objects that only contain a $ref property are wholly replaced with the referenced value.

Draft 2020-12 notes

In Draft 2020-12, $ref keywords may appear alongside other keywords. This changes the semantics of $ref from transclusion to delegation since the referencing and referenced schemas can contain the same keywords. In these situations dereferenceJSONSchema(schema) will not replace the $ref keyword, but it will replace the URI reference value with the actual dereferenced object:

For example, given the following schema:

{
  $defs: {
    alphanumericWithInitialLetter: {
      $ref: '#/$defs/alphanumeric',
      pattern: '^[a-zA-Z]'
    },
    alphanumeric: {
      type: 'string',
      pattern: '^[a-zA-Z0-9]*$'
    }
  }
}

Deferencing the schema would produce:

{
  $defs: {
    alphanumericWithInitialLetter: {
      $ref: {
        type: 'string',
        pattern: '^[a-zA-Z0-9]*$'
      },
      pattern: '^[a-zA-Z]'
    },
    alphanumeric: {
      type: 'string',
      pattern: '^[a-zA-Z0-9]*$'
    }
  }
}

Draft 04 notes

In Draft 04, having additional keywords alongside an occurance of $ref is not supported. Nonetheless, this package will attempt to dereference such situations according to best efforts and to maintain parity with other tools such as @apidevtools/json-schema-ref-parser.

For example, given the following schema:

{
  $defs: {
    alphanumericWithInitialLetter: {
      $ref: '#/$defs/alphanumeric',
      pattern: '^[a-zA-Z]'
    },
    alphanumeric: {
      type: 'string',
      pattern: '^[a-zA-Z0-9]*$'
    }
  }
}

Deferencing the schema would produce:

{
  $defs: {
    alphanumericWithInitialLetter: {
      type: 'string',
      pattern: '^[a-zA-Z]'
    },
    alphanumeric: {
      type: 'string',
      pattern: '^[a-zA-Z0-9]*$'
    }
  }
}

Notice how the pattern keyword from the referencing schema shadows the equivalent in the referenced schema.

Retrieving external documents

It's common for schema authors to organize complex schemas as multiple shared files. You can provide a custom retrieve function to dereferenceJSONSchema() in order to perform an filesystem or network operations.

The following example dereferences a JSON schema that references other files within a shared-schemas directory.

import { readFileSync } from 'fs'

const baseURI = resolve(__dirname, 'shared-schemas/root.json')

const retrieve = (uri: string) => {
  const data = readFileSync(uri, { encoding: 'utf8' })
  return JSON.parse(data)
}

const dereferencedSchema = dereferenceJSONSchema(rootSchema, { baseURI, retrieve })

The argument passed to your retrieve function is a URI that has been resolved according to the specification rules. Note that this URI is an identifier and not necessarily a network locator. In the case of a network-addressable URL, a schema need not be downloadable from its canonical URI.

Definining additional vocabularies

Note: This is only supported in Draft 2020-12.

Schema authors can require additional vocabularies not defined by the Core/Validation Dialect meta-schema in order to process the schema.

Tooling can extend the base JSONSchema type to provide type safety for additional vocabularies:

import { JSONSchema } from '@criteria/json-schema'

interface MyValidationKeywords {
  foo: boolean
}

type MyJSONSchema = JSONSchema<MyValidationKeywords>

const schema: MyJSONSchema = {}
// Property 'foo' is missing...

Passing the type to JSONSchema's generic argument ensures that the additional vocabulary is also applied to all subschemas.

Acknowledgments

Many test cases have been ported from @apidevtools/json-schema-ref-parser.

License

This project is MIT licensed.

About Criteria

Criteria is a collaborative platform for designing APIs.

Package Sidebar

Install

npm i @criteria/json-schema

Weekly Downloads

16

Version

0.9.1

License

MIT

Unpacked Size

214 kB

Total Files

129

Last publish

Collaborators

  • jamesmoschou-criteria