From f13b0ff2cd7a48c08f4c3df8ff119a5de2b3d97f Mon Sep 17 00:00:00 2001 From: EloB <476567+EloB@users.noreply.github.com> Date: Sun, 15 Nov 2026 07:51:25 +0100 Subject: [PATCH 1/2] test: cover an enum with qt-accessors that is also in a union `accessors-enum-union.schema` has the same enum twice, as a definition with `qt-accessors` and inline in a nullable union, which is what the TypeScript input makes of `f: E | null` for a string enum. A unit test covers two definitions of the same enum with `qt-enum-values`. Co-Authored-By: Claude Opus 5.5 --- .../inputs/schema/accessors-enum-union.1.json | 4 ++ .../inputs/schema/accessors-enum-union.2.json | 4 ++ .../accessors-enum-union.3.fail.enum.json | 4 ++ .../inputs/schema/accessors-enum-union.schema | 21 ++++++++ test/unit/enum-values-combine.test.ts | 48 +++++++++++++++++++ 5 files changed, 81 insertions(+) create mode 100644 test/inputs/schema/accessors-enum-union.1.json create mode 100644 test/inputs/schema/accessors-enum-union.2.json create mode 100644 test/inputs/schema/accessors-enum-union.3.fail.enum.json create mode 100644 test/inputs/schema/accessors-enum-union.schema create mode 100644 test/unit/enum-values-combine.test.ts diff --git a/test/inputs/schema/accessors-enum-union.1.json b/test/inputs/schema/accessors-enum-union.1.json new file mode 100644 index 000000000..9699165a4 --- /dev/null +++ b/test/inputs/schema/accessors-enum-union.1.json @@ -0,0 +1,4 @@ +{ + "f": "a", + "g": "b" +} diff --git a/test/inputs/schema/accessors-enum-union.2.json b/test/inputs/schema/accessors-enum-union.2.json new file mode 100644 index 000000000..c335be1f1 --- /dev/null +++ b/test/inputs/schema/accessors-enum-union.2.json @@ -0,0 +1,4 @@ +{ + "f": null, + "g": "a" +} diff --git a/test/inputs/schema/accessors-enum-union.3.fail.enum.json b/test/inputs/schema/accessors-enum-union.3.fail.enum.json new file mode 100644 index 000000000..c19c5e361 --- /dev/null +++ b/test/inputs/schema/accessors-enum-union.3.fail.enum.json @@ -0,0 +1,4 @@ +{ + "f": "c", + "g": "a" +} diff --git a/test/inputs/schema/accessors-enum-union.schema b/test/inputs/schema/accessors-enum-union.schema new file mode 100644 index 000000000..7f8aa0e6c --- /dev/null +++ b/test/inputs/schema/accessors-enum-union.schema @@ -0,0 +1,21 @@ +{ + "type": "object", + "properties": { + "f": { + "anyOf": [ + { "type": "string", "enum": ["a", "b"] }, + { "type": "null" } + ] + }, + "g": { "$ref": "#/definitions/E" } + }, + "required": ["f", "g"], + "additionalProperties": false, + "definitions": { + "E": { + "type": "string", + "enum": ["a", "b"], + "qt-accessors": { "a": "Alpha", "b": "Beta" } + } + } +} diff --git a/test/unit/enum-values-combine.test.ts b/test/unit/enum-values-combine.test.ts new file mode 100644 index 000000000..f8124dda7 --- /dev/null +++ b/test/unit/enum-values-combine.test.ts @@ -0,0 +1,48 @@ +import { InputData, JSONSchemaInput, quicktype } from "quicktype-core"; +import { describe, expect, test } from "vitest"; + +// Two definitions of the same enum are one type, so their `qt-enum-values` +// are combined. +async function cJSONOutput(fValues: Record): Promise { + const schema = JSON.stringify({ + definitions: { + E: { + type: "string", + enum: ["a", "b"], + "qt-enum-values": { a: "0", b: "1" }, + }, + F: { + type: "string", + enum: ["a", "b"], + "qt-enum-values": fValues, + }, + }, + }); + const schemaInput = new JSONSchemaInput(undefined); + for (const name of ["E", "F"]) { + await schemaInput.addSource({ + name, + schema, + uris: [`#/definitions/${name}`], + }); + } + const inputData = new InputData(); + inputData.addInput(schemaInput); + + const result = await quicktype({ inputData, lang: "cjson" }); + return result.lines.join("\n"); +} + +describe("enum values of the same enum", () => { + test("combine when every copy is the same", async () => { + const output = await cJSONOutput({ b: "1", a: "0" }); + expect(output).toContain("_A = 0,"); + expect(output).toContain("_B = 1,"); + }); + + test("still fail when the copies differ", async () => { + await expect(cJSONOutput({ a: "0", b: "2" })).rejects.toThrow( + "Cannot combine type attribute enumValues", + ); + }); +}); From 9a62b31da4fd94378faa940bfa2ac7eea46ff6ce Mon Sep 17 00:00:00 2001 From: EloB <476567+EloB@users.noreply.github.com> Date: Sun, 15 Nov 2026 07:51:25 +0100 Subject: [PATCH 2/2] fix: combine accessor names and identical enum values `AccessorNamesTypeAttributeKind` and `EnumValuesTypeAttributeKind` had no `combine`, so two types with accessor names (or enum values) that turned out to be the same type stopped with "Internal error: Cannot combine type attribute accessorNames." The TypeScript input gives a string enum's definition its member names as `qt-accessors`, so string enums in unions (`E | null`, `E[] | F`, an optional `E` in Python) and two enums with the same values failed, as did JSON Schema definitions of the same enum with `qt-accessors`. Accessor names now keep the entries every copy agrees on, and drop a name the copies disagree on, so it falls back to its default. Enum values combine only when every copy is the same; any other combination fails as before, because dropping a value could give two cases the same value. Co-Authored-By: Claude Opus 5.5 --- .../src/attributes/AccessorNames.ts | 28 +++++++++++++++++++ .../src/attributes/EnumValues.ts | 10 ++++++- 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/packages/quicktype-core/src/attributes/AccessorNames.ts b/packages/quicktype-core/src/attributes/AccessorNames.ts index 6b3283cf7..554e15727 100644 --- a/packages/quicktype-core/src/attributes/AccessorNames.ts +++ b/packages/quicktype-core/src/attributes/AccessorNames.ts @@ -1,4 +1,5 @@ import { + areEqual, iterableFirst, mapFromIterable, mapFromObject, @@ -28,11 +29,38 @@ export type AccessorEntry = string | Map; export type AccessorNames = Map; +// Combines the accessor names of types that turn out to be the same type, +// such as an enum and the same enum in a union, or two enums with the same +// cases: an entry they all agree on is kept; one they disagree on is +// dropped, so that name falls back to the default. +function combineAccessorNames(arr: AccessorNames[]): AccessorNames { + const result: AccessorNames = new Map(); + const conflicting = new Set(); + for (const accessors of arr) { + for (const [key, entry] of accessors) { + if (conflicting.has(key)) continue; + const existing = result.get(key); + if (existing === undefined) { + result.set(key, entry); + } else if (!areEqual(existing, entry)) { + result.delete(key); + conflicting.add(key); + } + } + } + + return result; +} + class AccessorNamesTypeAttributeKind extends TypeAttributeKind { public constructor() { super("accessorNames"); } + public combine(arr: AccessorNames[]): AccessorNames { + return combineAccessorNames(arr); + } + public makeInferred(_: AccessorNames): undefined { return undefined; } diff --git a/packages/quicktype-core/src/attributes/EnumValues.ts b/packages/quicktype-core/src/attributes/EnumValues.ts index 9cfa0678e..83f34deb0 100644 --- a/packages/quicktype-core/src/attributes/EnumValues.ts +++ b/packages/quicktype-core/src/attributes/EnumValues.ts @@ -1,4 +1,4 @@ -import { mapMap } from "collection-utils"; +import { areEqual, mapMap } from "collection-utils"; import type { JSONSchemaAttributes, @@ -20,6 +20,14 @@ class EnumValuesTypeAttributeKind extends TypeAttributeKind { super("enumValues"); } + // Copies of an enum's values that are all the same are kept; any other + // combination still fails, because dropping a value the copies disagree + // on could give two cases the same value. + public combine(arr: AccessorNames[]): AccessorNames | undefined { + if (arr.every((values) => areEqual(values, arr[0]))) return arr[0]; + return super.combine(arr); + } + public makeInferred(_: AccessorNames): undefined { return undefined; }