Skip to content

Commit 9e8c27d

Browse files
committed
Merge compatible definitions in union types
Most validation keywords apply to only one of the basic types, so a "string" type and an "array" type can share a definition without colliding as a ["string", "array"] type, as long as they don't have any incompatibilities with each other. Modify UnionTypeFormatter to collapse these disjoint types into a single definition, without using anyOf.
1 parent 541871b commit 9e8c27d

File tree

2 files changed

+38
-0
lines changed

2 files changed

+38
-0
lines changed

src/TypeFormatter/UnionTypeFormatter.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { SubTypeFormatter } from "../SubTypeFormatter";
44
import { BaseType } from "../Type/BaseType";
55
import { UnionType } from "../Type/UnionType";
66
import { TypeFormatter } from "../TypeFormatter";
7+
import { mergeDefinitions } from "../Utils/mergeDefinitions";
78
import { uniqueArray } from "../Utils/uniqueArray";
89

910
export class UnionTypeFormatter implements SubTypeFormatter {
@@ -45,6 +46,18 @@ export class UnionTypeFormatter implements SubTypeFormatter {
4546
}
4647
}
4748

49+
for (let idx = 0; idx < flattenedDefinitions.length - 1; idx++) {
50+
for (let comp = idx + 1; comp < flattenedDefinitions.length; ) {
51+
const merged = mergeDefinitions(flattenedDefinitions[idx], flattenedDefinitions[comp]);
52+
if (merged) {
53+
flattenedDefinitions[idx] = merged;
54+
flattenedDefinitions.splice(comp, 1);
55+
} else {
56+
comp++;
57+
}
58+
}
59+
}
60+
4861
return flattenedDefinitions.length > 1
4962
? {
5063
anyOf: flattenedDefinitions,

src/Utils/mergeDefinitions.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { Definition } from "../Schema/Definition";
2+
import { uniqueArray } from "./uniqueArray";
3+
4+
/**
5+
* Attempt to merge two disjoint definitions into one. Definitions are disjoint
6+
* (and therefore mergeable) if all of the following are true:
7+
* 1) Each has a 'type' property, and they share no types in common,
8+
* 2) The cross-type validation properties 'enum' and 'const' are not on either definition, and
9+
* 3) The two definitions have no properties besides 'type' in common.
10+
*
11+
* Returns the merged definition, or null if the two defs were not disjoint.
12+
*/
13+
export function mergeDefinitions(def1: Definition, def2: Definition): Definition | null {
14+
const { type: type1, ...props1 } = def1;
15+
const { type: type2, ...props2 } = def2;
16+
const types = [type1!, type2!].flat();
17+
if (!type1 || !type2 || uniqueArray(types).length !== types.length) {
18+
return null;
19+
}
20+
const keys = [Object.keys(props1), Object.keys(props2)].flat();
21+
if (keys.includes("enum") || keys.includes("const") || uniqueArray(keys).length !== keys.length) {
22+
return null;
23+
}
24+
return { type: types, ...props1, ...props2 };
25+
}

0 commit comments

Comments
 (0)