From b44e7d68d4879788e11fbedde372dc3bdb06ef7d Mon Sep 17 00:00:00 2001 From: Louis Bompart Date: Tue, 4 Aug 2026 22:24:07 +0200 Subject: [PATCH 1/2] feat(typescript-zod): validate additional properties --- .../TypeScriptZod/TypeScriptZodRenderer.ts | 25 ++++++++++-- .../src/language/TypeScriptZod/language.ts | 4 ++ ...ditional.2.fail.additional-properties.json | 7 ++++ test/languages.ts | 10 ++++- ...pescript-zod-additional-properties.test.ts | 40 +++++++++++++++++++ 5 files changed, 81 insertions(+), 5 deletions(-) create mode 100644 test/inputs/schema/class-with-additional.2.fail.additional-properties.json create mode 100644 test/unit/typescript-zod-additional-properties.test.ts diff --git a/packages/quicktype-core/src/language/TypeScriptZod/TypeScriptZodRenderer.ts b/packages/quicktype-core/src/language/TypeScriptZod/TypeScriptZodRenderer.ts index d9d62c617c..7e08d912a5 100644 --- a/packages/quicktype-core/src/language/TypeScriptZod/TypeScriptZodRenderer.ts +++ b/packages/quicktype-core/src/language/TypeScriptZod/TypeScriptZodRenderer.ts @@ -223,6 +223,22 @@ export class TypeScriptZodRenderer extends ConvenienceRenderer { return this._recursiveTypeRefs.has(t.typeRef); } + protected objectUnknownKeySuffix(t: ObjectType): Sourcelike { + const additionalProperties = t.getAdditionalProperties(); + if (additionalProperties === undefined) { + return ".strict()"; + } + if (additionalProperties.kind === "any") { + return ".passthrough()"; + } + + return [ + ".catchall(", + this.typeMapTypeFor(additionalProperties, false), + ")", + ]; + } + protected emitObject(name: Name, t: ObjectType): void { if (this.isRecursive(t)) { this.emitLazyObject(name, t); @@ -241,7 +257,7 @@ export class TypeScriptZodRenderer extends ConvenienceRenderer { ); }); }); - this.emitLine("});"); + this.emitLine("})", this.objectUnknownKeySuffix(t), ";"); if (!this._options.justSchema) { this.emitLine( "export type ", @@ -307,7 +323,7 @@ export class TypeScriptZodRenderer extends ConvenienceRenderer { }, ); }); - this.emitLine("})"); + this.emitLine("})", this.objectUnknownKeySuffix(t)); }); this.emitLine(");"); } @@ -383,8 +399,9 @@ export class TypeScriptZodRenderer extends ConvenienceRenderer { } } - // Finally return the reference to a class as that will need to be defined (where objects, maps, unions, intersections and arrays do not) - if (type instanceof ClassType) { + // Finally return references to named classes and full objects as those + // need to be defined. Maps, unions, intersections, and arrays do not. + if (type instanceof ClassType || type.kind === "object") { typeRefs.push(type.typeRef); } } diff --git a/packages/quicktype-core/src/language/TypeScriptZod/language.ts b/packages/quicktype-core/src/language/TypeScriptZod/language.ts index 63b9abab51..72e8b76147 100644 --- a/packages/quicktype-core/src/language/TypeScriptZod/language.ts +++ b/packages/quicktype-core/src/language/TypeScriptZod/language.ts @@ -51,6 +51,10 @@ export class TypeScriptZodTargetLanguage extends TargetLanguage< return true; } + public get supportsFullObjectType(): boolean { + return true; + } + protected makeRenderer( renderContext: RenderContext, untypedOptionValues: RendererOptions, diff --git a/test/inputs/schema/class-with-additional.2.fail.additional-properties.json b/test/inputs/schema/class-with-additional.2.fail.additional-properties.json new file mode 100644 index 0000000000..2554b6a622 --- /dev/null +++ b/test/inputs/schema/class-with-additional.2.fail.additional-properties.json @@ -0,0 +1,7 @@ +{ + "map": { + "foo": 3.1415, + "bar": true + }, + "unexpected": true +} diff --git a/test/languages.ts b/test/languages.ts index 5e721fad1e..f916cb4522 100644 --- a/test/languages.ts +++ b/test/languages.ts @@ -73,6 +73,7 @@ export type LanguageFeature = | "no-defaults" | "strict-optional" | "date-time" + | "additional-properties" | "integer-string" | "bool-string" | "uuid" @@ -2037,7 +2038,14 @@ export const TypeScriptZodLanguage: Language = { "e8b04.json", ], allowMissingNull: false, - features: ["enum", "union", "no-defaults", "date-time", "minmaxitems"], + features: [ + "enum", + "union", + "no-defaults", + "date-time", + "additional-properties", + "minmaxitems", + ], output: "TopLevel.ts", topLevel: "TopLevel", skipJSON: [ diff --git a/test/unit/typescript-zod-additional-properties.test.ts b/test/unit/typescript-zod-additional-properties.test.ts new file mode 100644 index 0000000000..e0a9336c65 --- /dev/null +++ b/test/unit/typescript-zod-additional-properties.test.ts @@ -0,0 +1,40 @@ +import { InputData, JSONSchemaInput, quicktype } from "quicktype-core"; +import { expect, test } from "vitest"; + +test("TypeScript Zod preserves JSON Schema additionalProperties semantics", async () => { + const schemaInput = new JSONSchemaInput(undefined); + await schemaInput.addSource({ + name: "TopLevel", + schema: JSON.stringify({ + type: "object", + properties: { + strict: { + type: "object", + properties: { value: { type: "string" } }, + additionalProperties: false, + }, + loose: { + type: "object", + properties: { value: { type: "string" } }, + additionalProperties: true, + }, + typed: { + type: "object", + properties: { value: { type: "string" } }, + additionalProperties: { type: "boolean" }, + }, + }, + required: ["strict", "loose", "typed"], + additionalProperties: false, + }), + }); + const inputData = new InputData(); + inputData.addInput(schemaInput); + + const result = await quicktype({ inputData, lang: "typescript-zod" }); + const output = result.lines.join("\n"); + + expect(output).toContain(".strict()"); + expect(output).toContain(".passthrough()"); + expect(output).toContain(".catchall(z.boolean())"); +}); From bf698edf89e12991e0ecfabcc7706147f4098b3d Mon Sep 17 00:00:00 2001 From: Louis Bompart Date: Tue, 4 Aug 2026 22:56:27 +0200 Subject: [PATCH 2/2] test(typescript-zod): simplify additional properties coverage --- ...s-with-additional.2.fail.additional-properties.json | 7 ------- test/languages.ts | 10 +--------- 2 files changed, 1 insertion(+), 16 deletions(-) delete mode 100644 test/inputs/schema/class-with-additional.2.fail.additional-properties.json diff --git a/test/inputs/schema/class-with-additional.2.fail.additional-properties.json b/test/inputs/schema/class-with-additional.2.fail.additional-properties.json deleted file mode 100644 index 2554b6a622..0000000000 --- a/test/inputs/schema/class-with-additional.2.fail.additional-properties.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "map": { - "foo": 3.1415, - "bar": true - }, - "unexpected": true -} diff --git a/test/languages.ts b/test/languages.ts index f916cb4522..5e721fad1e 100644 --- a/test/languages.ts +++ b/test/languages.ts @@ -73,7 +73,6 @@ export type LanguageFeature = | "no-defaults" | "strict-optional" | "date-time" - | "additional-properties" | "integer-string" | "bool-string" | "uuid" @@ -2038,14 +2037,7 @@ export const TypeScriptZodLanguage: Language = { "e8b04.json", ], allowMissingNull: false, - features: [ - "enum", - "union", - "no-defaults", - "date-time", - "additional-properties", - "minmaxitems", - ], + features: ["enum", "union", "no-defaults", "date-time", "minmaxitems"], output: "TopLevel.ts", topLevel: "TopLevel", skipJSON: [