UNPKG

json-schema-library

Version:

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

201 lines (185 loc) 7.88 kB
import { copy } from "fast-copy"; import { getRef } from "./keywords/$ref"; import { draft04 } from "./draft04"; import { draft06 } from "./draft06"; import { draft07 } from "./draft07"; import { draft2019 } from "./draft2019"; import { draft2020 } from "./draft2020"; import { pick } from "./utils/pick"; import { JsonSchema, BooleanSchema, Draft, isJsonSchema, JsonAnnotation, JsonError, isJsonError, isJsonAnnotation, isBooleanSchema } from "./types"; import { TemplateOptions } from "./methods/getData"; import { SchemaNode, SchemaNodeMethods, addKeywords, isSchemaNode } from "./SchemaNode"; import settings from "./settings"; import sanitizeErrors from "./utils/sanitizeErrors"; const { REGEX_FLAGS } = settings; export type CompileOptions = { /** * List of drafts to support. * * Drafts are selected by testing the passed `schema.$schema` for a matching id, which * is tested by each draft's `Draft.$schemaRegEx`. In case no draft matches `schema.$schema` * the last draft in the list will be used. * * @default [draft04, draft06, draft07, draft2019, draft2020] * * @example * import { draft04, draft07, draft2020 } from "json-schema-library" * compileSchema({ $schema: "draft-04" }, { drafts: [draft04, draft07, draft2020] }) */ drafts?: Draft[]; /** * Fallback _draft_ version in case no _draft_ is specified by `schema.$schema`. * * Drafts are selected by given `schema.$schema` or the last draft from `drafts` as a fallback. * Specifying `draft` will workthe same as a specifying `schema.$schema` in case no $schema is * defined. When no match can be found, the last _draft_ from `drafts` will be used. * * @example * // uses draft-04 * compileSchema({ $schema: "draft-04" }, { drafts: [draft04, draft07, draft2020] }) * * // uses draft-2020-12 * compileSchema({}, { drafts: [draft04, draft07, draft2020] }) * * // uses draft-07 * compileSchema({}, { draft: "draft-07", drafts: [draft04, draft07, draft2020] }) * // uses draft-04 * compileSchema({ $schema: "draft-04" }, { draft: "draft-07", drafts: [draft04, draft07, draft2020] }) * * // uses draft-2020 * compileSchema({ $schema: "draft-04" }, { draft: "draft-07", drafts: [draft2020] }) */ draft?: string; /** * Set node and its remote schemata as remote schemata for this node and schema to resolve $ref */ remote?: SchemaNode; /** * a list of remotes to add, requires a unique $id for each schema. Will be ignored if `remote` is set */ remotes?: JsonSchema[]; /** * Enables `format`-keyword assertions when this is set tor `true` or sets assertion as defined by * the given meta-schema. Set to `false` to deactivate format validation. * * @default true */ formatAssertion?: boolean | "meta-schema" | undefined; /** Set default options for all `node.getData` requests */ getDataDefaultOptions?: TemplateOptions; /** Set to true to throw an Error on errors in input schema. Defaults to false */ throwOnInvalidSchema?: boolean; /** Set to true to throw an Error when encountering an unresolvable ref */ throwOnInvalidRef?: boolean; }; const defaultDrafts: Draft[] = [draft04, draft06, draft07, draft2019, draft2020]; function getDraft(drafts: Draft[], $schema: string) { return drafts.find((d) => new RegExp(d.$schemaRegEx, REGEX_FLAGS).test($schema)) ?? drafts[drafts.length - 1]; } /** * With compileSchema we replace the schema and all sub-schemas with a schemaNode, * wrapping each schema with utilities and as much preevaluation as possible. Each * node will be reused for each task, but will create a compiledNode for bound data. */ export function compileSchema(schema: JsonSchema | BooleanSchema, options: CompileOptions = {}) { let remote = options.remote; if (Array.isArray(options.remotes) && options.remotes.length > 0 && !options.remote) { const remotes = [...options.remotes]; remotes.forEach((remote, index) => { if (remote.$id == null) { throw new Error(`required $id on remotes[${index}] is missing`); } }); remote = compileSchema(remotes.shift()!); remotes.forEach((r) => remote?.addRemoteSchema(r.$id, r)); } let formatAssertion = options.formatAssertion ?? true; const drafts = options.drafts ?? defaultDrafts; const draft = getDraft(drafts, isJsonSchema(schema) ? (options.draft ?? schema.$schema) : undefined); const node: SchemaNode & { schemaErrors?: JsonError[]; schemaAnnotations: JsonAnnotation[] } = { evaluationPath: "#", lastIdPointer: "#", schemaLocation: "#", dynamicId: "", reducers: [], resolvers: [], validators: [], schema: schema as JsonSchema, // @ts-expect-error self-reference added later context: { remotes: {}, dynamicAnchors: {}, ...(remote?.context ?? {}), anchors: {}, refs: {}, ...copy(pick(draft, "methods", "keywords", "version", "formats", "errors")), draft: options.draft, getDataDefaultOptions: options.getDataDefaultOptions, throwOnInvalidRef: options.throwOnInvalidRef ?? false, drafts }, ...SchemaNodeMethods }; node.context.rootNode = node; node.context.remotes[(isJsonSchema(schema) ? schema.$id : undefined) ?? "#"] = node; if (remote) { const metaSchema = getRef(node, node.schema.$schema); if (isSchemaNode(metaSchema) && metaSchema.schema.$vocabulary) { const vocabs = Object.keys(metaSchema.schema.$vocabulary); // const withAnnotations = vocabs.find((vocab) => vocab.includes("vocab/format-annotation")); const formatAssertionString = vocabs.find((vocab) => vocab.includes("vocab/format-assertion")); if (formatAssertionString && formatAssertion === "meta-schema") { formatAssertion = metaSchema.schema.$vocabulary[formatAssertionString] === true; } const validKeywords = Object.keys(metaSchema.getData({}, { addOptionalProps: true }) as object); if (validKeywords.length > 0) { node.context.keywords = node.context.keywords.filter((f) => validKeywords.includes(f.keyword)); } } } if (formatAssertion === false) { node.context.keywords = node.context.keywords.filter((f) => f.keyword !== "format"); } if (!isJsonSchema(schema) && !isBooleanSchema(schema)) { node.schemaErrors = [ node.createError("schema-error", { pointer: "#", schema, value: undefined, message: `JSON schema must be object or boolean - reveived: '${schema}'` }) ]; return node; } // parse and validate schema let schemaValidation = addKeywords(node).filter((err) => err != null); schemaValidation = sanitizeErrors(schemaValidation); const schemaErrors: JsonError[] = []; const schemaAnnotations: JsonAnnotation[] = []; schemaValidation.forEach((error) => { if (isJsonError(error)) { schemaErrors.push(error); } else if (isJsonAnnotation(error)) { schemaAnnotations.push(error); } }); if (options.throwOnInvalidSchema && schemaErrors.length > 0) { const error = new Error("Invalid schema passed to compileSchema"); // @ts-expect-error unknown error-property error.data = schemaErrors; throw error; } node.schemaErrors = schemaErrors; node.schemaAnnotations = schemaAnnotations; return node; }