Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -241,7 +257,7 @@ export class TypeScriptZodRenderer extends ConvenienceRenderer {
);
});
});
this.emitLine("});");
this.emitLine("})", this.objectUnknownKeySuffix(t), ";");
if (!this._options.justSchema) {
this.emitLine(
"export type ",
Expand Down Expand Up @@ -307,7 +323,7 @@ export class TypeScriptZodRenderer extends ConvenienceRenderer {
},
);
});
this.emitLine("})");
this.emitLine("})", this.objectUnknownKeySuffix(t));
});
this.emitLine(");");
}
Expand Down Expand Up @@ -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);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ export class TypeScriptZodTargetLanguage extends TargetLanguage<
return true;
}

public get supportsFullObjectType(): boolean {
return true;
}

protected makeRenderer<Lang extends LanguageName = "typescript-zod">(
renderContext: RenderContext,
untypedOptionValues: RendererOptions<Lang>,
Expand Down
40 changes: 40 additions & 0 deletions test/unit/typescript-zod-additional-properties.test.ts
Original file line number Diff line number Diff line change
@@ -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())");
});