@qninhdt/typespec-zod
v0.7.0
Published
TypeSpec emitter that generates Zod schemas from TypeSpec types.
Maintainers
Readme
@qninhdt/typespec-zod
TypeSpec emitter that generates namespace-grouped Zod schemas from default form models.
This emitter is intentionally focused on form and DTO shapes. It follows the same namespace and selector rules as the ORM-backed emitters, but it does not emit @table models.
What This Emitter Is For
Use this emitter when you want:
- Zod schemas generated from TypeSpec default form models
- inferred TypeScript types beside the schemas
- stable namespace-derived output layout
- rich field metadata for frontend forms
Installation
pnpm add -D \
@typespec/compiler \
@typespec/emitter-framework \
@alloy-js/core \
@alloy-js/typescript \
@qninhdt/typespec-orm \
@qninhdt/typespec-zod \
zodRuntime Expectations
Generated Zod output is intended to drop into TypeScript projects cleanly.
- standalone mode writes
package.json,tsconfig.json, andsrc/index.ts - generated code targets ESM-style package output
- runtime validation depends on
zod - inferred types are emitted in the same pass as the schemas, so no post-processing step is required
Configuration Reference
emit:
- "@qninhdt/typespec-zod"
options:
"@qninhdt/typespec-zod":
output-dir: "./outputs/zod"
standalone: true
library-name: "@acme/forms"
include: - "Demo.Platform.Forms"Supported options:
| Option | Type | Meaning |
| -------------- | ---------- | ------------------------------------------------- |
| output-dir | string | target directory handled by the TypeSpec compiler |
| standalone | boolean | write package metadata and emit under src/ |
| library-name | string | package name for standalone output |
| include | string[] | namespace or declaration selectors to keep |
| exclude | string[] | namespace or declaration selectors to drop |
Not supported:
filenamepackage-name- legacy post-write alias patching
Selector Behavior
Zod uses the same selector behavior as the ORM-backed emitters.
Examples:
include:
- "Demo.GamePlatform.Forms"
exclude:
- "Demo.GamePlatform.Forms.Internal"Behavior:
- selectors are dotted names, not glob patterns
excludewins overinclude- excluding a dependency required by a selected default form model fails emission
Output Layout
Given:
typescript
namespace App.Forms.Public;
Standalone output looks like:
text
outputs/zod/
package.json
tsconfig.json
src/
app/
forms/
public/
CreateInvitationForm.ts
index.ts
Non-standalone mode writes directly under the namespace folders and skips package metadata files.
Generated Package Contract
For each emitted default form model, the emitter writes:
- a
ModelSchema type Model = z.infer<typeof ModelSchema>ModelMetawhen field metadata is available
Standalone output also writes:
package.jsontsconfig.json- a root
src/index.tsbarrel that re-exports every generated data model
This means consumers can either import from the root barrel or from specific namespace paths.
Schema Example
`typescript import "@qninhdt/typespec-orm";
using Qninhdt.Orm;
namespace Demo.Accounts;
@table model User { @key id: uuid;
@maxLength(320) @format("email") email: string;
@maxLength(100) displayName: string; }
namespace Demo.Forms; model CreateInvitationForm { @title("Invitee Email") @placeholder("[email protected]") inviteeEmail: Demo.Accounts.User.email;
@title("Message") message?: text; }
@@inputType(CreateInvitationForm.message::type, "textarea"); `
Generated Behavior
For each default form model, the emitter generates:
ModelSchematype Model = z.infer<typeof ModelSchema>- optional
ModelMeta
Example shape:
`ts import { z } from "zod";
export const CreateInvitationFormSchema = z.object({ inviteeEmail: z.email().max(320), message: z.string().optional(), });
export type CreateInvitationForm = z.infer;
export const CreateInvitationFormMeta = { inviteeEmail: { title: "Invitee Email", placeholder: "[email protected]", inputType: "email", }, message: { title: "Message", inputType: "textarea", }, } as const; `
Form Metadata
The metadata export is built from:
@title@placeholder@@inputType- inferred input type from some formats such as
emailandurl
This gives frontend teams a single generated source for both validation and display hints.
Lookup Types And Constraint Inheritance
Zod generation works especially well with lookup types:
typescript
model PublicUser {
email: Demo.GamePlatform.Accounts.User.email;
}
That pattern lets a form or DTO model inherit scalar constraints from the source property, such as:
- string length bounds
- format-derived validators like
emailandurl - titles and placeholders when modeled on the default form field
If the public shape should diverge from the persistence model, define a dedicated default form property explicitly instead of chaining more lookup reuse.
Frontend Integration Pattern
The intended usage pattern is:
- model public-facing input shapes as default form models
- reuse field constraints with lookup types where it helps
- generate Zod output
- import
ModelSchema,Model, andModelMetain the frontend
That keeps validation, TypeScript inference, and form hints sourced from one schema without forcing frontend code to consume table models directly.
Supported Features
- namespace-first layout
- standalone package scaffolding via
library-name - root
index.tsbarrel - lookup-type constraint inheritance
- inferred TypeScript aliases emitted in the same render pass
- form metadata export
- shared filtering with
includeandexclude
Important Boundaries
- only default form models and
@tableMixinschemas are emitted - relation-heavy
@tablemodels are not the target of this emitter - foreign-key lookups to a non-
idcolumn (e.g.organizationCode: Organization.code) are emitted as a plain scalar reference matching the column's underlying scalar; the relationship is not preserved in the Zod output - if a default form model references a shape that cannot be represented cleanly as Zod output, emission fails; fix the source schema instead of expecting silent fallback behavior
Common Diagnostics And Gotchas
standalone-requires-library-nameStandalone package generation requireslibrary-name.- filtered dependency failures A selected default form model still needs every required dependency included by the selector set.
unsupported-typeThe TypeSpec field could not be mapped to a Zod schema and emission fails.- table-only shapes leaking into forms If a public form model starts to mirror a full persistence model, prefer writing an explicit default form model rather than reusing relation-heavy table shapes directly.
Practical guidance:
- keep default form models intentionally public-facing
- use lookup types for individual fields more often than for whole object graphs
- treat the root barrel as a convenience export, not a forced import style
Verification
The repo verifies generated Zod output with:
sh
pnpm run compile-examples
pnpm --dir outputs/file-vault/zod exec tsc -p tsconfig.json
pnpm --dir outputs/game-platform/zod exec tsc -p tsconfig.json
Related Docs
Made with heart by @qninhdt, with GPT-5.4 and Claude Opus 4.6.
