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
41 changes: 39 additions & 2 deletions packages/quicktype-core/src/UnionBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,26 @@ function attributesForTypes(
traverse(t, rootPath, typesArray.length === 1);
}

// A nested homogeneous union next to null becomes one non-null type during
// unification, so its attributes belong on that type rather than on the
// enclosing nullable union.
const allDescendants = defined(typesForUnion.get(rootPath[0]));
const nullableUnionsCollapsedIntoType = new Set(
Array.from(unions).filter((u) => {
if (unionsEquivalentToRoot.has(u)) return false;
const descendants = defined(typesForUnion.get(u));
const otherDescendants = Array.from(allDescendants).filter(
(t) => !descendants.has(t),
);
return (
descendants.size > 1 &&
new Set(Array.from(descendants, (t) => t.kind)).size === 1 &&
otherDescendants.length > 0 &&
otherDescendants.every((t) => t.kind === "null")
);
}),
);

const resultAttributes = mapMap(unionsForType, (unionForType, t) => {
const singleAncestors = Array.from(unionForType).filter(
(u) => defined(typesForUnion.get(u)).size === 1,
Expand All @@ -416,14 +436,31 @@ function attributesForTypes(
const inheritedAttributes = singleAncestors.map((u) =>
u.getAttributes(),
);
return combineTypeAttributes(
let attributes = combineTypeAttributes(
"union",
[t.getAttributes()].concat(inheritedAttributes),
);
const collapsedAncestors = Array.from(unionForType).filter(
(u): u is UnionType =>
u instanceof UnionType &&
nullableUnionsCollapsedIntoType.has(u),
);
for (const u of collapsedAncestors.reverse()) {
attributes = combineTypeAttributes(
"union",
u.getAttributes(),
increaseTypeAttributesDistance(attributes),
);
}

return attributes;
});
const unionAttributes = Array.from(unions).map((u) => {
const t = typesForUnion.get(u);
if (t !== undefined && t.size === 1) {
if (
(t !== undefined && t.size === 1) ||
nullableUnionsCollapsedIntoType.has(u)
) {
return emptyTypeAttributes;
}

Expand Down
3 changes: 3 additions & 0 deletions test/inputs/schema/enum-of-enums-null.1.fail.enum.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"format": "xml"
}
3 changes: 3 additions & 0 deletions test/inputs/schema/enum-of-enums-null.1.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"format": "rdf_xml"
}
3 changes: 3 additions & 0 deletions test/inputs/schema/enum-of-enums-null.2.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"format": null
}
48 changes: 48 additions & 0 deletions test/inputs/schema/enum-of-enums-null.schema
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "ConstructResponse",
"type": "object",
"required": ["format"],
"properties": {
"format": {
"description": "The format of the data.",
"anyOf": [
{ "$ref": "#/definitions/DataFormat" },
{ "type": "null" }
]
}
},
"additionalProperties": false,
"definitions": {
"DataFormat": {
"title": "DataFormat",
"description": "The format of the data.",
"oneOf": [
{
"title": "Turtle",
"description": "Turtle format",
"type": "string",
"enum": ["turtle"]
},
{
"title": "RDF XML",
"description": "RDF XML format",
"type": "string",
"enum": ["rdf_xml"]
},
{
"title": "N-Triples",
"description": "N-Triples format",
"type": "string",
"enum": ["n_triples"]
},
{
"title": "N-Quads",
"description": "N-Quads format",
"type": "string",
"enum": ["n_quads"]
}
]
}
}
}
1 change: 1 addition & 0 deletions test/languages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const skipsEnumValueValidation = [
"enum-large.schema",
"optional-enum.schema",
"const-non-string.schema",
"enum-of-enums-null.schema",
"haskell-enum-forbidden.schema",
"nullable-optional-one-of.schema",
"all-of-additional-properties-false.schema",
Expand Down
55 changes: 55 additions & 0 deletions test/unit/enum-of-enums-null-name.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import {
InputData,
JSONSchemaInput,
quicktype,
} from "../../packages/quicktype-core/src/index.js";
import { expect, test } from "vitest";

const schema = JSON.stringify({
$schema: "http://json-schema.org/draft-07/schema#",
type: "object",
required: ["format"],
properties: {
format: {
anyOf: [{ $ref: "#/definitions/DataFormat" }, { type: "null" }],
},
},
definitions: {
DataFormat: {
title: "DataFormat",
oneOf: [
{ title: "Turtle", type: "string", enum: ["turtle"] },
{ title: "RDF XML", type: "string", enum: ["rdf_xml"] },
{
title: "N-Triples",
type: "string",
enum: ["n_triples"],
},
{ title: "N-Quads", type: "string", enum: ["n_quads"] },
],
},
},
});

// The fixture harness verifies round-tripping, but generated identifiers are
// self-consistent even when the wrong title is chosen. Assert the emitted name
// directly so the outer oneOf title cannot regress to the first variant title.
test("uses the outer oneOf title for a nullable enum-of-enums", async () => {
const schemaInput = new JSONSchemaInput(undefined);
await schemaInput.addSource({ name: "Out", schema });
const inputData = new InputData();
inputData.addInput(schemaInput);

const result = await quicktype({
inputData,
lang: "typescript",
rendererOptions: { "prefer-types": true },
});
const output = result.lines.join("\n");

expect(output).toContain("format: DataFormat | null;");
expect(output).toContain(
'export type DataFormat = "turtle" | "rdf_xml" | "n_triples" | "n_quads";',
);
expect(output).not.toMatch(/export type Turtle\b/);
});
Loading