Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions test/inputs/schema/empty-object.1.fail.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[1, 2, 3]
1 change: 1 addition & 0 deletions test/inputs/schema/empty-object.1.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
6 changes: 6 additions & 0 deletions test/inputs/schema/empty-object.schema
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"id": "http://json-schema.org/geo",
"$schema": "http://json-schema.org/draft-06/schema#",
"type": "object",
"properties": {}
}
25 changes: 23 additions & 2 deletions test/languages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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" }],
Expand Down Expand Up @@ -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: [],
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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",

Expand Down
74 changes: 74 additions & 0 deletions test/unit/typescript-flow-top-level-alias.test.ts
Original file line number Diff line number Diff line change
@@ -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<string> {
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);
});
});
Loading