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).
331 lines • 9.55 kB
JavaScript
import {} from '../plugin/index.js';
import { schemaDescribesEmptySet } from '../dnf/index.js';
import { schemaDescribesSubsetFactory } from './schema-describes-subset-factory.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 function schemaDescribesSubset(potentialSubsetSchema, potentialSupersetSchema, options) {
return schemaDescribesSubset_(potentialSubsetSchema, potentialSupersetSchema, options);
}
/**
* Awkward construct to have this work with the factory function and at the same
* time have a nicely documented {@link schemaDescribesSubset}.
*
*/
const schemaDescribesSubset_ = schemaDescribesSubsetFactory(schemaDescribesEmptySet);
//# sourceMappingURL=schema-describes-subset.js.map