Skip to content

Customizable and hackable json-validator and json-schema utilities for traversal, data generation and validation

License

Notifications You must be signed in to change notification settings

sagold/json-schema-library

Repository files navigation

Npm package version CI Types

json-schema-library
json-schema-library

json-schema-library provides tools and utilities for working with JSON Schema - enabling creation, validation, and schema exploration. Unlike most validators and editors, which hide the inner workings, this library is designed for developers building custom tools around JSON Schema. It runs in both Node and browser environments, prioritizing flexibility and extensibility over minimal memory footprint or raw performance.



⚠️ This documentation refers to the upcoming release version 10, which can be installed by npm install json-schema-library@10.0.0-rc10. For the latest release please refer to the documentation of version 9.3.5. If you are on v10-rc refer to documentation of version 10.0.0-rc8 or lower

Quick start

npm install json-schema-library

json-schema-library includes a compileSchema function that converts a JSON Schema into a SchemaNode, which gives you easy-to-use methods for working with the schema.

import { compileSchema, SchemaNode } from "json-schema-library";
import myJsonSchema from "./myJsonSchema.json";
import myData from "./myData.json";

const schema: SchemaNode = compileSchema(myJsonSchema);
// validate data and collect errors if invalid
const { valid, errors } = schema.validate(myData);
// create data which validates to the compiled JSON Schema
const defaultData = schema.getData();
// access a subschema at a specific JSON Pointer location
const { node, error } = schema.getNode("#/image/title");
node && console.log(node.schema);

Per default, compileSchema uses the draft-version referenced in $schema for cross-draft support. In case $schema is omitted or invalid the latest schema (draft-2020-12) will be used. Customizing draft selection is documented in draft customization.

const schemaNode = compileSchema({ $schema: "draft-07" });
console.log(schemaNode.getDraftVersion()); // draft-07

Overview

compileSchema

Use compileSchema once to turn a JSON Schema into a tree of SchemaNodes. After that, you'll work with individual nodes in the tree. You can also pass an options object to compileSchema to customize how the nodes are created.

type CompileOptions = {
    // set of drafts to use
    drafts: Draft[];
    // a context to share
    remote: SchemaNode;
    // if format-validations should create errors. Defaults to true
    formatAssertion: boolean | "meta-schema";
    // default options for all calls to node.getData()
    getDataDefaultOptions?: TemplateOptions;
};

With this

import { compileSchema, draft04, draft06, draft07, draft2019, draft2020 } from "json-schema-library";

// only draft07 is used for all JSON schema
compileSchema(mySchema, { drafts: [draft07] });

// the created node will share a context with `anotherSchemaNode` enabling cross schema ref-resolution
// Note that anotherSchemaNode still uses its own drafts it was compiled with
compileSchema(mySchema, { remote: anotherSchemaNode });

// format validation is disabled
compileSchema(mySchema, { formatAssertion: false });

// for all calls to getData, `addOptionalProps` is `true` per default
compileSchema(mySchema, { getDataDefaultOptions: { addOptionalProps: true } });

Details on drafts are documented in draft customization. Details on getDataDefaultOptions are documented in getData.

SchemaNode

compileSchema builds a tree where each sub-schema becomes its own SchemaNode. Every node in the tree offers the same set of methods. For example:

const root = compileSchema(mySchema);
const rootData = root.getData();
const { node: titleNode } = root.getNode("#/image/title");
const titleData = titleNode?.getData();
Each node has an identity
const titleNode = compileSchema(mySchema).getNode("#/image/title");
console.log(titleNode.spointer); // #/properties/image/properties/title
console.log(titleNode.schemaId); // #/properties/image/properties/title
  • spointer refers to the path in schema and is extended by $ref, e.g. if image is defined on $defs: #/properties/image/$ref/properties/title
  • schemaId refers to the absolute path within the schema and will not change, e.g. #/$defs/properties/title
Each node has a reference to its parent node

The parent-node can be a sub-schema or intermediary node:

const root = compileSchema(mySchema);
const { node: childNode } = root.getNode("#/image");
assert(root === childNode.parent);
All nodes share a context

It is not advised to work on context directly, but it might be useful in some situations

A context is shared across all nodes of a schema

const root = compileSchema(mySchema);
const { node: childNode } = root.getNode("#/image");
assert(root.context === childNode.context);

And some context properties are shared across all schema added as remotes. The property rootNode refers to the root-schema for the current node

const root = compileSchema(mySchema);
const { node: childNode } = root.getNode("#/image");
assert(root === childNode.context.rootNode);

Note that rootNodes will change when working across remote schema (using $ref).

Draft Support

json-schema-library fully supports all core features of draft versions draft-04, draft-06, draft-07, draft-2019-09 and draft-2020-12. Additionally, most format-validations are supported per default besides the listed format below. You can always override or extend format validation as is documented in draft customization.

Overview draft support

Draft support is defined by running a validator against the official json-schema-test-suite.

Please note that these benchmarks refer to validation only. json-schema-library offers tooling outside of validation and strives to be as spec-compliant as possible.

Overview format validation support
  • ❌ unsupported formats iri, iri-reference, idn-hostname
  • ✅ supported formats: date, date-time, date, duration, ecmascript-regex, email, hostname, idn-email, ipv4, ipv6, json-pointer, regex, relative-json-pointer, time, unknown, uri-reference, uri-template, uri, uuid

SchemaNode methods

addRemoteSchema · compileSchema · createSchema · getChildSelection · getData · getNode · getNodeChild · getNodeRef · getNodeRoot · reduceNode · toDataNodes · toSchemaNodes · validate

addRemoteSchema

addRemoteSchema lets you add additional schemas that can be referenced by an URL using $ref. Use this to combine multiple schemas without changing the actual schema.

Each schemas is referenced by their unique $id (since draft-06, previously id). Usually an $id is specified as an url, for example https://mydomain.com/schema/schema-name or with a file extension like https://mydomain.com/schema/schema-name.json.

On a compiled schema

const schemaNode = compileSchema({
    $id: "https://sagold.com/local",
    type: "object",
    required: ["character"],
    properties: {
        character: {
            $ref: "https://sagold.com/remote"
        }
    }
});

use the exposed method addRemoteSchema to add a remote schema for $ref-resolution:

schemaNode.addRemoteSchema("https://sagold.com/remote", {
    $id: "https://sagold.com/remote",
    title: "A character",
    type: "string",
    minLength: 1,
    maxLength: 1
});

Note the given url and $id on the root schema should match. If $id is omitted it will be added from the passed url.

To access the remote schema, add a $ref within your local schema and the remote schema will be resolved automatically:

schemaNode.validate({ character: "AB" }); // maxLength error
schemaNode.getData({}); // { character: "A" } - default value resolved
// returns remote schema (from compiled local schema):
const { node, error } = schemaNode.getNodeRef("https://sagold.com/remote");

Note JSON Schema $ref-keyword can become tricky when combined with $ids in sub-schemas. For more details, see json-schema.org: Structuring a complex schema.

Adding remote schema to compileSchema

It is possible to pass remoteSchema on compileSchema by passing a SchemaNode (with all its remote schemas) in remote:

const remote = compileSchema({
    $id: "https://sagold.com/remote",
    $defs: {
        character: {
            title: "A character",
            type: "string",
            minLength: 1,
            maxLength: 1
        }
    }
});

const schemaNode = compileSchema({ $ref: "https://sagold.com/remote#/defs/character" }, { remote });
Access local subschemas in remote schemas

You can add a local uri reference to the remote schema by using the # separator. The following example resolves hte local path /$defs/character in the remote schema https://sagold.com/remote throught the combined url: https://sagold.com/remote#/$defs/character

const schemaNode = compileSchema({
    $id: "https://sagold.com/local",
    $ref: "https://sagold.com/remote#/$defs/character"
});

schemaNode.addRemoteSchema("https://sagold.com/remote", {
    $defs: {
        character: {
            title: "A character",
            type: "string",
            minLength: 1,
            maxLength: 1
        }
    }
});

schemaNode.validate("AB"); // maxLength error
schemaNode.getData("A"); // "A" - default value resolved
// returns remote schema (from compiled local schema):
const { node, error } = schemaNode.getNodeRef("https://sagold.com/remote#/$defs/character");

Note JSON Pointer are not restricted to $defs (definitions), but can reference any subschema. For example:

const schemaNode = compileSchema({
    $id: "https://sagold.com/local",
    $ref: "https://sagold.com/remote#/properties/character"
});

schemaNode.addRemoteSchema("https://sagold.com/remote", {
    type: "object",
    properties: {
        character: {
            title: "A character",
            type: "string",
            minLength: 1,
            maxLength: 1
        }
    }
});

schemaNode.validate("AB"); // maxLength error
schemaNode.getData("A"); // "A" - default value resolved
// returns remote schema (from compiled local schema):
schemaNode.getNodeRef("https://sagold.com/remote#/properties/character");

compileSchema

node.compileSchema creates a new schema node in the same context as node. With this, the created node will be able to resolve local $ref and remote $ref correctly. Note, the created schema will not be part of (linked) from any nodes in the schema-tree.

const someNode = node.compileSchema({ prefixItems: [{ type: "string" }, { $ref: "#/$defs/string" }] });

createSchema

createSchema returns a simple JSON Schema for the input data.

const schemaNode = compileSchema(mySchema);
const schema: JsonSchema = schemaNode.createSchema({ title: "initial value" });
console.log(schema); // { type: "string" }

getChildSelection

getChildSelection returns a list of available sub-schemas for the given property. In many cases, a single schema will be returned. For oneOf-schemas, a list of possible options is returned. This helper always returns a list of schemas.

const schemaNode = compileSchema(mySchema);
const schemas: SchemaNode[] = schemaNode.getChildSelection("content");
Example
import { compileSchema, JsonSchema } from "json-schema-library";

const jsonSchema = compileSchema({
    type: "object",
    properties: {
        content: {
            oneOf: [{ type: "string" }, { type: "number" }]
        }
    }
});

const childNodes: JsonSchema[] = jsonSchema.getChildSelection("content");

expect(childNodes.map((n) => n.schema)).to.deep.equal([{ type: "string" }, { type: "number" }]);

getData

getData creates input data from a JSON Schema that is valid to the schema. Where possible, the JSON Schema default property will be used to initially setup input data. Otherwise, the first values encountered (enum values, initial values, etc.) are used to build up the json-data.

const myData = compileSchema(myJsonSchema).getData();

Additionally, you can pass input data. getData will then complement any missing values from the schema, while keeping the initial values.

const myData = compileSchema(myJsonSchema).getData({ name: "input-data" });

Note If you are using references in your schema, getData will only resolve the first $ref in each path, ensuring no infinite data structures are created. In case the limit of 1 $ref resolution is too low, you can modify the value globally one by adjusting the json-schema-library settings:

const myData = compileSchema(myJsonSchema).getData(inputData, { recursionLimit: 2 });
Example
import { compileSchema, JsonSchema } from 'json-schema-library';

const myJsonSchema: JsonSchema = {
  type: 'object',
  required: ['name', 'option', 'list'],
  properties: {
    name: { type: 'string' },
    option: {
      type: 'string',
      enum: ['first-option', 'second-option']
    },
    list: {
      type: 'array',
      items: {
        type: 'string',
        default: 'new item'
      },
      minItems: 1
    }
  }
};

const schemaNode = new compileSchema(myJsonSchema);
const myData = schemaNode.getData();

expect(myData).to.deep.equal({
  name: ',
  option: 'first-option',
  list: ['new item']
});
Example with input data
import { compileSchema, JsonSchema } from "json-schema-library";

const myJsonSchema: JsonSchema = {
    type: "object",
    required: ["name", "option", "list"],
    properties: {
        name: { type: "string" },
        option: {
            type: "string",
            enum: ["first-option", "second-option"]
        },
        list: {
            type: "array",
            items: {
                type: "string",
                default: "new item"
            },
            minItems: 1
        }
    }
};

const jsonSchema = compileSchema(myJsonSchema);
const myData = jsonSchema.getData({ name: "input-data", list: [] });

expect(myData).to.deep.equal({
    name: "input-data",
    option: "first-option",
    list: ["new item"]
});
Option: extendDefaults (default: false)

Per default, getData does try to create data that is valid to the json-schema. Example: array-schemas with minItems: 1 will add one item to fullfil the validation criteria. You can use the option and pass { extendDefaults: false } to override this behaviour with a default value:

import { compileSchema } from "json-schema-library";

const myJsonSchema = {
    type: "array",
    default: [], // if omitted will add an array item
    items: {
        type: "string",
        enum: ["one", "two"]
    },
    minItems: 1 // usually adds an enty, but default states: []
};

const myData = compileSchema(myJsonSchema).getData(undefined, { extendDefaults: false });

expect(myData).to.deep.equal([]);
Option: addOptionalProps (default: false)

getData will only add required properties per default:

const data = compileSchema({
    required: ["title"],
    properties: {
        title: { type: "string" },
        subTitle: { type: "string", default: "sub-title" }
    }
}).getData(undefined);
console.log(data); // { title: "" }

With addOptionalProps:true, getData will also add all optional properties

const data = compileSchema({
    required: ["title"],
    properties: {
        title: { type: "string" },
        subTitle: { type: "string", default: "sub-title" }
    }
}).getData(undefined, { addOptionalProps: true });
console.log(data); // { title: "", subTitle: "sub-title" }
Option: removeInvalidData (default: false)

With removeInvalidData:true, getData will remove data that is invalid to the given schema;

const data = compileSchema({
    properties: { valid: { type: "string" } },
    additionalProperties: false
}).getData({ valid: "stays", invalid: "removed" }, { removeInvalidData: true });
console.log(data); // { valid: "stays" }

removeInvalidData:true will not remove data that is valid, but unspecified:

const data = compileSchema({
    properties: { valid: { type: "string" } },
    additionalProperties: true
}).getData({ valid: "stays", invalid: "removed" }, { removeInvalidData: true });
console.log(data); // { valid: "stays", invalid: "removed" }

getNode

getNode returns the JSON Schema from data location specified by a JSON Pointer. In many cases the JSON Schema can be retrieved without passing any data, but in situations where the schema is dynamic (for example in oneOf, dependencies, etc.), input-data is required or getNode will return a JsonError as is done when the JSON Pointer path is invalid.

const { node, error } = compileSchema(mySchema).getNode("/list/1/name", myData);
if (node) console.log(node.schema);

Note getNode will return a node=undefined for paths that lead to valid properties, but miss a schema definition. For example:

const { node, error } = compileSchema({ type: "object" }).getNode("/name");
console.log(node, error); // undefined, undefined

In case this is unwanted behaviour, use the withSchemaWarning option to return a json-error with code schema-warning instead:

const schemaNode = compileSchema({ type: "object" });
const { node, error } = schemaNode.getNode("/name", undefined, { withSchemaWarning: true });
console.log(node?.schema, error); // undefined, { type: "error", code: "schema-warning" }

Or set getNode to return a simple JSON Schema for the found data setting createSchema: true:

const schemaNode = compileSchema({ type: "object" });
const { node, error } = schemaNode.getNode("/name", { name: 123 }, { createSchema: true });
console.log(node?.schema, error); // { type: "number" }, undefined
Example
import { compileSchema } from "json-schema-library";

const mySchema = {
    type: "object",
    properties: {
        list: {
            type: "array",
            items: {
                oneOf: [
                    {
                        type: "object",
                        required: ["name"],
                        properties: {
                            name: {
                                type: "string",
                                title: "name of item"
                            }
                        }
                    },
                    {
                        type: "object",
                        required: ["description"],
                        properties: {
                            description: {
                                type: "string",
                                title: "description of item"
                            }
                        }
                    }
                ]
            }
        }
    }
};

const { node } = compileSchema(mySchema).getNode("/list/1", {
    list: [{ description: "..." }, { name: "my-item" }]
});

expect(node.schema).to.deep.equal({
    type: "object",
    required: ["name"],
    properties: {
        name: {
            type: "string",
            title: "name of item"
        }
    }
});
Evaluating errors

All returned json-errors have a data property with the following properties

  • pointer JSON Pointer to the location where the error occured. In case of omitted data, this is the last JSON Schema location that could be resolved
  • schema the JSON Schema of the last resolved location and the source of the error
  • value the data value at this location that could not be resolved
const { error } = schemaNode.getNode("/list/1");
if (error) {
    console.log(Object.keys(error.data)); // [pointer, schema, value]
}
About JsonPointer

JSON Pointer defines a string syntax for identifying a specific value within a Json document and is supported by Json-Schema. Given a Json document, it behaves similar to a lodash path (a[0].b.c), which follows JS-syntax, but instead uses / separators (e.g., a/0/b/c). In the end, you describe a path into the Json data to a specific point.

getNodeChild

getNodeChild retrieves the SchemaNode of a child property or index. Using get it is possible to incrementally go through the data, retrieving the schema for each next item.

const mySchema = { type: "object", properties: { title: { type: "string" } } };
const root = compileSchema(mySchema);
const { node } = root.getNodeChild("title", { title: "value" });
if (node == null) return;
console.log(node.schema);
Example
import { compileSchema, JsonSchema } from "json-schema-library";

const root = compileSchema(mySchema);
const localSchema: JsonSchema = {
    oneOf: [
        {
            type: "object",
            properties: { title: { type: "string" } }
        },
        {
            type: "object",
            properties: { title: { type: "number" } }
        }
    ]
};

const schema = root.getNodeChild("title", { title: 4 }).node?.schema;

expect(schema).to.deep.eq({ type: "number" });

getNodeRef

getNodeRef retrieves the SchemaNode of a $ref string.

const root = compileSchema(mySchema);
root.addRemoteSchema("https://remote.com/schema", remoteSchema);

root.getNodeRef("#/$defs/title"); // title-schema of mySchema
root.getNodeRef("https://remote.com/schema"); // remoteSchema

getNodeRoot

getNodeRoot returns the rootNode containing the initial JSON Schema

const root = compileSchema(mySchema);
const { node } = root.getNode("/image/title");

if (node) {
    assert(node.getNodeRoot() === root); // success
}

reduceNode

reduceNode compiles dynamic JSON schema keywords of a SchemaNode according to the given data. This utility helps walking down the schema-tree with a set of data and it helps getting a mostly complete json-schema for a specific data-value.

const { node: reducedNode } = compileSchema({
    properties: {
        trigger: { type: "boolean"}
    }
    dependentSchemas: {
        trigger: {
            required: ["title"],
            properties: {
                title: { type: "string" }
            }
        }
    }
}).reduceNode({ trigger: true });

expect(reducedNode.schema).to.deep.eq({
    required: ["title"],
    properties: {
        trigger: { type: "boolean"},
        title: { type: "string" }
    }
});

⚠️ Please be aware that certain schema-definitions are lost when resolving or merging sub-schemas. This mainly refers to validation-properties, but also some ambigiuous schema might get overriden.

toDataNodes

each iterates over each data-item (object, array and value) and emits the data-item, schema and location to a callback.

type DataNode = { pointer: string; value: unknown; node: SchemaNode };
const schemaNode = compileSchema(mySchema);
const nodes: DataNode[] = schemaNode.toDataNodes(myData);
Example
import { compileSchema, JsonSchema, JsonPointer } from "json-schema-library";

const mySchema: JsonSchema = {
    type: "array",
    items: [{ type: "number" }, { type: "string" }]
};

const schemaNode = compileSchema(mySchema);
schemaNode.toDataNodes([5, "nine"]).map((dataNode) => ({
    schema: dataNode.node.schema,
    value: dataNode.value,
    pointer: dataNode.pointer
}));

expect(calls).to.deep.equal([
    { schema: mySchema, value: [5, "nine"], pointer: "#" },
    { schema: { type: "number" }, value: 5, pointer: "#/0" },
    { schema: { type: "string" }, value: "nine", pointer: "#/1" }
]);

toSchemaNodes

eachSchema emits each sub-schema definition to a callback. A sub-schema is any schema-definition like in properties["property"], anyOf[1], contains, $defs["name"], etc.

const nodes: SchemaNode[] = compileSchema(mySchema).toSchemaNodes();
Example
import { compileSchema, JsonSchema, SchemaNode } from "json-schema-library";

const mySchema: JsonSchema = {
    type: "array",
    items: {
        oneOf: [{ type: "number" }, { $ref: "#/$defs/value" }]
    },
    $defs: {
        value: { type: "string" },
        object: { type: "object" }
    }
};

const nodes = compileSchema(mySchema)
    .toSchemaNodes(myCallback)
    .map((node) => node.schema);

expect(calls).to.deep.equal([
    mySchema,
    { oneOf: [{ type: "number" }, { $ref: "#/$defs/value" }] },
    { type: "number" },
    { $ref: "#/$defs/value" },
    { type: "string" },
    { type: "object" }
]);

validate

validate is a complete JSON Schema validator for your input data. Calling validate will return a list of validation errors for the passed data.

const { valid, errors } = compileSchema(myJsonSchema).validate(myData);
// { valid: boolean, errors: JsonError[] }
About type JsonError

In json-schema-library all errors are in the format of a JsonError:

type JsonError = {
    type: "error";
    code: string;
    message: string;
    data?: { [p: string]: any };
};

In almost all cases, a JSON Pointer is given on error.data.pointer, which points to the source within data where the error occured. For more details on how to work with errors, refer to section custom errors.

Example
const myJsonSchema: JsonSchema = {
    type: "object",
    additionalProperties: false
};

const { errors } = compileSchema(myJsonSchema).validate({ name: "my-data" });

expect(errors).to.deep.equal([
    {
        type: "error",
        code: "no-additional-properties-error",
        message: "Additional property `name` in `#` is not allowed",
        data: { property: "name", properties: [], pointer: "#" }
    }
]);

You can also use async validators to validate data with json-schema. For this, another property asyncErrors is exposed on validate:

const { isValid, errors, errorsAsync } = compileSchema(myJsonSchema).validate(myData);

if (errorsAsync.length > 0) {
    const additionalErrors = (await Promise.all(errorsAsync)).filter((err) => err != null);
}

Per default json-schema-library does not have async validators, so errorsAsync is always empty. If you add async validators, a list of Promise<JsonError|undefined> is return and you need to resolve and filter non-errors (undefined) yourself.

Note isValid only refers to errors. errorsAsync has to be evaluated separately

Example Async Validation
import { JsonSchemaValidator, draft2020 } from "json-schema-library";
// return Promise<JsonError>
const customValidator: JsonSchemaValidator = async ({ node, pointer, data }) => {
    return node.createError("type-error", {
        schema: {},
        pointer,
        value: data
    });
};

const draftList = [
    extendDraft(jsonEditorDraft, {
        keywords: {
            custom: customValidator
        }
    })
];

const { isValid, errorsAsync } = compileSchema({ custom: true }).validate("data");
console.log(isValid, errors.length); // true, 0

const errors = await Promise.all(errorsAsync);
console.log(errors); /// [{ code: "type-error", value: "data", pointer: "#", ... }]

Draft Customization

Extending a Draft · Keyword

json-schema-library uses the concept of drafts to support different versions of the JSON Schema specification — such as Draft 04, Draft 07, or 2020-12 — and to allow customization of schema behavior.

Each draft describes how a schema should be parsed, validated, and interpreted. Drafts can also be extended or modified to change or enhance behavior, such as:

  • Replacing or adding new keywords (oneOf, if/then, custom ones, etc.)
  • Defining or overriding format validators (format: "email", etc.)
  • Customizing or localizing error messages
  • Tweaking how schema nodes behave during parsing or resolution

Out of the box, the library exports all compliant JSON Schema drafts:

import { draft04, draft06, draft07, draft2019, draft2020 } from "json-schema-library";

When you compile a schema, the library will automatically select the correct draft based on the $schema field — or fall back to the last draft in the list:

compileSchema(schema, { drafts: [draft04, draft07, draft2020] });

A Draft is an object that defines the core behavior and extensions for a schema. It includes:

type Draft = {
    $schemaRegEx: string;
    version: DraftVersion;
    keywords: Keyword[];
    errors: ErrorConfig;
    formats: typeof formats;
    methods: {};
};

Here’s a breakdown of what each piece does:

$schemaRegEx

A regex string that identifies whether a draft should be used for a given schema, based on the $schema property. For example:

draft.$schemaRegEx === "draft[-/]2020-12";
// matches "$schema": "https://json-schema.org/draft/2020-12/schema"
// matches "$schema": "draft-2020-12"

When compiling, drafts are matched from left to right — the first one that matches is used. If no match is found, the last draft in the list is used as a fallback. If you're only using one draft, the $schemaRegEx check is skipped.

version

Describes the draft version (e.g., "2020-12"). This is mostly used for debugging and logging.

keywords

A list of keyword handlers for that draft, such as properties, allOf, oneOf, $ref, and more. Each keyword defines how the library should parse and validate that keyword. You can override, extend, or remove any keyword. Learn more in Keyword.

errors

An object mapping error types to either template strings or error functions. These can be used to customize error messages globally or define more intelligent error generation logic.

formats

An object mapping format names (like "email", "uuid", "date-time") to custom validation functions. You can override or add formats depending on your needs.

methods

Draft-specific implementations for certain core behaviors in SchemaNode, such as how child schemas are selected or how schemas are converted to data nodes. These can be overridden in custom drafts if needed.

Available methods
createSchema: typeof createSchema;
getChildSelection: typeof getChildSelection;
getData: typeof getData;
toDataNodes: typeof toDataNodes;

Extending a Draft

You may want to extend a draft when the default JSON Schema behavior does not fit your needs. Whether you want to add new keywords, modify error messages, or define custom formats for your validation, extendDraft helps you adjust the draft version to meet your specific requirements.

Examples:

import { extendDraft, draft2020, oneOfFuzzyKeyword, createCustomError, render, ErrorData } from "json-schema-library";

const myDraft = extendDraft(draft2020, {
    // Match all $schema
    $schemaRegEx: "",

    // Register a custom "oneOf" keyword, replacing the existing one
    keywords: [oneOfFuzzyKeyword],

    formats: {
        // Add a new "format": "test", which returns an error when the value is "test"
        test: ({ data, node, pointer }) => {
            if (data === "test") {
                return node.createError("test-error", {
                    schema: node.schema,
                    pointer: pointer,
                    value: data,
                    customValue: "test"
                });
            }
        }
    },

    errors: {
        // Add a new custom error "test-error"
        "test-error": "Test error for value {{value}} - {{customValue}}",

        // Overwrite the existing MaxLengthError message
        "max-length-error": "Too many characters",

        // Add a dynamic MinLengthError with custom logic
        "min-length-error": (data: ErrorData) => {
            if (data.minLength === 1) {
                return {
                    type: "error",
                    code: "min-length-one-error",
                    message: "Input is required",
                    data
                };
            }
            return {
                type: "error",
                code: "min-length-error",
                message: render("Value in `{{pointer}}` is `{{length}}`, but should be `{{minimum}}` at minimum", data),
                data
            };
        }
    }
});
About `oneOfFuzzyKeyword`

If you're working with complex schemas that use the oneOf keyword to validate multiple options, oneOfFuzzyKeyword offers an alternative approach. It scores the schemas to return the best match, even if none of the schemas fully validate the input data. This makes error messages more readable and helps identify the most appropriate schema when multiple options exist.

oneOfFuzzyKeyword helps when no schema fully validates the data but you want to prioritize schemas based on how well they fit the input. This makes it easier to interpret validation results for complex conditions.

Keyword

Keywords hold the main logic for JSON Schema functionality. Each Keyword corresponds to a JSON Schema keyword like properties, prefixItems, oneOf, etc and offers implementations to parse, validate, resolve and reduce. Note that support for each implementation is optional, dependending on the feature requirements. The main properties of a Keyword:

  • a Keyword is only processed if the specified keyword is available as property on the JSON Schema
  • an optional order property may be added as order of keyword execution is sometimes important (additionalItems last, $ref evaluation first)
  • the list of keywords is unique by property-value keyword
  • for a given function addX, a function X must be present
type Keyword = {
    id: string;
    keyword: string;
    order?: number;
    parse?: (node: SchemaNode) => void;
    addResolve?: (node: SchemaNode) => boolean;
    resolve?: JsonSchemaResolver;
    addValidate?: (node: SchemaNode) => boolean;
    validate?: JsonSchemaValidator;
    addReduce?: (node: SchemaNode) => boolean;
    reduce?: JsonSchemaReducer;
};

For examples on keyword implementations refer to ./src/keywords.

parse

parse will be executed on compile time, usually to add a compiled sub-schema on the parent-node.

Example of keyword using parse
export const notKeyword: Keyword = {
    id: "not",
    keyword: "not",
    parse: parseNot
};

export function parseNot(node: SchemaNode) {
    const { schema, spointer, schemaId } = node;
    if (schema.not != null) {
        node.not = node.compileSchema(schema.not, `${spointer}/not`, `${schemaId}/not`);
    }
}

resolve

A resolver returns a child-schema for a property-key, item-index or undefined if the key does not apply.

Example of keyword using resolve
export const propertiesKeyword: Keyword = {
    id: "property",
    keyword: "properties",
    parse: parseProperties,
    addResolve: (node: SchemaNode) => node.properties != null,
    resolve: propertyResolver
};

function propertyResolver({ node, key }: JsonSchemaResolverParams) {
    return node.properties?.[key];
}

reduce

A reducer replaces the JSON Schema keyword to a simple, static JSON Schema based on the current data

Example of keyword using reduce
export const typeKeyword: Keyword = {
    id: "type",
    keyword: "type",
    addReduce: (node) => Array.isArray(node.schema.type),
    reduce: reduceType,
    addValidate: ({ schema }) => schema.type != null,
    validate: validateType
};

function reduceType({ node, pointer, data }: JsonSchemaReducerParams): undefined | SchemaNode {
    const dataType = getJsonSchemaType(data, node.schema.type);
    if (dataType !== "undefined" && Array.isArray(node.schema.type) && node.schema.type.includes(dataType)) {
        return node.compileSchema({ ...node.schema, pointer, type: dataType }, node.spointer);
    }
    return undefined;
}

Keyword extensions

oneOfProperty

For oneOf resolution, JSON Schema states that data is valid if it validates against exactly one of those sub-schemas. In some scenarios this is unwanted behaviour, as the actual oneOf schema is known and only validation errors of this exact sub-schema should be returned.

For an explicit oneOf resolution, the JSON Schema may be extended by a property oneOfProperty. This will always associate an entry with a matching value (instead of schema validation) and return only this schema or validation errors, depending on the current task. For example:

const schema = {
    oneOfProperty: "id",
    oneOf: [
        {
            type: "object",
            properties: { id: { const: "1" }, title: { type: "number" } }
        },
        {
            type: "object",
            properties: { id: { const: "2" }, title: { type: "number" } }
        },
        {
            type: "object",
            properties: { id: { const: "3" }, title: { type: "number" } }
        }
    ]
};

const resolvedNode = compileSchema(schema).reduce({ id: "2", title: "not a number" });

// will always return (even if invalid)
expect(resolvedNode?.schema).to.deep.eq({
    type: "object",
    properties: { id: { const: "2" }, title: { type: "number" } }
});

Breaking Changes

v10.0.0

This update involves some significant changes in how you work with the library, so please carefully review the migration guide and adjust your implementation accordingly.

In version v10.0.0, we've made significant changes to the library’s API, particularly in how we handle drafts and schemas. These changes are required to support features like dynamicAnchor, unevaluatedItems, and oneOfIndex and to integrate with the headless-json-editor. The previous approach of directly working with JSON schema objects lacked the flexibility needed for more advanced features and extensibility.

The new implementation revolves around compiling schemas into a SchemaNode tree. This change offers a more fitting, simpler, and extensible approach to working with JSON schemas.

Key Changes:

  • Compile Schema: The compileSchema function now replaces the previous Draft-Class approach.
  • SchemaNode Representation: All schemas are now represented as SchemaNode, which holds the schema and provides an easier way to work with them.

Breaking Changes:

compileSchema is now a standalone function and replaces the Draft class. All return values for JSON Schema are now SchemaNode objects that contain a schema property.

```ts
// PREVIOUSLY
const draft = new Draft(schema);

// NOW
const node = compileSchema(schema);
```

Changed Methods:

  • draft.createSchemaOf(schema)node.createSchema(schema)
  • draft.each(data, callback)const nodes = node.toDataNodes(data)
  • draft.eachSchema(callback)const nodes = node.toSchemaNodes()
  • draft.getChildSchemaSelection(property)node.getChildSelection(property)
  • draft.getNode(options)node.getNode(pointer, data, options)
  • draft.getTemplate(inputData)node.getData(inputData)
  • draft.isValid(data)node.validate(data).valid
  • draft.step(property, data)node.getNodeChild(property, data)

Renamed Properties: templateDefaultOptionsgetDataDefaultOptions

Draft Customization: Customizing drafts has changed completely. The previous methods of extending drafts are no longer valid, and draft handling is now centered around SchemaNode.

Removed Error Property name: Error property name has been removed from JsonError in favor of code.

Removed Configuration Option: The templateDefaultOptions property has been removed from the global settings object. You should now configure it using the compileSchema options:

compileSchema(schema, {
    getDataDefaultOptions: {
        addOptionalProps: false,
        removeInvalidData: false,
        extendDefaults: true
    }
});

Changed remote $id support in addRemoteSchema. An $id has to be a valid url (previously any value was accepted)

v9.0.0

breaking changes:

  • getSchema signature changed in favour of an options object. Instead of draft.getNode(pointer, data) arguments have to be passed as an object draft.getNode({ pointer, data }). This removes setting unwanted optional arguments and keeps the api more stable in the future (e.g. withSchemaWarning option)
  • JsonError now must expose pointer, schema and value consistently on data property

updates

  • getSchema consistently returns errors and can return errors for empty schema using withSchemaWarning option

v8.0.0

With version v8.0.0, getData was improved to better support optional properties and utilize existing core logic, making it more reliable. Breaking changes:

  • Renamed JSONError to JsonError and JSONSchema to JsonSchema
  • getData only adds required properties. Behaviour can be changed by getData default options
  • Internal schema property oneOfSchema has been replaced by schema.getOneOfOrigin()
  • Changed unique-items-error to point to error for duplicated item and changed data-properties
  • Removed SchemaService as it was no longer used nor tested
Exposed new helper functions
  • mergeSchema - Merges to two json schema
  • reduceNode - Reduce schema by merging dynamic constructs into a static json schema omitting those properties
  • isDynamicSchema - Returns true if the passed schema contains dynamic properties (if, dependencies, allOf, etc)
  • resolveDynamicSchema - Resolves all dynamic schema definitions for the given input data and returns the resulting JSON Schema without any dynamic schema definitions.

About

Customizable and hackable json-validator and json-schema utilities for traversal, data generation and validation

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages