UNPKG

json-schema-describes-subset

Version:

Tools for static JSON schema analysis, including functions to determine if one schema describes a subset of another or if a schema describes the empty set or to convert a schema to its disjunctive normal form (DNF).

323 lines (322 loc) 9.22 kB
import type { JSONSchema } from '../json-schema/index.js'; import type { Options } from '../options/index.js'; /** * Tries to determine whether the first argument JSON schema * (`potentialSubsetSchema`) describes a subset of the set of data values * described by the second argument JSON schema (`potentialSupersetSchema`). * * @returns Returns `true` if it does find a reason to do so. * * If such a reason cannot be found, usually `null` is returned to indicate * the possibility of false negatives. (Not having found any reason to return * `true` doesn't mean that there aren't any.) * * This behavior is sufficient for many use cases and has been the focus so far. * The ability to determine true positive `true` results is fairly powerful and * will work in many complex cases. (See the following [examples](https://github.com/jobohner/json-schema-describes-subset/blob/v0.4.0/src/schema-describes-subset/schema-describes-subset.ts#example) and * [Limitations](https://github.com/jobohner/json-schema-describes-subset/blob/v0.4.0/docs/README.md#limitations).) * The true positive `false` return value is currently only returned if an * example data value that satisfies `potentialSubsetSchema` but not * `potentialSupersetSchema` can be trivially found. * See [Limitations](https://github.com/jobohner/json-schema-describes-subset/blob/v0.4.0/docs/README.md#limitations) for more details. * * @example If a few of the following examples that return `true` seem unintuitive * at first glance, try to find a data value that satisfies the first schema but * not the second one. Failing to find such a data value might help to * understand why `true` is returned. (If, contrary to expectations, you * actually are able to find such a data value, please do report a * [bug](https://github.com/jobohner/json-schema-describes-subset/issues)). * * ```ts * import { schemaDescribesSubset } from 'json-schema-describes-subset' * * console.log( * schemaDescribesSubset( * { * type: 'number', * }, * true, * ), * ) // logs: `true` * * console.log( * schemaDescribesSubset(false, { * type: 'number', * }), * ) // logs: `true` * * console.log( * schemaDescribesSubset( * { * type: ['number', 'boolean', 'string', 'null'], * }, * { type: ['number', 'null'] }, * ), * ) // logs: `false` * * console.log( * schemaDescribesSubset( * { type: 'integer' }, * { type: ['number', 'string', 'boolean'] }, * ), * ) // logs: `true` * * console.log( * schemaDescribesSubset( * { * minimum: 5.5, * }, * { * exclusiveMinimum: 5.5, * }, * ), * ) // logs: `false` * * console.log( * schemaDescribesSubset( * { * minimum: 5.6, * }, * { * exclusiveMinimum: 5.5, * }, * ), * ) // logs: `true` * * console.log( * schemaDescribesSubset( * { minimum: 10, maximum: 30, multipleOf: 5 }, * { anyOf: [{ multipleOf: 3 }, { multipleOf: 20 }, { enum: [10, 25] }] }, * ), * ) // logs: `true` * * console.log( * schemaDescribesSubset( * { type: 'string', maxLength: 5, minLength: 10 }, * { type: 'null' }, * ), * ) // logs: `true` * * console.log( * schemaDescribesSubset( * { * prefixItems: [{ type: 'string' }, { type: 'boolean' }], * items: { type: 'object' }, * }, * { * prefixItems: [ * { type: ['string', 'number'] }, * { type: 'boolean' }, * { type: 'object' }, * ], * }, * ), * ) // logs: `true` * * console.log( * schemaDescribesSubset( * { contains: { type: 'number' }, minContains: 5 }, * { minItems: 5 }, * ), * ) // logs: `true` * * console.log( * schemaDescribesSubset( * { * prefixItems: [{ type: 'number' }, { type: 'boolean' }], * items: { type: 'string' }, * maxItems: 3, * }, * { uniqueItems: true }, * ), * ) // logs: `true` * * console.log( * schemaDescribesSubset( * { required: ['a'], maxProperties: 2 }, * { * anyOf: [ * { properties: { b: { type: 'string' } } }, * { properties: { c: { type: 'string' } } }, * ], * }, * ), * ) // logs: `true` * * console.log( * schemaDescribesSubset( * { maxProperties: 2, required: ['abc', 'def'] }, * { propertyNames: { minLength: 2 } }, * ), * ) // logs: `true` * * console.log( * schemaDescribesSubset( * { maxProperties: 1 }, * { * anyOf: [ * { properties: { x: { type: 'string' } } }, * { patternProperties: { '^a$': { type: 'string' } } }, * ], * }, * ), * ) // logs: `true` * * console.log( * schemaDescribesSubset( * { * additionalProperties: { type: 'number' }, * properties: { a: { type: 'string' } }, * }, * { * additionalProperties: { type: 'number' }, * properties: { * a: { type: 'string' }, * b: { type: ['boolean', 'number'] }, * }, * }, * ), * ) // logs: `true` * * console.log( * schemaDescribesSubset( * { * allOf: [ * { * properties: { * aa: { type: 'string' }, * aaa: { type: 'string' }, * aaaa: { type: 'string' }, * }, * patternProperties: { * '^b+$': { type: 'string' }, * }, * }, * { * additionalProperties: { type: 'number' }, * patternProperties: { * '^a+$': { type: 'string' }, * '^b+$': true, * }, * }, * { * propertyNames: { not: { pattern: '^b+$' } }, * }, * ], * }, * { * additionalProperties: { type: 'number' }, * patternProperties: { * '^a+$': { type: 'string' }, * }, * }, * ), * ) // logs: `true` * * console.log( * schemaDescribesSubset( * { * patternProperties: { * '^a+$': { type: 'string' }, * '^b+$': { type: 'boolean' }, * }, * propertyNames: { pattern: '^a+$' }, * }, * { * additionalProperties: false, * patternProperties: { '^a+$': { type: 'string' } }, * }, * ), * ) // logs: `true` * * console.log( * schemaDescribesSubset( * { required: ['a', 'b', 'c'] }, * { dependentRequired: { a: ['b', 'c'] } }, * ), * ) // logs: `true` * * console.log( * schemaDescribesSubset( * { * properties: { * b: { type: 'number' }, * }, * additionalProperties: false, * }, * { * properties: { * b: { type: ['string', 'number'] }, * }, * dependentSchemas: { * a: { * properties: { * b: { * type: 'string', * }, * }, * }, * }, * }, * ), * ) // logs: `true` * ``` * * @remarks Use Cases * * This function is useful whenever you want to ensure that different data * interfaces are compatible with each other. * * For example, it can be used to check whether a new API version is backwards * compatible with the old one. * * Several other good use cases where a function like * `schemaDescribesSubset` might come in handy, are described in the * introduction of the paper * [Type Safety with JSON Subschema](https://arxiv.org/abs/2106.05271), which * follows the same goal as this function using a slightly different approach. * * ### How does this work? * * The implementation utilizes {@link schemaDescribesEmptySet} and the fact that * A  B if and only if A  ¬B = ∅. (That * relation should be obvious if illustrated in a venn diagram.) * * It basically looks similar to this: * * ```typescript * function schemaDescribesSubset( * potentialSubsetSchema: JSONSchema, * potentialSupersetSchema: JSONSchema, * ): boolean | null { * return schemaDescribesEmptySet({ * allOf: [ * potentialSubsetSchema, * { not: potentialSupersetSchema }, * ], * }) * } * ``` * * ### Good to know: Validation using `schemaDescribesSubset` * * `schemaDescribesSubset` uses * [Ajv](https://ajv.js.org/json-schema.html#draft-2020-12) to validate `consts` * among others. It can be configured using {@link ValidationPlugin}s. If you * ever need a routine that validates a value `a` against a schema `B` and that * is equally configured, an alternative to importing and configuring Ajv would * be to use: * * ```typescript * schemaDescribesSubset({const: a}, B) * ``` * * This is one of the cases where * [a definite boolean is always returned and never `null`](https://github.com/jobohner/json-schema-describes-subset/blob/v0.4.0/docs/README.md#limitations). * * However, since this is not optimized for performance, configuring and using a * validator might often be the better choice. * */ export declare function schemaDescribesSubset(potentialSubsetSchema: JSONSchema, potentialSupersetSchema: JSONSchema, options?: Options | undefined): boolean | null; export type SchemaDescribesSubset = typeof schemaDescribesSubset;