diff --git a/packages/quicktype-core/src/language/TypeScriptFlow/TypeScriptFlowBaseRenderer.ts b/packages/quicktype-core/src/language/TypeScriptFlow/TypeScriptFlowBaseRenderer.ts index 1c282f21a..09c06ee80 100644 --- a/packages/quicktype-core/src/language/TypeScriptFlow/TypeScriptFlowBaseRenderer.ts +++ b/packages/quicktype-core/src/language/TypeScriptFlow/TypeScriptFlowBaseRenderer.ts @@ -238,14 +238,14 @@ export abstract class TypeScriptFlowBaseRenderer extends JavaScriptRenderer { protected emitTypes(): void { this.forEachTopLevel("none", (t, name) => { - if (!t.isPrimitive() && !(t instanceof ArrayType)) { + if (this.namedTypeToNameForTopLevel(t) !== undefined) { return; } this.ensureBlankLine(); this.emitDescription(this.descriptionForType(t)); this.emitLine( - t instanceof ArrayType ? "export type " : "type ", + t.isPrimitive() ? "type " : "export type ", name, " = ", this.sourceFor(t).source, diff --git a/test/inputs/schema/empty-object.1.fail.json b/test/inputs/schema/empty-object.1.fail.json new file mode 100644 index 000000000..b5d8bb58d --- /dev/null +++ b/test/inputs/schema/empty-object.1.fail.json @@ -0,0 +1 @@ +[1, 2, 3] diff --git a/test/inputs/schema/empty-object.1.json b/test/inputs/schema/empty-object.1.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/test/inputs/schema/empty-object.1.json @@ -0,0 +1 @@ +{} diff --git a/test/inputs/schema/empty-object.schema b/test/inputs/schema/empty-object.schema new file mode 100644 index 000000000..f5c0db910 --- /dev/null +++ b/test/inputs/schema/empty-object.schema @@ -0,0 +1,6 @@ +{ + "id": "http://json-schema.org/geo", + "$schema": "http://json-schema.org/draft-06/schema#", + "type": "object", + "properties": {} +} diff --git a/test/languages.ts b/test/languages.ts index ed41f5904..b527cc346 100644 --- a/test/languages.ts +++ b/test/languages.ts @@ -1199,7 +1199,11 @@ export const JavaScriptPropTypesLanguage: Language = { "spotify-album.json", // renderer does not support recursion "76ae1.json", // renderer does not support recursion ], - skipSchema: ["integer-before-number.schema"], // Python-specific union-order regression. + skipSchema: [ + // The renderer does not support a bare top-level map. + "empty-object.schema", + "integer-before-number.schema", // Python-specific union-order regression. + ], skipMiscJSON: false, rendererOptions: { "module-system": "es6" }, quickTestRendererOptions: [{ converters: "top-level" }], @@ -1372,7 +1376,11 @@ I havea no idea how to encode these tests correctly. "php-mixed-union.json", "nst-test-suite.json", ], - skipSchema: ["integer-before-number.schema"], // Python-specific union-order regression. + skipSchema: [ + // The renderer does not support a bare top-level map. + "empty-object.schema", + "integer-before-number.schema", // Python-specific union-order regression. + ], skipMiscJSON: false, rendererOptions: { "just-types": "true" }, quickTestRendererOptions: [], @@ -1917,6 +1925,7 @@ export const HaskellLanguage: Language = { "nested-intersection-union.schema", "prefix-items.schema", "direct-union.schema", + "empty-object.schema", ...skipsEnumValueValidation, ...skipsMapValueValidation, "intersection.schema", @@ -1969,6 +1978,8 @@ export const PHPLanguage: Language = { ], skipMiscJSON: true, skipSchema: [ + // The renderer does not support a bare top-level map. + "empty-object.schema", "integer-before-number.schema", // Python-specific union-order regression. // PHP class names are case-insensitive, but the namer dedups // case-sensitively, so this declares classes that collide (same @@ -2078,6 +2089,8 @@ export const TypeScriptZodLanguage: Language = { ], skipMiscJSON: false, skipSchema: [ + // The renderer does not support a bare top-level map. + "empty-object.schema", "integer-before-number.schema", // Python-specific union-order regression. "any.schema", ...skipsUntypedUnions, @@ -2203,6 +2216,8 @@ export const TypeScriptEffectSchemaLanguage: Language = { ], skipMiscJSON: false, skipSchema: [ + // The renderer does not support a bare top-level map. + "empty-object.schema", "integer-before-number.schema", // Python-specific union-order regression. "any.schema", ...skipsUntypedUnions, @@ -2295,6 +2310,12 @@ export const ElixirLanguage: Language = { // for the Elixir emitter could be a user-controlled 'strict' mode that pattern matches even on unions of only primitive types. ...skipsMapValueValidation, + // A bare top-level map is emitted as a Jason.decode!/encode! pass-through + // with no shape validation, so the .fail.json case (a non-object) is not + // rejected and round-trips instead of exiting nonzero. Same permissiveness + // class as go-schema-pattern-properties above. + "empty-object.schema", + // The generated top-level type is not emitted as a TopLevel module the fixture can call. "recursive-union-flattening.schema", diff --git a/test/unit/typescript-flow-top-level-alias.test.ts b/test/unit/typescript-flow-top-level-alias.test.ts new file mode 100644 index 000000000..9af4ec011 --- /dev/null +++ b/test/unit/typescript-flow-top-level-alias.test.ts @@ -0,0 +1,74 @@ +// A top-level empty-object schema is inferred as a map. The schema fixture +// exercises its generated converters, but those converters inline the map +// type, so they still compile when the public top-level alias is missing. +// Assert the declaration itself here to prevent that regression. + +import { + InputData, + JSONSchemaInput, + type LanguageName, + quicktype, +} from "quicktype-core"; +import { describe, expect, test } from "vitest"; + +async function renderSchema( + lang: LanguageName, + name: string, + schema: object, +): Promise { + const schemaInput = new JSONSchemaInput(undefined); + await schemaInput.addSource({ name, schema: JSON.stringify(schema) }); + const inputData = new InputData(); + inputData.addInput(schemaInput); + + const result = await quicktype({ + inputData, + lang, + rendererOptions: { "just-types": true }, + }); + return result.lines.join("\n"); +} + +const emptyObjectSchema = { + $schema: "http://json-schema.org/draft-06/schema#", + type: "object", + properties: {}, +}; + +describe("TypeScript/Flow unnamed top-level aliases", () => { + test.each([ + ["typescript", "unknown"], + ["flow", "mixed"], + ] as const)("%s emits an empty-object map alias", async (lang, anyType) => { + const output = await renderSchema( + lang, + "EmptySchema", + emptyObjectSchema, + ); + + expect(output).toContain( + `export type EmptySchema = { [key: string]: ${anyType} };`, + ); + }); + + test.each([ + "typescript", + "flow", + ] as const)("%s does not alias a map whose value type claims the top-level name", async (lang) => { + const output = await renderSchema(lang, "TopLevel", { + type: "object", + additionalProperties: { + type: "object", + properties: { + one: { type: "integer" }, + two: { type: "boolean" }, + }, + required: ["one", "two"], + }, + }); + const declarations = + output.match(/export (?:type|interface) TopLevel\b/g) ?? []; + + expect(declarations).toHaveLength(1); + }); +});