diff --git a/packages/quicktype-core/src/rewrites/FlattenUnions.ts b/packages/quicktype-core/src/rewrites/FlattenUnions.ts index 4357e7e7d2..aab4d85119 100644 --- a/packages/quicktype-core/src/rewrites/FlattenUnions.ts +++ b/packages/quicktype-core/src/rewrites/FlattenUnions.ts @@ -59,6 +59,16 @@ export function flattenUnions( .join(","); } + function containsIntersection(t: Type, seen: Set): boolean { + if (seen.has(t.index)) return false; + seen.add(t.index); + + if (t instanceof IntersectionType) return true; + if (!(t instanceof UnionType)) return false; + + return iterableSome(t.members, (m) => containsIntersection(m, seen)); + } + function replace( types: ReadonlySet, builder: GraphRewriteBuilder, @@ -86,7 +96,9 @@ export function flattenUnions( trefs.map((tref) => derefTypeRef(tref, graph)), ); if ( - iterableSome(typesToUnify, (t) => t instanceof IntersectionType) + iterableSome(typesToUnify, (t) => + containsIntersection(t, new Set()), + ) ) { trefs = trefs.map((tref) => builder.reconstituteType(derefTypeRef(tref, graph)), diff --git a/test/inputs/schema/nested-intersection-union.1.fail.union.json b/test/inputs/schema/nested-intersection-union.1.fail.union.json new file mode 100644 index 0000000000..cedeb3f00b --- /dev/null +++ b/test/inputs/schema/nested-intersection-union.1.fail.union.json @@ -0,0 +1,8 @@ +{ + "input": [ + { + "content": "valid item" + }, + ["this array is not a valid item"] + ] +} diff --git a/test/inputs/schema/nested-intersection-union.1.json b/test/inputs/schema/nested-intersection-union.1.json new file mode 100644 index 0000000000..3018c7a100 --- /dev/null +++ b/test/inputs/schema/nested-intersection-union.1.json @@ -0,0 +1,20 @@ +{ + "input": [ + { + "content": "hello", + "id": "item-1", + "output": "42", + "summary": "all fields present" + }, + { + "content": "just a message" + }, + { + "id": "item-2", + "output": "function result" + }, + { + "summary": "just reasoning" + } + ] +} diff --git a/test/inputs/schema/nested-intersection-union.schema b/test/inputs/schema/nested-intersection-union.schema new file mode 100644 index 0000000000..0453188e30 --- /dev/null +++ b/test/inputs/schema/nested-intersection-union.schema @@ -0,0 +1,58 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "properties": { + "input": { + "type": "array", + "items": { + "oneOf": [ + { "$ref": "#/definitions/Message" }, + { "$ref": "#/definitions/Item" } + ] + } + } + }, + "required": ["input"], + "definitions": { + "Message": { + "type": "object", + "properties": { + "content": { "type": "string" } + }, + "required": ["content"] + }, + "Item": { + "oneOf": [ + { "$ref": "#/definitions/FunctionOutput" }, + { "$ref": "#/definitions/ReasoningItem" } + ] + }, + "FunctionOutput": { + "allOf": [ + { "$ref": "#/definitions/BaseItem" }, + { + "type": "object", + "properties": { + "output": { "type": "string" } + }, + "required": ["output"] + } + ] + }, + "BaseItem": { + "type": "object", + "properties": { + "id": { "type": "string" } + }, + "required": ["id"] + }, + "ReasoningItem": { + "type": "object", + "properties": { + "summary": { "type": "string" } + }, + "required": ["summary"] + } + } +} diff --git a/test/languages.ts b/test/languages.ts index 5d971b30ca..503ea71c6d 100644 --- a/test/languages.ts +++ b/test/languages.ts @@ -545,6 +545,7 @@ export const CJSONLanguage: Language = { "class-with-additional.schema", "go-schema-pattern-properties.schema", "multi-type-enum.schema", + "nested-intersection-union.schema", /* Constraints (min/max and regex) are not supported (for the current implementation, can be added later, should abord parsing and return NULL) */ "minmaxlength.schema", "optional-const-ref.schema", @@ -615,6 +616,8 @@ export const CPlusPlusLanguage: Language = { skipSchema: [ // uses too much memory "keyword-unions.schema", + // The generated deserializer accepts non-object values when all class properties are optional. + "nested-intersection-union.schema", // Recursive top-level unions produce aliases that can refer to later aliases. "recursive-union-flattening.schema", ], @@ -1016,6 +1019,9 @@ I havea no idea how to encode these tests correctly. "integer-string.schema", "intersection.schema", ...skipsUntypedUnions, + // The test driver prints the circe DecodingFailure and exits 0, so + // expected-failure samples cannot be detected. + "nested-intersection-union.schema", "date-time-or-string.schema", "implicit-one-of.schema", "go-schema-pattern-properties.schema", @@ -1155,6 +1161,9 @@ export const KotlinLanguage: Language = { // which is not represented in the types (implicit-class-array-union); // class-map-union: KlaxonException: Couldn't find a suitable constructor for class UnionValue to initialize with {} ...skipsUntypedUnions, + // Deserializes an array where a union of two classes is expected + // instead of rejecting it. + "nested-intersection-union.schema", "class-with-additional.schema", "go-schema-pattern-properties.schema", // IllegalArgumentException @@ -1240,6 +1249,9 @@ export const KotlinJacksonLanguage: Language = { // which is not represented in the types (implicit-class-array-union); // class-map-union: KlaxonException: Couldn't find a suitable constructor for class UnionValue to initialize with {} ...skipsUntypedUnions, + // Deserializes an array where a union of two classes is expected + // instead of rejecting it. + "nested-intersection-union.schema", "class-with-additional.schema", "go-schema-pattern-properties.schema", // IllegalArgumentException @@ -1451,6 +1463,9 @@ export const HaskellLanguage: Language = { skipSchema: [ "any.schema", ...skipsUntypedUnions, + // The test driver encodes the Maybe result, so a failed decode prints + // "null" and exits 0 — expected-failure samples cannot be detected. + "nested-intersection-union.schema", "direct-union.schema", ...skipsEnumValueValidation, "go-schema-pattern-properties.schema",