@x0k/json-schema-merge 1.0.3 → 1.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,880 @@
1
+ import type {
2
+ JSONSchema7,
3
+ JSONSchema7Definition,
4
+ JSONSchema7Type,
5
+ JSONSchema7TypeName,
6
+ } from "json-schema";
7
+
8
+ import {
9
+ intersection,
10
+ union,
11
+ type Deduplicator,
12
+ type Intersector,
13
+ } from "../../array.ts";
14
+ import { identity } from "../../function.ts";
15
+ import { lcm } from "../../math.ts";
16
+ import { isAllowAnySchema } from "../json-schema.ts";
17
+
18
+ import { simplePatternsMerger } from "./patterns.ts";
19
+
20
+ type SchemaKey = keyof JSONSchema7;
21
+
22
+ function createPairCombinations<T, R>(
23
+ l: T[],
24
+ r: T[],
25
+ action: (a: T, b: T) => R
26
+ ) {
27
+ const ll = l.length;
28
+ const rl = r.length;
29
+ if (ll > 0 && rl > 0) {
30
+ for (let i = 0; i < ll; i++) {
31
+ const lv = l[i]!;
32
+ for (let j = 0; j < rl; j++) {
33
+ action(lv, r[j]!);
34
+ }
35
+ }
36
+ }
37
+ }
38
+
39
+ function mergeBooleans(l: boolean, r: boolean) {
40
+ return l || r;
41
+ }
42
+
43
+ function createRecordsMerge<T>(merge: (l: T, r: T) => T) {
44
+ return (left: Record<string, T>, right: Record<string, T>) => {
45
+ const target = { ...left };
46
+ const keys = Object.keys(right);
47
+ const l = keys.length;
48
+ for (let i = 0; i < l; i++) {
49
+ const key = keys[i]!;
50
+ target[key] =
51
+ left[key] === undefined ? right[key]! : merge(left[key], right[key]!);
52
+ }
53
+ return target;
54
+ };
55
+ }
56
+
57
+ /**
58
+ * An assigner function operates at the schema-object level.
59
+ * It receives the partially merged `target` and the original
60
+ * `left` and `right` schemas.
61
+ *
62
+ * In most cases, it modifies and returns the `target` object,
63
+ * but it may also return a completely new schema object if needed.
64
+ *
65
+ * Assigners are used for keywords that cannot be merged by simple
66
+ * value-level functions, often because they interact with other
67
+ * keywords or require holistic decisions.
68
+ */
69
+ export type Assigner<R extends {}> = (target: R, l: R, r: R) => R;
70
+
71
+ function createMap<R>(items: Iterable<[SchemaKey[], R]>) {
72
+ const map = new Map<SchemaKey, R>();
73
+ for (const pair of items) {
74
+ for (const key of pair[0]) {
75
+ map.set(key, pair[1]);
76
+ }
77
+ }
78
+ return map;
79
+ }
80
+
81
+ function assignSchemaDefinitionOrRecordOfSchemaDefinitions<
82
+ K extends {
83
+ [T in SchemaKey]: JSONSchema7[T] extends
84
+ JSONSchema7Definition | Record<string, JSONSchema7Definition> | undefined
85
+ ? T
86
+ : never;
87
+ }[SchemaKey],
88
+ >(target: JSONSchema7, key: K, value: JSONSchema7[K]) {
89
+ if (value === undefined || isAllowAnySchema(value)) {
90
+ delete target[key];
91
+ } else {
92
+ target[key] = value;
93
+ }
94
+ }
95
+
96
+ const PROPERTIES_ASSIGNER_KEYS = [
97
+ "properties",
98
+ "patternProperties",
99
+ "additionalProperties",
100
+ ] as const satisfies SchemaKey[];
101
+
102
+ interface CompiledPattern {
103
+ regExp: RegExp;
104
+ schema: JSONSchema7Definition;
105
+ }
106
+
107
+ function compilePatterns(patterns: Record<string, JSONSchema7Definition>) {
108
+ const keys = Object.keys(patterns);
109
+ const l = keys.length;
110
+ const result: CompiledPattern[] = [];
111
+ for (let i = 0; i < l; i++) {
112
+ const source = keys[i]!;
113
+ result.push({
114
+ regExp: new RegExp(source),
115
+ schema: patterns[source]!,
116
+ });
117
+ }
118
+ return [result, keys] as const;
119
+ }
120
+
121
+ const EMPTY_PATTERNS_AND_KEYS: [CompiledPattern[], string[]] = [[], []];
122
+
123
+ /**
124
+ * @returns `true` when `false` schema occurred
125
+ */
126
+ function appendKeyConstraints(
127
+ target: (JSONSchema7 | true)[],
128
+ key: string,
129
+ patterns: CompiledPattern[]
130
+ ): boolean {
131
+ const l = patterns.length;
132
+ for (let i = 0; i < l; i++) {
133
+ const p = patterns[i]!;
134
+ if (!p.regExp.test(key)) {
135
+ continue;
136
+ }
137
+ const s = p.schema;
138
+ if (s === false) {
139
+ return true;
140
+ }
141
+ target.push(s);
142
+ }
143
+ return false;
144
+ }
145
+
146
+ const ITEMS_ASSIGNER_KEYS = [
147
+ "items",
148
+ "additionalItems",
149
+ ] as const satisfies SchemaKey[];
150
+
151
+ const CONDITION_ASSIGNER_KEYS = [
152
+ "if",
153
+ "then",
154
+ "else",
155
+ ] as const satisfies SchemaKey[];
156
+
157
+ function assignCondition(target: JSONSchema7, source: JSONSchema7) {
158
+ if (source.if !== undefined) {
159
+ target.if = source.if;
160
+ }
161
+ if (source.then !== undefined) {
162
+ target.then = source.then;
163
+ }
164
+ if (source.else !== undefined) {
165
+ target.else = source.else;
166
+ }
167
+ return target;
168
+ }
169
+
170
+ type AssignerKey =
171
+ | (typeof PROPERTIES_ASSIGNER_KEYS)[number]
172
+ | (typeof ITEMS_ASSIGNER_KEYS)[number]
173
+ | (typeof CONDITION_ASSIGNER_KEYS)[number];
174
+
175
+ function intersectSchemaTypes(
176
+ a: JSONSchema7TypeName,
177
+ b: JSONSchema7TypeName
178
+ ): JSONSchema7TypeName | undefined {
179
+ if (a === b) {
180
+ return a;
181
+ }
182
+ switch (a) {
183
+ case "number": {
184
+ if (b === "integer") {
185
+ return "integer";
186
+ }
187
+ }
188
+ // eslint-disable-next-line no-fallthrough
189
+ case "integer": {
190
+ if (b === "number") {
191
+ return "integer";
192
+ }
193
+ }
194
+ // eslint-disable-next-line no-fallthrough
195
+ default:
196
+ return undefined;
197
+ }
198
+ }
199
+
200
+ /**
201
+ * A merger function combines two values for a specific JSON Schema keyword.
202
+ */
203
+ export type Merger<T> = (a: T, b: T) => T;
204
+
205
+ /**
206
+ * A validation function that ensures consistency between two schema keywords.
207
+ */
208
+ export type Check<K extends SchemaKey> = (
209
+ target: Required<Pick<JSONSchema7, K>>
210
+ ) => boolean;
211
+
212
+ export type CheckEntry<A extends SchemaKey, B extends SchemaKey> = readonly [
213
+ A,
214
+ B,
215
+ Check<A | B>,
216
+ ];
217
+
218
+ export function check<A extends SchemaKey, B extends SchemaKey>(
219
+ a: A,
220
+ b: B,
221
+ check: Check<A | B>
222
+ ): CheckEntry<A, B> {
223
+ return [a, b, check];
224
+ }
225
+
226
+ function createChecksMap(checks: Iterable<CheckEntry<SchemaKey, SchemaKey>>) {
227
+ const map = new Map<
228
+ SchemaKey,
229
+ { oppositeKey: SchemaKey; check: (target: JSONSchema7) => void }[]
230
+ >();
231
+ for (const [a, b, check] of checks) {
232
+ const fn = (target: JSONSchema7) => {
233
+ if (!check(target as Required<JSONSchema7>)) {
234
+ throw new Error(
235
+ `Schema keys '${a}' and '${b}' are conflicting (${a}: ${JSON.stringify(target[a])}, ${b}: ${JSON.stringify(target[b])})`
236
+ );
237
+ }
238
+ };
239
+ for (const k of [
240
+ [a, b],
241
+ [b, a],
242
+ ]) {
243
+ let arr = map.get(k[0]);
244
+ if (arr === undefined) {
245
+ arr = [];
246
+ map.set(k[0], arr);
247
+ }
248
+ arr.push({ oppositeKey: k[1], check: fn });
249
+ }
250
+ }
251
+ return map;
252
+ }
253
+
254
+ export interface MergeOptions {
255
+ /**
256
+ * Custom function to test whether a regular expression `subExpr`
257
+ * is considered a subset of another `superExpr`.
258
+ * @default Object.is
259
+ */
260
+ isSubRegExp?: (subExpr: string, superExpr: string) => boolean;
261
+
262
+ /**
263
+ * Merger function for combining regular expression patterns
264
+ * @default simplePatternsMerger
265
+ */
266
+ mergePatterns?: Merger<string>;
267
+
268
+ /**
269
+ * Intersector function for merging JSON values (enum keyword)
270
+ * @default intersection
271
+ */
272
+ intersectJson?: Intersector<JSONSchema7Type>;
273
+
274
+ /**
275
+ * Deduplication strategy for JSON Schema definitions.
276
+ * @default identity
277
+ */
278
+ deduplicateJsonSchemaDef?: Deduplicator<JSONSchema7Definition>;
279
+
280
+ /**
281
+ * Fallback merger applied when no keyword-specific merger is defined.
282
+ * @default identity
283
+ */
284
+ defaultMerger?: Merger<any>;
285
+
286
+ /**
287
+ * A mapping of schema keywords to merger functions.
288
+ *
289
+ * - A merger operates on **values of a single keyword** (`a`, `b` → merged value).
290
+ * - When provided, a custom merger **overrides the default merger** for that keyword.
291
+ */
292
+ mergers?: Partial<{
293
+ [K in SchemaKey]: Merger<Exclude<JSONSchema7[K], undefined>>;
294
+ }>;
295
+
296
+ /**
297
+ * A collection of keyword groups with associated assigner functions.
298
+ *
299
+ * - An assigner operates at the **schema-object level** (`target`, `left`, `right`).
300
+ * - Custom assigners are **appended** to the default assigners,
301
+ * but can also **replace behavior** for specific keywords if they overlap.
302
+ */
303
+ assigners?: Iterable<[SchemaKey[], Assigner<JSONSchema7>]>;
304
+
305
+ /**
306
+ * Consistency checks to validate relationships between
307
+ * pairs of schema keywords (e.g. `minimum` ≤ `maximum`).
308
+ *
309
+ * - A check ensures that two related keywords do not conflict.
310
+ * - Providing this option **replaces the default checks** completely.
311
+ *
312
+ * @default DEFAULT_CHECKS
313
+ */
314
+ checks?: Iterable<CheckEntry<SchemaKey, SchemaKey>>;
315
+ }
316
+
317
+ export const DEFAULT_CHECKS = [
318
+ check("minimum", "maximum", (t) => t.maximum >= t.minimum),
319
+ check("exclusiveMinimum", "maximum", (t) => t.maximum > t.exclusiveMinimum),
320
+ check("minimum", "exclusiveMaximum", (t) => t.exclusiveMaximum > t.minimum),
321
+ check(
322
+ "exclusiveMinimum",
323
+ "exclusiveMaximum",
324
+ (t) => t.exclusiveMaximum > t.exclusiveMinimum
325
+ ),
326
+ check("minLength", "maxLength", (t) => t.maxLength >= t.minLength),
327
+ check("minItems", "maxItems", (t) => t.maxItems >= t.minItems),
328
+ check(
329
+ "minProperties",
330
+ "maxProperties",
331
+ (t) => t.maxProperties >= t.minProperties
332
+ ),
333
+ ];
334
+
335
+ export function createMerger({
336
+ mergePatterns = simplePatternsMerger,
337
+ isSubRegExp = Object.is,
338
+ intersectJson = intersection,
339
+ deduplicateJsonSchemaDef = identity,
340
+ defaultMerger = identity,
341
+ assigners = [],
342
+ checks = DEFAULT_CHECKS,
343
+ mergers,
344
+ }: MergeOptions = {}) {
345
+ function mergeArrayOfSchemaDefinitions(
346
+ schemas: JSONSchema7Definition[]
347
+ ): JSONSchema7Definition {
348
+ const l = schemas.length;
349
+ let result = schemas[0]!;
350
+ for (let i = 1; i < l; i++) {
351
+ const r = mergeSchemaDefinitions(result, schemas[i]!);
352
+ if (r === false) {
353
+ return false;
354
+ }
355
+ if (isAllowAnySchema(r)) {
356
+ continue;
357
+ }
358
+ result = r;
359
+ }
360
+ return result;
361
+ }
362
+
363
+ function createProperty(
364
+ constraints: (JSONSchema7 | true)[],
365
+ key: string,
366
+ value: JSONSchema7Definition,
367
+ patterns: CompiledPattern[],
368
+ oppositeValue: JSONSchema7Definition | undefined,
369
+ oppositePatterns: CompiledPattern[],
370
+ oppositeAdditional: JSONSchema7 | false | undefined
371
+ ): JSONSchema7Definition | undefined {
372
+ constraints.length = 0;
373
+ if (value === false) {
374
+ return false;
375
+ }
376
+ constraints.push(value);
377
+ const isOppositeValueDefined = oppositeValue !== undefined;
378
+ if (isOppositeValueDefined) {
379
+ if (oppositeValue === false) {
380
+ return false;
381
+ }
382
+ constraints.push(oppositeValue);
383
+ }
384
+ if (appendKeyConstraints(constraints, key, oppositePatterns)) {
385
+ return false;
386
+ }
387
+ const isNotYetAllowed = constraints.length < 2;
388
+ if (oppositeAdditional === false) {
389
+ // There are no allowing constraints from opposite side -> drop property
390
+ if (isNotYetAllowed) {
391
+ return undefined;
392
+ }
393
+ // Applying patterns of current schema cause they may disappear
394
+ if (appendKeyConstraints(constraints, key, patterns)) {
395
+ return false;
396
+ }
397
+ } else if (isNotYetAllowed && oppositeAdditional !== undefined) {
398
+ constraints.push(oppositeAdditional);
399
+ }
400
+ const l = constraints.length;
401
+ if (l === 1) {
402
+ return constraints[0];
403
+ }
404
+ return mergeArrayOfSchemaDefinitions(constraints);
405
+ }
406
+
407
+ function assignPatternPropertiesAndAdditionalPropertiesMerge(
408
+ target: Record<string, JSONSchema7Definition>,
409
+ patterns: Record<string, JSONSchema7Definition> | undefined,
410
+ patternKeys: string[],
411
+ matchedPatterns: Set<string>,
412
+ oppositeAdditional: JSONSchema7Definition,
413
+ isOppositeTruthy: boolean
414
+ ) {
415
+ const l = patternKeys.length;
416
+ if (l > 0 && oppositeAdditional !== false) {
417
+ if (isOppositeTruthy) {
418
+ // TODO: in some cases we can just assign new value instead of copying
419
+ Object.assign(target, patterns);
420
+ } else {
421
+ for (let i = 0; i < l; i++) {
422
+ const pattern = patternKeys[i]!;
423
+ if (matchedPatterns.has(pattern)) {
424
+ continue;
425
+ }
426
+ target[pattern] = mergeSchemaDefinitions(
427
+ patterns![pattern]!,
428
+ oppositeAdditional
429
+ );
430
+ }
431
+ }
432
+ }
433
+ return target;
434
+ }
435
+
436
+ const propertiesAssigner: Assigner<JSONSchema7> = (
437
+ target,
438
+ {
439
+ properties: lProps = {},
440
+ patternProperties: lPatterns,
441
+ additionalProperties: lAdditional = true,
442
+ },
443
+ {
444
+ properties: rProps = {},
445
+ patternProperties: rPatterns,
446
+ additionalProperties: rAdditional = true,
447
+ }
448
+ ) => {
449
+ // Special case
450
+ const isLAddTruthy = isAllowAnySchema(lAdditional);
451
+ const isRAddTruthy = isAllowAnySchema(rAdditional);
452
+ if (isLAddTruthy && isRAddTruthy) {
453
+ assignSchemaDefinitionOrRecordOfSchemaDefinitions(
454
+ target,
455
+ "properties",
456
+ mergeRecordsOfSchemaDefinitions(lProps, rProps)
457
+ );
458
+ assignSchemaDefinitionOrRecordOfSchemaDefinitions(
459
+ target,
460
+ "patternProperties",
461
+ lPatterns && rPatterns
462
+ ? mergeRecordsOfSchemaDefinitions(lPatterns, rPatterns)
463
+ : (lPatterns ?? rPatterns)
464
+ );
465
+ delete target.additionalProperties;
466
+ return target;
467
+ }
468
+ // Additional Properties
469
+ const additionalProperties = mergeSchemaDefinitions(
470
+ lAdditional,
471
+ rAdditional
472
+ );
473
+ assignSchemaDefinitionOrRecordOfSchemaDefinitions(
474
+ target,
475
+ "additionalProperties",
476
+ additionalProperties
477
+ );
478
+ // Properties
479
+ const properties: Record<string, JSONSchema7Definition> = {};
480
+ const lKeys = Object.keys(lProps);
481
+ const lKeysLen = lKeys.length;
482
+ const [lCompiledPatterns, lPatternKeys] = lPatterns
483
+ ? compilePatterns(lPatterns)
484
+ : EMPTY_PATTERNS_AND_KEYS;
485
+ const [rCompiledPatterns, rPatternKeys] = rPatterns
486
+ ? compilePatterns(rPatterns)
487
+ : EMPTY_PATTERNS_AND_KEYS;
488
+ const constraints: (JSONSchema7 | true)[] = [];
489
+ const lKeysSet = new Set<string>();
490
+ const mappedRAdditional = isRAddTruthy ? undefined : rAdditional;
491
+ for (let i = 0; i < lKeysLen; i++) {
492
+ const key = lKeys[i]!;
493
+ lKeysSet.add(key);
494
+ const prop = createProperty(
495
+ constraints,
496
+ key,
497
+ lProps[key]!,
498
+ lCompiledPatterns,
499
+ rProps[key],
500
+ rCompiledPatterns,
501
+ mappedRAdditional
502
+ );
503
+ if (prop !== undefined) {
504
+ properties[key] = prop;
505
+ }
506
+ }
507
+ const rKeys = Object.keys(rProps);
508
+ const rKeysLen = rKeys.length;
509
+ const mappedLAdditional = isLAddTruthy ? undefined : lAdditional;
510
+ for (let i = 0; i < rKeysLen; i++) {
511
+ const key = rKeys[i]!;
512
+ if (lKeysSet.has(key)) {
513
+ continue;
514
+ }
515
+ const prop = createProperty(
516
+ constraints,
517
+ key,
518
+ rProps[key]!,
519
+ rCompiledPatterns,
520
+ undefined,
521
+ lCompiledPatterns,
522
+ mappedLAdditional
523
+ );
524
+ if (prop !== undefined) {
525
+ properties[key] = prop;
526
+ }
527
+ }
528
+ assignSchemaDefinitionOrRecordOfSchemaDefinitions(
529
+ target,
530
+ "properties",
531
+ properties
532
+ );
533
+ // Pattern Properties
534
+ // (lPatterns and rPatterns) or (lPatterns and rAdditional) or (rPatterns and lAdditional)
535
+ let patterns: Record<string, JSONSchema7Definition> = {};
536
+ const matchedPatterns = new Set<string>();
537
+ if (lPatternKeys.length > 0 && rPatternKeys.length > 0) {
538
+ createPairCombinations(lPatternKeys, rPatternKeys, (lKey, rKey) => {
539
+ if (isSubRegExp(lKey, rKey)) {
540
+ matchedPatterns.add(lKey);
541
+ }
542
+ if (isSubRegExp(rKey, lKey)) {
543
+ matchedPatterns.add(rKey);
544
+ }
545
+ patterns[mergePatterns(lKey, rKey)] = mergeSchemaDefinitions(
546
+ lPatterns![lKey]!,
547
+ rPatterns![rKey]!
548
+ );
549
+ });
550
+ }
551
+ patterns = assignPatternPropertiesAndAdditionalPropertiesMerge(
552
+ patterns,
553
+ lPatterns,
554
+ lPatternKeys,
555
+ matchedPatterns,
556
+ rAdditional,
557
+ isRAddTruthy
558
+ );
559
+ patterns = assignPatternPropertiesAndAdditionalPropertiesMerge(
560
+ patterns,
561
+ rPatterns,
562
+ rPatternKeys,
563
+ matchedPatterns,
564
+ lAdditional,
565
+ isLAddTruthy
566
+ );
567
+ assignSchemaDefinitionOrRecordOfSchemaDefinitions(
568
+ target,
569
+ "patternProperties",
570
+ patterns
571
+ );
572
+ return target;
573
+ };
574
+
575
+ const itemsAssigner: Assigner<JSONSchema7> = (
576
+ target,
577
+ // NOTE: Schema that has `additionalItems` without an `items` keyword is invalid
578
+ // so the assigner should be triggered only be colliding `items` properties
579
+ // so default values are used only for type narrowing
580
+ { items: lItems = [], additionalItems: lAdditional },
581
+ { items: rItems = [], additionalItems: rAdditional }
582
+ ) => {
583
+ const isLArr = Array.isArray(lItems);
584
+ const isRArr = Array.isArray(rItems);
585
+ const itemsArray: JSONSchema7Definition[] = [];
586
+ target.items = itemsArray;
587
+ if (isLArr && isRArr) {
588
+ const [l, additional, tail] =
589
+ lItems.length < rItems.length
590
+ ? [lItems.length, lAdditional, rItems]
591
+ : [rItems.length, rAdditional, lItems];
592
+ let i = 0;
593
+ for (; i < l; i++) {
594
+ itemsArray.push(mergeSchemaDefinitions(lItems[i]!, rItems[i]!));
595
+ }
596
+ if (additional === false) {
597
+ target.additionalItems = false;
598
+ } else {
599
+ const isAdditionalTruthy =
600
+ additional === undefined || isAllowAnySchema(additional);
601
+ for (; i < tail.length; i++) {
602
+ itemsArray.push(
603
+ isAdditionalTruthy
604
+ ? tail[i]!
605
+ : mergeSchemaDefinitions(tail[i]!, additional)
606
+ );
607
+ }
608
+ assignSchemaDefinitionOrRecordOfSchemaDefinitions(
609
+ target,
610
+ "additionalItems",
611
+ lAdditional !== undefined && rAdditional !== undefined
612
+ ? mergeSchemaDefinitions(lAdditional, rAdditional)
613
+ : (lAdditional ?? rAdditional)
614
+ );
615
+ }
616
+ } else if (isLArr || isRArr) {
617
+ const [arr, item, additional] = (
618
+ isLArr ? [lItems, rItems, lAdditional] : [rItems, lItems, rAdditional]
619
+ ) as [
620
+ JSONSchema7Definition[],
621
+ JSONSchema7Definition,
622
+ JSONSchema7Definition | undefined,
623
+ ];
624
+ assignSchemaDefinitionOrRecordOfSchemaDefinitions(
625
+ target,
626
+ "additionalItems",
627
+ additional && mergeSchemaDefinitions(additional, item)
628
+ );
629
+ for (let i = 0; i < arr.length; i++) {
630
+ itemsArray.push(mergeSchemaDefinitions(arr[i]!, item));
631
+ }
632
+ } else {
633
+ delete target.additionalItems;
634
+ target.items = mergeSchemaDefinitions(lItems, rItems);
635
+ }
636
+ return target;
637
+ };
638
+
639
+ const conditionAssigner: Assigner<JSONSchema7> = (target, l, r) => {
640
+ assignCondition(target, l);
641
+ const cond = assignCondition({}, r);
642
+ if (target.allOf === undefined) {
643
+ target.allOf = [cond];
644
+ } else {
645
+ target.allOf = target.allOf.concat(cond);
646
+ }
647
+ return target;
648
+ };
649
+
650
+ function mergeArraysOfSchemaDefinition(
651
+ l: JSONSchema7Definition[],
652
+ r: JSONSchema7Definition[]
653
+ ) {
654
+ const definitions: JSONSchema7Definition[] = [];
655
+ createPairCombinations(l, r, (a, b) => {
656
+ try {
657
+ definitions.push(mergeSchemaDefinitions(a, b));
658
+ } catch {}
659
+ });
660
+ if (definitions.length === 0) {
661
+ throw new Error(
662
+ `No valid schema combinations could be produced for "${JSON.stringify(l)}" and "${JSON.stringify(r)}"; the merged result is empty`
663
+ );
664
+ }
665
+ return deduplicateJsonSchemaDef(definitions);
666
+ }
667
+
668
+ const ASSIGNERS_MAP = createMap([
669
+ [PROPERTIES_ASSIGNER_KEYS, propertiesAssigner],
670
+ [ITEMS_ASSIGNER_KEYS, itemsAssigner],
671
+ [CONDITION_ASSIGNER_KEYS, conditionAssigner],
672
+ ...assigners,
673
+ ]);
674
+
675
+ const CHECKS_MAP = createChecksMap(checks);
676
+
677
+ function mergeSchemaDefinitions(
678
+ left: JSONSchema7Definition,
679
+ right: JSONSchema7Definition
680
+ ) {
681
+ if (left === false || right === false) {
682
+ return false;
683
+ }
684
+ if (isAllowAnySchema(left)) {
685
+ if (isAllowAnySchema(right)) {
686
+ return true;
687
+ }
688
+ return right;
689
+ }
690
+ if (isAllowAnySchema(right)) {
691
+ return left;
692
+ }
693
+ let target = { ...left };
694
+ const assigners = new Set<Assigner<JSONSchema7>>();
695
+ const checks = new Set<(target: JSONSchema7) => void>();
696
+ const rKeys = Reflect.ownKeys(right) as SchemaKey[];
697
+ const l = rKeys.length;
698
+ for (let i = 0; i < l; i++) {
699
+ const rKey = rKeys[i]!;
700
+ const rv = right[rKey];
701
+ if (rv === undefined) {
702
+ continue;
703
+ }
704
+ const checkData = CHECKS_MAP.get(rKey);
705
+ if (checkData !== undefined) {
706
+ const l = checkData.length;
707
+ for (let j = 0; j < l; j++) {
708
+ const item = checkData[j];
709
+ if (left[item.oppositeKey] !== undefined) {
710
+ checks.add(item.check);
711
+ }
712
+ }
713
+ }
714
+ const lv = left[rKey];
715
+ if (lv === undefined) {
716
+ // @ts-expect-error too complex
717
+ target[rKey] = rv;
718
+ continue;
719
+ }
720
+ const assign = ASSIGNERS_MAP.get(rKey);
721
+ if (assign) {
722
+ assigners.add(assign);
723
+ continue;
724
+ }
725
+ const merge = MERGERS[rKey] ?? defaultMerger;
726
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
727
+ target[rKey] = merge(lv as never, rv as never);
728
+ }
729
+ for (const assign of assigners) {
730
+ target = assign(target, left, right);
731
+ }
732
+ for (const check of checks) {
733
+ check(target);
734
+ }
735
+ return target;
736
+ }
737
+
738
+ const mergeRecordsOfSchemaDefinitions = createRecordsMerge(
739
+ mergeSchemaDefinitions
740
+ );
741
+
742
+ const MERGERS: {
743
+ [K in SchemaKey]?: Merger<Exclude<JSONSchema7[K], undefined>>;
744
+ } = {
745
+ $id: defaultMerger,
746
+ $ref: defaultMerger,
747
+ $schema: defaultMerger,
748
+ $comment: defaultMerger,
749
+ $defs: mergeRecordsOfSchemaDefinitions,
750
+ definitions: mergeRecordsOfSchemaDefinitions,
751
+ type: (a, b) => {
752
+ if (a === b) {
753
+ return a;
754
+ }
755
+ const isAArr = Array.isArray(a);
756
+ const isBArr = Array.isArray(b);
757
+ if (!isAArr && !isBArr) {
758
+ const intersection = intersectSchemaTypes(a, b);
759
+ if (intersection !== undefined) {
760
+ return intersection;
761
+ }
762
+ } else if (isAArr || isBArr) {
763
+ const r = new Set<JSONSchema7TypeName>();
764
+ if (isAArr && isBArr) {
765
+ createPairCombinations(a, b, (x, y) => {
766
+ const type = intersectSchemaTypes(x, y);
767
+ if (type !== undefined) {
768
+ r.add(type);
769
+ }
770
+ });
771
+ } else {
772
+ const arr = (isAArr ? a : b) as JSONSchema7TypeName[];
773
+ const el = (isAArr ? b : a) as JSONSchema7TypeName;
774
+ const l = arr.length;
775
+ for (let i = 0; i < l; i++) {
776
+ const intersection = intersectSchemaTypes(el, arr[i]!);
777
+ if (intersection !== undefined) {
778
+ r.add(intersection);
779
+ }
780
+ }
781
+ }
782
+ const s = r.size;
783
+ if (s === 1) {
784
+ return r.values().next().value!;
785
+ }
786
+ if (s > 1) {
787
+ return Array.from(r);
788
+ }
789
+ }
790
+ throw new Error(
791
+ `It is not possible to create an intersection of the following incompatible types: ${a.toString()}, ${b.toString()}`
792
+ );
793
+ },
794
+ default: defaultMerger,
795
+ description: defaultMerger,
796
+ title: defaultMerger,
797
+ const: defaultMerger,
798
+ format: defaultMerger,
799
+ contentEncoding: defaultMerger,
800
+ contentMediaType: defaultMerger,
801
+ not: (a, b) => {
802
+ const items = deduplicateJsonSchemaDef([a, b]);
803
+ return items.length === 1 ? items[0]! : { anyOf: items };
804
+ },
805
+ pattern: mergePatterns,
806
+ readOnly: mergeBooleans,
807
+ writeOnly: mergeBooleans,
808
+ enum: (a, b) => {
809
+ const data = intersectJson(a, b);
810
+ if (data.length === 0) {
811
+ throw new Error(
812
+ `Intersection of the following enums is empty: "${JSON.stringify(
813
+ a
814
+ )}", "${JSON.stringify(b)}"`
815
+ );
816
+ }
817
+ return data;
818
+ },
819
+ anyOf: mergeArraysOfSchemaDefinition,
820
+ oneOf: mergeArraysOfSchemaDefinition,
821
+ allOf: (l, r) => deduplicateJsonSchemaDef(l.concat(r)),
822
+ propertyNames: mergeSchemaDefinitions,
823
+ contains: mergeSchemaDefinitions,
824
+ dependencies: createRecordsMerge((a, b) => {
825
+ if (Array.isArray(a)) {
826
+ if (Array.isArray(b)) {
827
+ return union(a, b);
828
+ }
829
+ return mergeSchemaDefinitions(b, { required: a });
830
+ }
831
+ if (Array.isArray(b)) {
832
+ return mergeSchemaDefinitions(a, { required: b });
833
+ }
834
+ return mergeSchemaDefinitions(a, b);
835
+ }),
836
+ examples: (l, r) => {
837
+ // https://datatracker.ietf.org/doc/html/draft-handrews-json-schema-validation-01#section-10.4
838
+ if (!Array.isArray(l) || !Array.isArray(r)) {
839
+ throw new Error(
840
+ `Value of the 'examples' field should be an array, but got "${JSON.stringify(
841
+ l
842
+ )}" and "${JSON.stringify(r)}"`
843
+ );
844
+ }
845
+ // TODO: Proper deduplication
846
+ return union(l, r);
847
+ },
848
+ multipleOf: (a, b) => {
849
+ let factor = 1;
850
+ while (!Number.isInteger(a) || !Number.isInteger(b)) {
851
+ factor *= 10;
852
+ a *= 10;
853
+ b *= 10;
854
+ }
855
+ return lcm(a, b) / factor;
856
+ },
857
+ exclusiveMaximum: Math.min,
858
+ maximum: Math.min,
859
+ maxItems: Math.min,
860
+ maxLength: Math.min,
861
+ maxProperties: Math.min,
862
+ exclusiveMinimum: Math.max,
863
+ minimum: Math.max,
864
+ minItems: Math.max,
865
+ minLength: Math.max,
866
+ minProperties: Math.max,
867
+ uniqueItems: mergeBooleans,
868
+ required: union,
869
+ ...mergers,
870
+ } satisfies {
871
+ [K in Exclude<SchemaKey, AssignerKey>]-?: Merger<
872
+ Exclude<JSONSchema7[K], undefined>
873
+ >;
874
+ };
875
+
876
+ return {
877
+ mergeSchemaDefinitions,
878
+ mergeArrayOfSchemaDefinitions,
879
+ };
880
+ }