TypeGen — Zod emitter (runtime validation)
TypeTarget.Zod emits Zod schemas — TypeScript source that
ships a runtime validator alongside an inferred type. Pairs with the TypeScript
target (both can emit in parallel, independent files, zero coupling) or runs
standalone when you want only Zod without a separate interface file.
[GenerateTypes(Targets = TypeTarget.TypeScript | TypeTarget.Zod, OutputDir = "../client/src/api")]public class Order{ public int Id { get; set; } [ZEmail] public string Email { get; set; } = ""; [ZRange(1, 100)] public int Qty { get; set; } public OrderStatus Status { get; set; }}→ Order.ts (TypeScript emitter, unchanged):
export interface Order { id: number; email: string; qty: number; status: OrderStatus;}→ Order.schema.ts (Zod emitter, new):
import { z } from 'zod';import { OrderStatusSchema } from './OrderStatus.schema';
export const OrderSchema = z.object({ id: z.number().int(), email: z.email(), qty: z.number().int().gte(1).lte(100), status: OrderStatusSchema,});export type Order = z.infer<typeof OrderSchema>;Independent from TypeScript emitter by default. Both files are generated from the same
SchemaModel and regenerate together on the next build. The TS interface stays as the
ergonomic type-only view (cheap to import, no runtime dep); the Zod schema
carries the runtime validator and its own z.infer alias for Zod-only consumers.
Enable ConformToTypeScriptTypes when you also want z.toZod<T>() to make the
TypeScript compiler prove that both generated shapes match.
Validation constraint mapping
Section titled “Validation constraint mapping”Same attributes that drive OpenAPI constraints map to Zod chained calls:
| C# | Zod |
|---|---|
[MinLength(n)], [ZMinLength(n)] | .min(n) |
[MaxLength(n)], [ZMaxLength(n)] | .max(n) |
[Range(min, max)], [ZRange(min, max)] | .gte(min).lte(max) |
[RegularExpression("pat")], [ZMatch("pat")] | .regex(/pat/) |
[EmailAddress], [ZEmail] | z.email() |
[Url], [ZUrl] | z.url() |
[CreditCard], [ZCreditCard] | z.creditCard() |
[ZIban] | z.iban() |
System.Guid | z.uuid() |
System.DateTime | z.iso.datetime() |
System.DateOnly | z.iso.date() |
The emitter targets Zod 4 — it emits the top-level format factories
(z.uuid(), z.email(), z.iso.datetime(), …) rather than the chained
z.string().uuid() forms that Zod 4 deprecated. Install Zod 4: npm install zod@^4.
Zod-only formats can be selected without changing the OpenAPI contract:
[ZodFormat(ZodStringFormat.Ulid)]public string PublicId { get; set; } = "";
b.ForType<Payment>() .Property(x => x.CardToken) .ZodFormat(ZodStringFormat.Base64Url);
// Zod 4.6 custom NanoID length:b.ForType<Payment>() .Property(x => x.PublicToken) .ZodNanoId(16);Available formats are email, URL, UUID, date, date-time, hostname, ULID, NanoID, Base64, Base64URL, credit card, and IBAN.
External schemas with [ZodSchema]
Section titled “External schemas with [ZodSchema]”[TsType] controls only the static TypeScript type; it cannot provide runtime
validation. Use [ZodSchema] when a third-party package or shared frontend
module already exports the runtime schema for a C# property:
public sealed class OrderDto{ [TsType("Point", ImportFrom = "geojson")] [ZodSchema("GeoJSONPointSchema", ImportFrom = "zod-geojson")] public Point? DeliveryAreaPoint { get; init; }
[TsType("Feature", ImportFrom = "geojson")] [ZodSchema("GeoJSONFeatureSchema", ImportFrom = "zod-geojson")] public Feature? DeliveryAreaFeature { get; init; }}The Zod file imports and uses those schemas directly:
import { GeoJSONFeatureSchema, GeoJSONPointSchema } from 'zod-geojson';
export const OrderDtoSchema = z.object({ deliveryAreaPoint: (GeoJSONPointSchema).nullish(), deliveryAreaFeature: (GeoJSONFeatureSchema).nullish(),});Install the referenced runtime package in the generated client’s project:
npm install zod zod-geojsonnpm install --save-dev @types/geojsonThe same override is available when the C# model cannot be annotated:
b.ForType<OrderDto>() .Property(x => x.DeliveryAreaPoint) .ZodSchema("GeoJSONPointSchema", "zod-geojson");The schema expression is emitted verbatim. Omit ImportFrom for inline
expressions such as z.string().startsWith('ord_'). TypeGen still applies the
property’s nullable, optional, read-only, and PatchField<T> modifier after the
override. Other inferred validation constraints are not appended because the
explicit schema owns validation for that property.
When ImportFrom is set, a bare identifier supplies its own import name. For
compound expressions, specify the exported name explicitly:
[ZodSchema("GeoJSONPointSchema.refine(point => point.coordinates.length >= 2)", ImportFrom = "zod-geojson", Import = "GeoJSONPointSchema")]public Point? DeliveryAreaPoint { get; init; }
// Or on a referenced type:b.ForType<OrderDto>() .Property(x => x.DeliveryAreaPoint) .ZodSchema("GeoJSONPointSchema.refine(point => point.coordinates.length >= 2)", "zod-geojson", "GeoJSONPointSchema");TypeGen reports TG0021 if a compound expression has ImportFrom without an
explicit import name. It aliases external imports when their names clash with a
generated schema or another module. Override expressions that refer to generated
schemas are included in import, ordering, and cycle analysis; cyclic references
are emitted through z.lazy(...).
Imports sharing a module are grouped and deduplicated in both single-file and
file-per-class output. External schemas also participate normally in
z.compile(...), generated guards, and TanStack payload parsing. With
ConformToTypeScriptTypes, the external schema’s inferred type must exactly
match the [TsType] expression—not merely be structurally assignable. For
zod-geojson, use its own inferred aliases when exact conformance is enabled:
[TsType("GeoJSONPoint", ImportFrom = "zod-geojson")][ZodSchema("GeoJSONPointSchema", ImportFrom = "zod-geojson")]public Point? DeliveryAreaPoint { get; init; }Type mapping
Section titled “Type mapping”| C# | Zod |
|---|---|
int, long, short, byte | z.number().int() |
float, double | z.number() |
decimal | z.string() (precision-preserving, matches TS) |
string | z.string() |
bool | z.boolean() |
T? (nullable) | .nullish() (null ∪ undefined ∪ absent) |
PatchField<T> | T.optional() (omission is distinct from an explicit value) |
List<T>, T[] | z.array(T) |
Dictionary<string, V> | z.record(z.string(), V) |
| user DTO | direct ref {Name}Schema (cross-file import) |
[ZodSchema("X", ImportFrom = "pkg")] | imported runtime schema X |
numeric enum | z.union([z.literal(0), z.literal(1), …]) |
enum + [JsonStringEnumConverter] | z.enum(['A', 'B', …]) |
Polymorphic unions
Section titled “Polymorphic unions”[JsonPolymorphic] + [JsonDerivedType] on the C# side produces a
z.discriminatedUnion — matching the TypeScript discriminated-union semantics
exactly, with exhaustive narrowing from the discriminator literal:
export const ShapeSchema = z.discriminatedUnion('kind', [ CircleSchema, SquareSchema,]);export type Shape = z.infer<typeof ShapeSchema>;
// In CircleSchema:export const CircleSchema = z.object({ kind: z.literal('circle'), radius: z.number(),});Inheritance
Section titled “Inheritance”When the base class is in the emit set, derived schemas compose via .extend():
// Derived class Order : Entityexport const OrderSchema = EntitySchema.extend({ customer: z.string(),});Configuration
Section titled “Configuration”b.Zod(z =>{ z.OutputDir = "../client/src/validation"; z.FileLayout = ZodFileLayout.SingleFile; z.SingleFileName = "schemas.ts"; z.SchemaConstSuffix = "Schema"; // default; "XxxSchema" z.EmitInferredTypes = true; // default; adds `export type X = z.infer<…>` z.FileSuffix = ".schema"; // `Order.schema.ts` avoids collision with TS's `Order.ts` z.ConformToTypeScriptTypes = true; // opt-in `z.toZod<T>()` compile-time check z.Compilation = ZodCompilationMode.Compile; // opt-in optimized parser z.EmitValidationGuards = true; // emits `isOrder(value)` using `.validate()`});Recursive DTO references are emitted with z.lazy(...), including mutually
recursive types split across files. Compilation is opt-in because Zod uses
generated JavaScript internally; unsupported schemas safely fall back to the
normal parser.
Consumer install: the emitted code imports zod — add it to the frontend
project: npm install zod@^4.6.1. The emitter targets Zod 4’s top-level format
factories. TypeGen doesn’t bundle or generate the dep.