Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/quicktype-core/src/Run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,7 @@ class Run implements RunContext {
graph,
stringTypeMapping,
conflateNumbers,
targetLanguage.supportsUnionsWithMultipleObjectTypes,
true,
debugPrintReconstitution,
);
Expand Down Expand Up @@ -371,6 +372,7 @@ class Run implements RunContext {
graph,
stringTypeMapping,
conflateNumbers,
targetLanguage.supportsUnionsWithMultipleObjectTypes,
false,
debugPrintReconstitution,
);
Expand Down Expand Up @@ -435,6 +437,7 @@ class Run implements RunContext {
graph,
stringTypeMapping,
conflateNumbers,
targetLanguage.supportsUnionsWithMultipleObjectTypes,
false,
debugPrintReconstitution,
);
Expand Down Expand Up @@ -484,6 +487,7 @@ class Run implements RunContext {
graph,
stringTypeMapping,
conflateNumbers,
targetLanguage.supportsUnionsWithMultipleObjectTypes,
false,
debugPrintReconstitution,
);
Expand Down
4 changes: 4 additions & 0 deletions packages/quicktype-core/src/TargetLanguage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,10 @@ export abstract class TargetLanguage<
return false;
}

public get supportsUnionsWithMultipleObjectTypes(): boolean {
return false;
}

public get supportsFullObjectType(): boolean {
return false;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,10 @@ export class TypeScriptTargetLanguage extends TargetLanguage<
return true;
}

public get supportsUnionsWithMultipleObjectTypes(): boolean {
return true;
}

public get supportsFullObjectType(): boolean {
return true;
}
Expand Down
44 changes: 41 additions & 3 deletions packages/quicktype-core/src/rewrites/FlattenUnions.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { iterableSome, setFilter } from "collection-utils";
import { iterableEvery, iterableSome, setFilter } from "collection-utils";

import { emptyTypeAttributes } from "../attributes/TypeAttributes.js";
import type { GraphRewriteBuilder } from "../GraphRewriting.js";
Expand All @@ -8,13 +8,17 @@ import { IntersectionType, type Type, UnionType } from "../Type/Type.js";
import type { StringTypeMapping } from "../Type/TypeBuilderUtils.js";
import type { TypeGraph } from "../Type/TypeGraph.js";
import { type TypeRef, derefTypeRef, typeRefIndex } from "../Type/TypeRef.js";
import { makeGroupsToFlatten } from "../Type/TypeUtils.js";
import {
makeGroupsToFlatten,
setOperationMembersRecursively,
} from "../Type/TypeUtils.js";
import { UnifyUnionBuilder, unifyTypes } from "../UnifyClasses.js";

export function flattenUnions(
graph: TypeGraph,
stringTypeMapping: StringTypeMapping,
conflateNumbers: boolean,
supportsUnionsWithMultipleObjectTypes: boolean,
makeObjectTypes: boolean,
debugPrintReconstitution: boolean,
): [TypeGraph, boolean] {
Expand Down Expand Up @@ -157,8 +161,42 @@ export function flattenUnions(
(t) => t instanceof UnionType,
) as Set<UnionType>;
const nonCanonicalUnions = setFilter(allUnions, (u) => !u.isCanonical);
// IntersectionAccumulator can only represent one member of each kind, so
// object unions that participate in intersections still need flattening.
const unionsInIntersections = new Set<UnionType>();
if (supportsUnionsWithMultipleObjectTypes) {
for (const t of graph.allTypesUnordered()) {
if (!(t instanceof IntersectionType)) continue;
for (const member of setOperationMembersRecursively(
t,
undefined,
)[0]) {
if (member instanceof UnionType) {
unionsInIntersections.add(member);
}
}
}
}

const unionsToFlatten = setFilter(nonCanonicalUnions, (u) => {
if (
!supportsUnionsWithMultipleObjectTypes ||
unionsInIntersections.has(u)
) {
return true;
}

const members = setOperationMembersRecursively(u, undefined)[0];
return (
members.size <= 1 ||
!iterableEvery(
members,
(m) => m.kind === "class" || m.kind === "object",
)
);
});
let foundIntersection = false;
const groups = makeGroupsToFlatten(nonCanonicalUnions, (members) => {
const groups = makeGroupsToFlatten(unionsToFlatten, (members) => {
messageAssert(members.size > 0, "IRNoEmptyUnions", {});
if (!iterableSome(members, (m) => m instanceof IntersectionType))
return true;
Expand Down
6 changes: 6 additions & 0 deletions test/inputs/schema/anyof-object-union.1.fail.class-union.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"firstProperty": {
"a": "alpha",
"d": "delta"
}
}
12 changes: 12 additions & 0 deletions test/inputs/schema/anyof-object-union.1.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"firstProperty": {
"a": "alpha",
"b": 1,
"c": true
},
"secondProperty": {
"d": "delta",
"e": 2,
"f": false
}
}
44 changes: 44 additions & 0 deletions test/inputs/schema/anyof-object-union.schema
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
{
"$ref": "#/definitions/TestObject",
"definitions": {
"TestObject": {
"type": "object",
"additionalProperties": false,
"properties": {
"firstProperty": {
"anyOf": [
{ "$ref": "#/definitions/MyObjectA" },
{ "$ref": "#/definitions/MyObjectB" }
]
},
"secondProperty": {
"anyOf": [
{
"type": "array",
"items": { "$ref": "#/definitions/MyObjectA" }
},
{ "$ref": "#/definitions/MyObjectB" }
]
}
}
},
"MyObjectA": {
"type": "object",
"additionalProperties": false,
"properties": {
"a": { "type": "string" },
"b": { "type": "number" },
"c": { "type": "boolean" }
}
},
"MyObjectB": {
"type": "object",
"additionalProperties": false,
"properties": {
"d": { "type": "string" },
"e": { "type": "number" },
"f": { "type": "boolean" }
}
}
}
}
18 changes: 17 additions & 1 deletion test/languages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ const skipsArrayElementValidation = ["issue2680-top-level-array.schema"];
export type LanguageFeature =
| "enum"
| "union"
| "class-union"
| "no-defaults"
| "strict-optional"
| "date-time"
Expand Down Expand Up @@ -1115,7 +1116,14 @@ export const TypeScriptLanguage: Language = {
"e8b04.json",
],
allowMissingNull: false,
features: ["enum", "union", "no-defaults", "strict-optional", "date-time"],
features: [
"enum",
"union",
"class-union",
"no-defaults",
"strict-optional",
"date-time",
],
output: "TopLevel.ts",
topLevel: "TopLevel",
skipJSON: [],
Expand Down Expand Up @@ -1463,6 +1471,9 @@ export const KotlinLanguage: Language = {
"top-level-enum.schema",
"top-level-primitive.schema",
"recursive-union-flattening.schema",
// Pre-existing Klaxon constructor-matching limitation for merged
// multi-object unions, unrelated to the TypeScript-only union change.
"anyof-object-union.schema",
// A top-level array is deserialized without enforcing its element
// type, so a mistyped element round-trips instead of failing.
...skipsArrayElementValidation,
Expand Down Expand Up @@ -1677,6 +1688,11 @@ export const KotlinXLanguage: Language = {
// deserialization fails at runtime (documented TODO in
// KotlinXRenderer.ts).
"accessors.schema",
// secondProperty is an array|object union, which the kotlinx
// renderer emits as a sealed class without serializer wiring, so it
// fails to deserialize at runtime (documented TODO in
// KotlinXRenderer.ts).
"anyof-object-union.schema",
"bool-string.schema",
"class-map-union.schema",
"class-with-additional.schema",
Expand Down
Loading