Free & open source — no account required
Free & open source — no account required
This technical guide provides an in-depth analysis of the json to zod engine, best practices for implementation, and data security standards.
TypeScript types are erased at runtime — they don't protect you from a malformed API response, a form submission with an unexpected shape, or a database record with a missing field. Zod fills that gap. By generating a Zod schema from your JSON, you get both the TypeScript type and the runtime validator from a single source of truth, so they can never drift apart.
// Input JSON from an API
{
"api_version": "v2",
"status": "success",
"data": {
"count": 42,
"items": [
{ "id": "p_001", "name": "Widget", "price": 19.99, "in_stock": true }
]
}
}
// Generated Zod Schema
import { z } from 'zod';
const ItemSchema = z.object({
id: z.string().uuid(),
name: z.string().min(1),
price: z.number().positive(),
in_stock: z.boolean(),
});
const ApiResponseSchema = z.object({
api_version: z.string(),
status: z.enum(['success', 'error']),
data: z.object({
count: z.number().int().nonnegative(),
items: z.array(ItemSchema),
}),
});
export type ApiResponse = z.infer<typeof ApiResponseSchema>;
export type Item = z.infer<typeof ItemSchema>;
Zod offers two parsing modes and picking the wrong one is a common mistake:
// parse() — throws ZodError on failure
// Use for: server-side startup validation, tests, CI checks
const config = ConfigSchema.parse(process.env);
// safeParse() — returns { success, data } or { success, error }
// Use for: user input, API responses, anywhere you want to handle errors gracefully
const result = UserSchema.safeParse(req.body);
if (!result.success) {
const errors = result.error.flatten().fieldErrors;
return res.status(400).json({ errors });
}
const user = result.data; // fully typed User
A good rule: parse() for things that should never be wrong (app config at startup), safeParse() for external data where failures are expected and recoverable.
Zod isn't just validation — it can transform data during parsing. This replaces the separate "validate then map" pattern:
const UserSchema = z.object({
// Coerce string → Date automatically
created_at: z.string().datetime().transform(s => new Date(s)),
// Normalize email to lowercase
email: z.string().email().transform(s => s.toLowerCase()),
// Convert cents to dollars
balance_cents: z.number().int().transform(cents => cents / 100),
});
// result.data.created_at is a Date, not a string
const result = UserSchema.safeParse(rawApiData);
When your API returns objects that vary by a type field, Zod's discriminatedUnion gives you precise validation and exhaustive type checking:
const ClickEventSchema = z.object({
type: z.literal('click'),
x: z.number(),
y: z.number(),
});
const KeypressEventSchema = z.object({
type: z.literal('keypress'),
key: z.string(),
ctrlKey: z.boolean(),
});
const DomEventSchema = z.discriminatedUnion('type', [
ClickEventSchema,
KeypressEventSchema,
]);
type DomEvent = z.infer<typeof DomEventSchema>;
// Narrowing works exactly like with manually-written TypeScript unions
function handleEvent(event: DomEvent) {
if (event.type === 'click') {
console.log(event.x, event.y);
}
}
Use discriminatedUnion instead of union when possible — it's faster (Zod checks the discriminator first) and gives better error messages.
Zod is the most popular validation library for React Hook Form via the @hookform/resolvers package:
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
// Generate this schema from your JSON sample, then refine it
const CheckoutSchema = z.object({
email: z.string().email('Invalid email address'),
cardNumber: z.string().regex(/^\d{16}$/, 'Must be 16 digits'),
expiryMonth: z.number().int().min(1).max(12),
expiryYear: z.number().int().min(new Date().getFullYear()),
});
type CheckoutForm = z.infer<typeof CheckoutSchema>;
export function CheckoutForm() {
const { register, handleSubmit, formState: { errors } } = useForm<CheckoutForm>({
resolver: zodResolver(CheckoutSchema),
});
// errors.email.message, errors.cardNumber.message — all typed
}
// app/api/users/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { z } from 'zod';
const CreateUserSchema = z.object({
name: z.string().min(2).max(50),
email: z.string().email(),
role: z.enum(['admin', 'user', 'guest']).default('user'),
});
export async function POST(req: NextRequest) {
const body = await req.json();
const result = CreateUserSchema.safeParse(body);
if (!result.success) {
return NextResponse.json(
{ error: result.error.flatten().fieldErrors },
{ status: 400 }
);
}
// result.data is fully typed — safe to use in DB queries
const user = await db.users.create({ data: result.data });
return NextResponse.json(user, { status: 201 });
}
const UserSchema = z.object({
id: z.string().uuid(),
name: z.string().min(1),
email: z.string().email(),
bio: z.string().optional(), // field may be absent
deletedAt: z.string().nullable(), // field present but may be null
nickname: z.string().nullish(), // absent OR null (optional + nullable)
});
// For PATCH endpoints — all fields optional
const UpdateUserSchema = UserSchema.partial();
// For bulk operations — pick only what's needed
const UserPreviewSchema = UserSchema.pick({ id: true, name: true });
z.infer: If you define a Zod schema and a separate TypeScript interface manually, they can drift. Always derive the type: type User = z.infer<typeof UserSchema>..passthrough(): It allows extra fields through without validation. Only use it when you intentionally want to forward unknown keys.z.coerce for query params: URL search params are always strings. z.number() will fail on "42" — use z.coerce.number() instead.AddressSchema, ContactSchema) and compose them.Valibot offers a modular architecture with much smaller bundle size through tree-shaking (ideal for client-side code). Joi is battle-tested in Node.js but doesn't generate TypeScript types natively. Yup is popular with Formik but has weaker TypeScript inference than Zod. ArkType aims for near-zero runtime overhead with a concise syntax. Zod wins on developer experience and ecosystem integration — it's the default choice for most TypeScript projects in 2024.
Q: Does Zod affect bundle size?
A: Zod v3 is around 12kb minified + gzipped. For server-side code (Next.js API routes, Node.js), size is irrelevant. For client-side bundles, Valibot is the tree-shakeable alternative.
Q: Can I use Zod with existing TypeScript interfaces?
A: The recommended approach is to define the Zod schema first and infer the TypeScript type from it. Going the other direction (interface → Zod) requires third-party tools like ts-to-zod and is fragile with complex types.
Q: How do I handle optional fields?
A: .optional() means the key may be absent. .nullable() means it can be null. .nullish() means either. These map directly to TypeScript: T | undefined, T | null, and T | null | undefined.
Q: What's the difference between z.union and z.discriminatedUnion?
A: z.union tries each schema in order until one succeeds — O(n) in the worst case. z.discriminatedUnion uses a known discriminator field to jump directly to the matching schema — O(1). Always prefer discriminatedUnion when your variants have a common type or kind field.
Q: How do I show Zod validation errors in a form?
A: error.flatten() returns { fieldErrors: { fieldName: string[] } } — easy to map to form field error messages. With React Hook Form + zodResolver, this is handled automatically.
Q: Can Zod validate arrays at the top level (not inside an object)?
A: Yes: const TagsSchema = z.array(z.string().min(1)).min(1).max(20). This validates an array directly, not wrapped in an object.
Is the processing local-only?
Absolutely. TypeMorph operates entirely within your browser's sandbox. We use Web Workers for high-performance computation without ever transmitting your JSON, SQL, or API data to a remote server.
Can I use this for enterprise projects?
Yes. The tool is designed for professional software engineers who require GDPR compliance and data privacy. It is trusted by developers at top-tier startups and financial institutions.
Why pasting proprietary company data into third-party web tools is a major liability, and how to stay safe.
A deep dive into combining Zod, React Query, and TypeScript for bulletproof API integration.
Code generation is just the beginning. Discover how a schema-first approach can eliminate 90% of your integration bugs.