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).

594 lines (593 loc) 19.8 kB
import { type JSONSchema } from '../json-schema/index.js'; import type { RawDNF, ConjunctionOfLiterals } from '../atomic-schema/to-raw-dnf/index.js'; import type { JSONSchemaType } from '../json-schema-type/index.js'; import { type InternalOptions, type Options, type OptionsSimplificationPlugin } from '../options/index.js'; import { type NonConstConjunctionSchemaObject, type ConjunctionSchemaObject, type SimplificationPlugin, type SimplificationResultSchemaByType, type SimplificationResultSchemaForType, type ValidateConst } from '../plugin/index.js'; import type { InstancesByConstructor } from '../utils/instances-by-constructor/index.js'; import type { BuiltInSimplificationPlugin } from '../built-in-plugins/index.js'; export declare function createValidateConst(options: InternalOptions, schema: JSONSchema): ValidateConst; /** * Brings the given disjunct of a {@link RawDNF} to a simpler canonical form by * summarizing keywords. Returns `false` (which is a valid JSON Schema) if there * is a contradiction in the disjunct. * */ export declare function simplifyDisjunct<Type extends JSONSchemaType, SimplificationPlugin_ extends SimplificationPlugin>({ type, atomicSchemasByConstructor, negatedAtomicSchemasByConstructor, options, validateConst, }: { type: Type; atomicSchemasByConstructor: InstancesByConstructor; negatedAtomicSchemasByConstructor: InstancesByConstructor; options: InternalOptions<SimplificationPlugin_>; validateConst: ValidateConst; }): SimplificationResultSchemaForType<SimplificationPlugin_, Type>; export type JSONSchemaTypeForDNF = Exclude<JSONSchemaType, 'null' | 'boolean'>; export declare function isJSONSchemaTypeForDNF(type: JSONSchemaType): type is JSONSchemaTypeForDNF; /** * @param rawDisjunct Represents multiple disjuncts, because there might be * multiple types * */ export declare function simplifyRawDisjunct<SimplificationPlugin_ extends SimplificationPlugin>(rawDisjunct: ConjunctionOfLiterals, validateConst: ValidateConst, options: InternalOptions<SimplificationPlugin_>): SimplificationResultSchemaByType<SimplificationPlugin_, JSONSchemaTypeForDNF>[]; export type Disjunct<SimplificationPlugin_ extends SimplificationPlugin> = Exclude<SimplificationResultSchemaByType<SimplificationPlugin_, JSONSchemaTypeForDNF>, boolean>; export type DNF<SimplificationPlugin_ extends SimplificationPlugin> = boolean | { anyOf: Disjunct<SimplificationPlugin_>[]; }; /** * This is equivalent to * * ```ts * type GeneralDNFSpelledOut = * | boolean * | { * anyOf: ( * | { const: unknown } * | { * [mergeableKeyword: string]: unknown * type: 'string' | 'number' | 'object' | 'array' * allOf?: JSONSchema[] * const?: never * anyOf?: never * not?: never * } * )[] * } * ``` * */ export type GeneralDNF = DNF<SimplificationPlugin>; /** * Is equivalent to {@link GeneralDNF} * */ export type GeneralDNFSpelledOut = boolean | { anyOf: ({ const: unknown; } | { [mergeableKeyword: string]: unknown; type: 'string' | 'number' | 'object' | 'array'; allOf?: JSONSchema[]; const?: never; anyOf?: never; not?: never; })[]; }; /** * This is equivalent to * * ```ts * type DefaultDNFSpelledOut = * | boolean * | { * anyOf: ( * | { const: unknown } * | { * type: 'number' * maximum?: number * minimum?: number * multipleOf?: number * allOf?: ( * | { not: { const: number } } * | { not: { multipleOf: number } } * | { $ref: string } * | { not: { $ref: string } } * )[] * const?: never * anyOf?: never * not?: never * } * | { * type: 'string' * maxLength?: number * minLength?: number * allOf?: ( * | { not: { const: string } } * | { pattern: string } * | { not: { pattern: string } } * | { $ref: string } * | { not: { $ref: string } } * )[] * const?: never * anyOf?: never * not?: never * } * | { * type: 'object' * maxProperties?: number * minProperties?: number * patternProperties?: Record<string, JSONSchema> * properties?: Record<string, JSONSchema> * propertyNames?: JSONSchema * required?: string[] * allOf?: ( * | { not: { const: Record<string, unknown> } } * | { * additionalProperties: JSONSchema * properties?: Record<string, true> * patternProperties?: Record<string, true> * } * | { not: { patternProperties: Record<string, JSONSchema> } } * | { * not: { * additionalProperties: JSONSchema * properties?: Record<string, true> * patternProperties?: Record<string, true> * } * } * | { not: { propertyNames: JSONSchema } } * | { $ref: string } * | { not: { $ref: string } } * )[] * const?: never * anyOf?: never * not?: never * } * | { * type: 'array' * items?: JSONSchema * maxItems?: number * minItems?: number * prefixItems?: JSONSchema[] * uniqueItems?: boolean * allOf?: ( * | { not: { const: unknown[] } } * | { * contains: JSONSchema * minContains?: number * maxContains?: number * } * | { * not: { uniqueItems?: boolean } * } * | { * not: { * prefixItems?: true[] * items?: JSONSchema * } * } * | { $ref: string } * | { not: { $ref: string } } * )[] * const?: never * anyOf?: never * not?: never * } * )[] * } * ``` * */ export type DefaultDNF = DNF<BuiltInSimplificationPlugin>; /** * Is equivalent to {@link DefaultDNF} * */ export type DefaultDNFSpelledOut = boolean | { anyOf: ({ const: unknown; } | { type: 'number'; maximum?: number; minimum?: number; multipleOf?: number; allOf?: ({ not: { const: number; }; } | { not: { multipleOf: number; }; } | { $ref: string; } | { not: { $ref: string; }; })[]; const?: never; anyOf?: never; not?: never; } | { type: 'string'; maxLength?: number; minLength?: number; allOf?: ({ not: { const: string; }; } | { pattern: string; } | { not: { pattern: string; }; } | { $ref: string; } | { not: { $ref: string; }; })[]; const?: never; anyOf?: never; not?: never; } | { type: 'object'; maxProperties?: number; minProperties?: number; patternProperties?: Record<string, JSONSchema>; properties?: Record<string, JSONSchema>; propertyNames?: JSONSchema; required?: string[]; allOf?: ({ not: { const: Record<string, unknown>; }; } | { additionalProperties: JSONSchema; properties?: Record<string, true>; patternProperties?: Record<string, true>; } | { not: { patternProperties: Record<string, JSONSchema>; }; } | { not: { additionalProperties: JSONSchema; properties?: Record<string, true>; patternProperties?: Record<string, true>; }; } | { not: { propertyNames: JSONSchema; }; } | { $ref: string; } | { not: { $ref: string; }; })[]; const?: never; anyOf?: never; not?: never; } | { type: 'array'; items?: JSONSchema; maxItems?: number; minItems?: number; prefixItems?: JSONSchema[]; uniqueItems?: boolean; allOf?: ({ not: { const: unknown[]; }; } | { contains: JSONSchema; minContains?: number; maxContains?: number; } | { not: { uniqueItems?: boolean; }; } | { not: { prefixItems?: true[]; items?: JSONSchema; }; } | { $ref: string; } | { not: { $ref: string; }; })[]; const?: never; anyOf?: never; not?: never; })[]; }; /** * Sort const schemas to the start of the array (mainly for aesthetic reasons) * */ export declare function sortCompareDisjuncts(...disjuncts: [ ConjunctionSchemaObject<NonConstConjunctionSchemaObject>, ConjunctionSchemaObject<NonConstConjunctionSchemaObject> ]): -1 | 0 | 1; /** * Removes unnecessary disjuncts (if they are subsets of other disjuncts) * */ export declare function removeSubsetDisjuncts<Disjunct_ extends JSONSchema>(disjuncts: Disjunct_[], schemaDescribesSubset: (potentialSubset: JSONSchema, potentialSuperset: JSONSchema) => boolean | null): Disjunct_[]; /** * Removes unnecessary conjuncts (if they are supersets of other conjuncts) * */ export declare function removeSupersetConjuncts<Conjunct_ extends JSONSchema>(conjuncts: Conjunct_[], schemaDescribesSubset: (potentialSubset: JSONSchema, potentialSuperset: JSONSchema) => boolean | null): Conjunct_[]; export type DNFFromOptions<Options_ extends Options | undefined> = DNF<BuiltInSimplificationPlugin | OptionsSimplificationPlugin<Options_>>; /** * Transforms the given schema to a [disjunctive normal * form](https://en.wikipedia.org/wiki/Disjunctive_normal_form) similar to the * one utilized by {@link schemaDescribesEmptySet}. * * @returns The resulting dnf schema will be equivalent to the provided schema (meaning * that it will accept the same data values) but all * [boolean combinations](https://json-schema.org/understanding-json-schema/reference/combining) * will be restructured. * * Subschemas that represent property values of a JSON object or elements of a * JSON array do not represent boolean combinations. They are currently * considered atomic for that purpose. * * The resulting dnf schema will be simplified so that disjuncts that were determined * to be unsatisfiable are already eliminated. If each disjunct was determined * to be unsatisfiable the return value is `false`. * * The return type's most general form (without specified * {@link Options.plugins | plugin} types, for example returned by * `toDNF<Options>(...)`) is equivalent to: * * ```ts * type GeneralDNFSpelledOut = * | boolean * | { * anyOf: ( * | { const: unknown } * | { * [mergeableKeyword: string]: unknown * type: 'string' | 'number' | 'object' | 'array' * allOf?: JSONSchema[] * const?: never * anyOf?: never * not?: never * } * )[] * } * ``` * * If the provided option's type does not contain any custom * {@link Options.plugins | plugins}, the default return type (for example * returned by `toDNF(schema)` (without options) or by * `toDNF<{ plugins: [] }>(...)`) is equivalent to: * * ```ts * type DefaultDNFSpelledOut = * | boolean * | { * anyOf: ( * | { const: unknown } * | { * type: 'number' * maximum?: number * minimum?: number * multipleOf?: number * allOf?: ( * | { not: { const: number } } * | { not: { multipleOf: number } } * | { $ref: string } * | { not: { $ref: string } } * )[] * const?: never * anyOf?: never * not?: never * } * | { * type: 'string' * maxLength?: number * minLength?: number * allOf?: ( * | { not: { const: string } } * | { pattern: string } * | { not: { pattern: string } } * | { $ref: string } * | { not: { $ref: string } } * )[] * const?: never * anyOf?: never * not?: never * } * | { * type: 'object' * maxProperties?: number * minProperties?: number * patternProperties?: Record<string, JSONSchema> * properties?: Record<string, JSONSchema> * propertyNames?: JSONSchema * required?: string[] * allOf?: ( * | { not: { const: Record<string, unknown> } } * | { * additionalProperties: JSONSchema * properties?: Record<string, true> * patternProperties?: Record<string, true> * } * | { not: { patternProperties: Record<string, JSONSchema> } } * | { * not: { * additionalProperties: JSONSchema * properties?: Record<string, true> * patternProperties?: Record<string, true> * } * } * | { not: { propertyNames: JSONSchema } } * | { $ref: string } * | { not: { $ref: string } } * )[] * const?: never * anyOf?: never * not?: never * } * | { * type: 'array' * items?: JSONSchema * maxItems?: number * minItems?: number * prefixItems?: JSONSchema[] * uniqueItems?: boolean * allOf?: ( * | { not: { const: unknown[] } } * | { * contains: JSONSchema * minContains?: number * maxContains?: number * } * | { * not: { uniqueItems?: boolean } * } * | { * not: { * prefixItems?: true[] * items?: JSONSchema * } * } * | { $ref: string } * | { not: { $ref: string } } * )[] * const?: never * anyOf?: never * not?: never * } * )[] * } * ``` * * The return type will adjust according to the (explicit or inferred) type of * the property `plugins` of the provided `options`. * * @example ```typescript * import { toDNF } from 'json-schema-describes-subset' * * console.log( * toDNF({ * anyOf: [{ minimum: 2 }, { exclusiveMinimum: 1 }], * }), * ) * ``` * * logs: * * ```json * { * "anyOf": [ * { "const": null }, * { "const": true }, * { "const": false }, * { "type": "number", "minimum": 1, "allOf": [{ "not": { "const": 1 } }] }, * { "type": "string" }, * { "type": "array" }, * { "type": "object" } * ] * } * ``` * * *** * * ```typescript * import { toDNF } from 'json-schema-describes-subset' * * console.log( * toDNF({ * anyOf: [{ multipleOf: 2 }, { multipleOf: 3 }, { multipleOf: 4 }], * }), * ) * ``` * * logs: * * ```json * { * "anyOf": [ * { "const": null }, * { "const": true }, * { "const": false }, * { "type": "number", "multipleOf": 2 }, * { "type": "number", "multipleOf": 3 }, * { "type": "string" }, * { "type": "array" }, * { "type": "object" } * ] * } * ``` * * @remarks Use cases * * This function was created mainly for demonstration purposes, but might also * have some real world use cases. For example when creating a data mocking * tool, that generates example data for a given schema, it might be easier to * generate that data for one of the logically flat disjuncts instead of a * complex schema which is logically deeply nested. * */ export declare function toDNF<const Options_ extends Options | undefined = undefined>(schema: JSONSchema, options?: Options_): DNFFromOptions<Options_>; export declare function rawDisjunctDescribesEmptySet<SimplificationPlugin_ extends SimplificationPlugin>(rawDisjunct: ConjunctionOfLiterals, validateConst: ValidateConst, options: InternalOptions<SimplificationPlugin_>): boolean | null; export declare function rawDNFDescribesEmptySet(rawDNF: RawDNF, validateConst: ValidateConst, options: InternalOptions): boolean | null; /** * Tries to determine whether the provided JSON Schema is unsatisfiable and * therefore describes the empty set. In that case, the schema would be * equivalent to the `false` schema. * * @returns Returns `true` if it does find a reason why the schema will not accept any * value. * * If such a reason cannot be found, usually `null` is returned to indicate the * possibility of false negatives. * * The true positive `false` return value is currently only returned if an * example data value that satisfies the schema can be trivially found. * See [Limitations](https://github.com/jobohner/json-schema-describes-subset/blob/v0.4.0/src/dnf/dnf.ts#limitations) for more details. * * @example ```ts * import { schemaDescribesEmptySet } from 'json-schema-describes-subset' * * console.log(schemaDescribesEmptySet(false)) // logs: `true` * * console.log( * schemaDescribesEmptySet( * // this schema will accept anything that is not a number * { minimum: 2, maximum: 1 }, * ), * ) // logs: `false` * * console.log( * schemaDescribesEmptySet({ * type: 'number', * minimum: 2, * maximum: 1, * }), * ) // logs: `true` * ``` * * @remarks How does this work? * * The provided schema is first transformed to a [disjunctive normal * form](https://en.wikipedia.org/wiki/Disjunctive_normal_form) similar to the * one returned by {@link toDNF}. Then each disjunct is checked for * contradictions which would make it unsatisfiable. If a contradiction is found * for each disjunct, the complete schema is unsatisfiable and `true` is * returned. * */ export declare function schemaDescribesEmptySet(schema: JSONSchema, options?: Options | undefined): boolean | null; export type SchemaDescribesEmptySet = typeof schemaDescribesEmptySet; export declare function schemaDescribesEmptySetInternal(schema: JSONSchema, options: InternalOptions): boolean | null; export type SchemaDescribesEmptySetInternal = typeof schemaDescribesEmptySetInternal;