diff --git a/packages/quicktype-core/src/language/TypeScriptFlow/TypeScriptRenderer.ts b/packages/quicktype-core/src/language/TypeScriptFlow/TypeScriptRenderer.ts index b162b9384..a597e315f 100644 --- a/packages/quicktype-core/src/language/TypeScriptFlow/TypeScriptRenderer.ts +++ b/packages/quicktype-core/src/language/TypeScriptFlow/TypeScriptRenderer.ts @@ -8,7 +8,12 @@ import { singleWord, } from "../../Source.js"; import { camelCase, utf16StringEscape } from "../../support/Strings.js"; -import type { ArrayType, 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"; @@ -33,6 +38,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); + } + // An array with `minItems` >= 1 becomes a tuple that spells out the // guaranteed elements, followed by a rest element: `minItems: 2` // renders as `[T, T, ...T[]]`. Only `minItems` shapes the type; @@ -81,7 +94,7 @@ export class TypeScriptRenderer extends TypeScriptFlowBaseRenderer { "(json: ", jsonType, "): ", - this.sourceFor(t).source, + t instanceof ArrayType ? name : this.sourceFor(t).source, ]; } @@ -93,7 +106,7 @@ export class TypeScriptRenderer extends TypeScriptFlowBaseRenderer { "public static ", camelCaseName, "ToJson(value: ", - this.sourceFor(t).source, + t instanceof ArrayType ? name : this.sourceFor(t).source, "): ", returnType, ]; @@ -116,7 +129,7 @@ export class TypeScriptRenderer extends TypeScriptFlowBaseRenderer { (_t, name) => { topLevelNames.push(", ", name); }, - isNamedType, + (t) => isNamedType(t) || t instanceof ArrayType, ); this.emitLine( "// import { Convert", @@ -127,6 +140,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.fail.no-defaults.json b/test/inputs/schema/root-array-ref.1.fail.no-defaults.json new file mode 100644 index 000000000..e82af46d6 --- /dev/null +++ b/test/inputs/schema/root-array-ref.1.fail.no-defaults.json @@ -0,0 +1,3 @@ +[ + {} +] 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 000000000..469ffced6 --- /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 000000000..5066b3f0a --- /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/languages.ts b/test/languages.ts index 120f3170b..431a7059b 100644 --- a/test/languages.ts +++ b/test/languages.ts @@ -678,6 +678,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", // The default-value fail sample also relies on required-property // enforcement, which cJSON does not do. "default-value.schema", @@ -1002,6 +1005,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", // The default-value fail sample also relies on required-property enforcement. "default-value.schema", "multi-type-enum.schema", @@ -1706,6 +1712,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", "issue2680-top-level-array.schema", ], skipMiscJSON: false, @@ -1918,6 +1925,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", // The default-value fail sample also relies on required-property enforcement. "default-value.schema", "required-non-properties.schema", @@ -1976,6 +1986,7 @@ export const PHPLanguage: Language = { "top-level-enum.schema", // The driver does not support top-level arrays. "union.schema", + "root-array-ref.schema", "issue2680-top-level-array.schema", ], rendererOptions: {}, @@ -2090,6 +2101,11 @@ export const TypeScriptZodLanguage: Language = { // The default-value fail sample also relies on required-property enforcement. "default-value.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: [], @@ -2273,6 +2289,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", // The default-value fail sample also relies on required-property enforcement. "default-value.schema", "boolean-subschema.schema", 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 000000000..133b50964 --- /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"); +});