From 66fc617a2507f9765baf54474382bde951829abe Mon Sep 17 00:00:00 2001 From: Mark Probst Date: Mon, 20 Jul 2026 18:08:54 -0400 Subject: [PATCH 1/4] fix(typescript): emit named type alias for root-level array types (#1309) TypeScript collapsed a root-level array-of-objects type into a single interface named after the array (using directlyReachableSingleNamedType), dropping the array wrapper entirely and emitting no array type alias. Other languages such as Go correctly emit a named array alias plus a separately named object type. Fix: override namedTypeToNameForTopLevel in TypeScriptRenderer to stop collapsing ArrayType top levels into their item type, and emit an `export type Foo = Bar[];` alias for root-level array types, updating converter signatures/usage comments to reference it. Added test/inputs/schema/root-array-ref.schema (+ .1.json sample) as an end-to-end fixture, and test/unit/typescript-root-array-schema.test.ts to assert the exact naming/structure (fixture round-trip tests alone can't express that). Fixes #1309 Co-Authored-By: gpt-5.6-sol via pi --- .../TypeScriptFlow/TypeScriptRenderer.ts | 41 +++++++++++++++++-- test/inputs/schema/root-array-ref.1.json | 5 +++ test/inputs/schema/root-array-ref.schema | 26 ++++++++++++ .../unit/typescript-root-array-schema.test.ts | 28 +++++++++++++ 4 files changed, 96 insertions(+), 4 deletions(-) create mode 100644 test/inputs/schema/root-array-ref.1.json create mode 100644 test/inputs/schema/root-array-ref.schema create mode 100644 test/unit/typescript-root-array-schema.test.ts diff --git a/packages/quicktype-core/src/language/TypeScriptFlow/TypeScriptRenderer.ts b/packages/quicktype-core/src/language/TypeScriptFlow/TypeScriptRenderer.ts index 1d0d488424..9adbcace0f 100644 --- a/packages/quicktype-core/src/language/TypeScriptFlow/TypeScriptRenderer.ts +++ b/packages/quicktype-core/src/language/TypeScriptFlow/TypeScriptRenderer.ts @@ -1,7 +1,12 @@ import type { Name } from "../../Naming.js"; import { type Sourcelike, modifySource } from "../../Source.js"; import { camelCase, utf16StringEscape } from "../../support/Strings.js"; -import type { ClassType, EnumType, Type } from "../../Type/index.js"; +import { + ArrayType, + type ClassType, + type EnumType, + type Type, +} from "../../Type/index.js"; import { isNamedType } from "../../Type/TypeUtils.js"; import type { JavaScriptTypeAnnotations } from "../JavaScript/index.js"; @@ -17,6 +22,14 @@ export class TypeScriptRenderer extends TypeScriptFlowBaseRenderer { return ["Array", "Date"]; } + protected namedTypeToNameForTopLevel(type: Type): Type | undefined { + if (type instanceof ArrayType) { + return undefined; + } + + return super.namedTypeToNameForTopLevel(type); + } + protected uncheckedParsedJson(t: Type, parsedJson: Sourcelike): Sourcelike { // With `raw-type any` and `prefer-unknown` the deserializer's // parameter is `unknown`, which can't be returned as the target @@ -40,7 +53,7 @@ export class TypeScriptRenderer extends TypeScriptFlowBaseRenderer { "(json: ", jsonType, "): ", - this.sourceFor(t).source, + t instanceof ArrayType ? name : this.sourceFor(t).source, ]; } @@ -52,7 +65,7 @@ export class TypeScriptRenderer extends TypeScriptFlowBaseRenderer { "public static ", camelCaseName, "ToJson(value: ", - this.sourceFor(t).source, + t instanceof ArrayType ? name : this.sourceFor(t).source, "): ", returnType, ]; @@ -75,7 +88,7 @@ export class TypeScriptRenderer extends TypeScriptFlowBaseRenderer { (_t, name) => { topLevelNames.push(", ", name); }, - isNamedType, + (t) => isNamedType(t) || t instanceof ArrayType, ); this.emitLine( "// import { Convert", @@ -86,6 +99,26 @@ export class TypeScriptRenderer extends TypeScriptFlowBaseRenderer { ); } + protected emitTypes(): void { + super.emitTypes(); + + this.forEachTopLevel("none", (t, name) => { + if (!(t instanceof ArrayType)) { + return; + } + + this.ensureBlankLine(); + this.emitDescription(this.descriptionForType(t)); + this.emitLine( + "export type ", + name, + " = ", + this.sourceFor(t).source, + ";", + ); + }); + } + protected emitEnum(e: EnumType, enumName: Name): void { this.emitDescription(this.descriptionForType(e)); diff --git a/test/inputs/schema/root-array-ref.1.json b/test/inputs/schema/root-array-ref.1.json new file mode 100644 index 0000000000..469ffced62 --- /dev/null +++ b/test/inputs/schema/root-array-ref.1.json @@ -0,0 +1,5 @@ +[ + { + "name": "quicktype" + } +] diff --git a/test/inputs/schema/root-array-ref.schema b/test/inputs/schema/root-array-ref.schema new file mode 100644 index 0000000000..5066b3f0a4 --- /dev/null +++ b/test/inputs/schema/root-array-ref.schema @@ -0,0 +1,26 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$ref": "#/definitions/GetResp", + "definitions": { + "GetResp": { + "title": "GetResp", + "type": "array", + "items": { + "$ref": "#/definitions/SomeObject" + } + }, + "SomeObject": { + "title": "SomeObject", + "type": "object", + "additionalProperties": false, + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ] + } + } +} diff --git a/test/unit/typescript-root-array-schema.test.ts b/test/unit/typescript-root-array-schema.test.ts new file mode 100644 index 0000000000..133b509641 --- /dev/null +++ b/test/unit/typescript-root-array-schema.test.ts @@ -0,0 +1,28 @@ +import { readFileSync } from "node:fs"; + +import { + FetchingJSONSchemaStore, + InputData, + JSONSchemaInput, + quicktype, +} from "quicktype-core"; +import { expect, test } from "vitest"; + +test("TypeScript preserves a named root array and its object item type", async () => { + const schema = readFileSync( + new URL("../inputs/schema/root-array-ref.schema", import.meta.url), + "utf8", + ); + const schemaInput = new JSONSchemaInput(new FetchingJSONSchemaStore()); + await schemaInput.addSource({ name: "PR", schema }); + + const inputData = new InputData(); + inputData.addInput(schemaInput); + + const result = await quicktype({ inputData, lang: "typescript" }); + const output = result.lines.join("\n"); + + expect(output).toContain("export interface SomeObject"); + expect(output).toContain("export type PR = SomeObject[];"); + expect(output).not.toContain("export interface PR"); +}); From e4ba40b3e85f8cf94893e022353608c41ccbf9ac Mon Sep 17 00:00:00 2001 From: Mark Probst Date: Mon, 20 Jul 2026 19:36:11 -0400 Subject: [PATCH 2/4] test: add missing fixture cases for root-array-ref.schema (#3033) Co-Authored-By: Claude --- test/inputs/schema/root-array-ref.1.fail.no-defaults.json | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 test/inputs/schema/root-array-ref.1.fail.no-defaults.json diff --git a/test/inputs/schema/root-array-ref.1.fail.no-defaults.json b/test/inputs/schema/root-array-ref.1.fail.no-defaults.json new file mode 100644 index 0000000000..e82af46d6f --- /dev/null +++ b/test/inputs/schema/root-array-ref.1.fail.no-defaults.json @@ -0,0 +1,3 @@ +[ + {} +] From 191b700dc26c71584e333a7d43799b682200f442 Mon Sep 17 00:00:00 2001 From: Mark Probst Date: Tue, 21 Jul 2026 12:10:25 -0400 Subject: [PATCH 3/4] test: skip root-array-ref.schema for languages that can't handle it The new root-array-ref.schema fixture is a top-level array whose item type carries its own title. Three languages cannot round-trip it and were failing CI: - kotlinx: top-level arrays render as `typealias TopLevel = JsonArray`, which doesn't compile (same documented limitation as union.schema). - php: the fixture driver does not support top-level arrays (same as union.schema). - typescript-zod: the renderer emits only the titled item schema (SomeObjectSchema) and no TopLevelElementSchema, so the driver can't locate a top-level schema to parse the array against. Skip the fixture for these three, mirroring the existing union.schema precedent, rather than weakening the fixture for languages that handle it correctly. The positive and negative cases still run for TypeScript and the other schema languages. Co-Authored-By: Claude --- test/languages.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/test/languages.ts b/test/languages.ts index 4e2cf42083..5c837de213 100644 --- a/test/languages.ts +++ b/test/languages.ts @@ -1688,6 +1688,7 @@ export const KotlinXLanguage: Language = { // Top-level array: `typealias TopLevel = JsonArray` doesn't // compile (documented TODO in KotlinXRenderer.ts). "union.schema", + "root-array-ref.schema", ], skipMiscJSON: false, rendererOptions: { framework: "kotlinx" }, @@ -1953,6 +1954,7 @@ export const PHPLanguage: Language = { "top-level-enum.schema", // The driver does not support top-level arrays. "union.schema", + "root-array-ref.schema", ], rendererOptions: {}, quickTestRendererOptions: [], @@ -2068,6 +2070,11 @@ export const TypeScriptZodLanguage: Language = { "recursive-union-flattening.schema", "required.schema", "required-non-properties.schema", + // Top-level array whose item type has its own title: the zod + // renderer emits only the item schema (SomeObjectSchema) and no + // TopLevelElementSchema, so the driver can't locate a top-level + // schema to parse the array against. + "root-array-ref.schema", ], rendererOptions: {}, quickTestRendererOptions: [], From 301d14ecb1d7522c78658b3d70d491f45363f1d1 Mon Sep 17 00:00:00 2001 From: Mark Probst Date: Tue, 21 Jul 2026 12:21:39 -0400 Subject: [PATCH 4/4] test: skip root-array-ref.schema for languages that don't enforce required The root-array-ref negative sample ([{}], an array element missing the required "name" property) relies on required-property enforcement. Four more languages already skip required.schema because their generated code or driver cannot detect a missing required property, so the new fixture's fail sample is likewise not rejected and the fixture fails: - cjson: required properties absent are not checked. - swift: on Linux the missing-required-property failure is not rejected. - haskell: the driver encodes the Maybe result, so a failed decode prints null and exits 0. - elixir: struct keys cannot be enforced at runtime. Skip root-array-ref.schema for each, mirroring their existing required.schema skips. The positive and negative cases still run for the languages that do enforce required (TypeScript, C#, Java, Rust, etc.). Co-Authored-By: Claude --- test/languages.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/test/languages.ts b/test/languages.ts index 5c837de213..1eebc28bd4 100644 --- a/test/languages.ts +++ b/test/languages.ts @@ -648,6 +648,9 @@ export const CJSONLanguage: Language = { /* Required properties absent are not checked (for the current implementation, can be added later, should abord parsing and return NULL) */ "intersection.schema", "required.schema", + /* Root-level array of a required-property object: the missing + * required property in the fail sample is likewise not rejected. */ + "root-array-ref.schema", /* Pure Any type not supported (for the current implementation, can be added later, should manage a callback to provide the final application a way to handle it at parsing and creation of cJSON) */ "any.schema", "direct-union.schema", @@ -969,6 +972,9 @@ export const SwiftLanguage: Language = { // This works on macOS, but on Linux one of the failure test cases doesn't fail ...skipsUntypedUnions, "required.schema", + // Same Linux-only issue: the missing-required-property fail sample + // for the root-level array is not rejected. + "root-array-ref.schema", "multi-type-enum.schema", "intersection.schema", ...skipsMapValueValidation, @@ -1898,6 +1904,9 @@ export const HaskellLanguage: Language = { "keyword-unions.schema", "optional-any.schema", "required.schema", + // The driver encodes the Maybe result, so the missing-required-property + // fail sample for the root-level array decodes to null and exits 0. + "root-array-ref.schema", "required-non-properties.schema", ], rendererOptions: {}, @@ -2248,6 +2257,9 @@ export const ElixirLanguage: Language = { // Struct keys cannot be enforced at runtime in Elixir and their values will just be set to null. "strict-optional.schema", "required.schema", + // Same reason: the missing-required-property fail sample for the + // root-level array is not rejected. + "root-array-ref.schema", "boolean-subschema.schema", "intersection.schema", "optional-any.schema",