TypeScript’s type system catches errors at compile time, but API responses arrive at runtime. Zod bridges the gap: it validates the shape and types of runtime data and infers TypeScript types from the same schema. Writing Zod schemas by hand for complex JSON structures is slow. A JSON to Zod generator turns any JSON sample into a complete, editable schema in seconds.

What Is Zod?

Zod is a TypeScript-first schema declaration and validation library. Unlike JSON.parse() which returns any, Zod:

  • Validates that data matches an expected shape
  • Infers TypeScript types from schemas (no duplicate type definitions)
  • Produces detailed, structured error messages
  • Runs in both Node.js and the browser
import { z } from 'zod'

const UserSchema = z.object({
  id: z.number(),
  name: z.string(),
  email: z.string().email(),
  role: z.enum(['admin', 'user', 'guest'])
})

type User = z.infer<typeof UserSchema>
// Equivalent to: { id: number; name: string; email: string; role: 'admin' | 'user' | 'guest' }

const result = UserSchema.safeParse(apiResponse)
if (result.success) {
  console.log(result.data.name) // typed as string
} else {
  console.error(result.error.issues)
}

JSON to Zod: How the Conversion Works

Given a JSON sample:

{
  "id": 42,
  "name": "Alice",
  "email": "[email protected]",
  "age": 28,
  "active": true,
  "tags": ["admin", "beta"],
  "address": {
    "street": "123 Main St",
    "city": "Portland",
    "zip": "97201"
  },
  "metadata": null
}

The generator produces:

import { z } from 'zod'

const Schema = z.object({
  id: z.number(),
  name: z.string(),
  email: z.string(),
  age: z.number(),
  active: z.boolean(),
  tags: z.array(z.string()),
  address: z.object({
    street: z.string(),
    city: z.string(),
    zip: z.string()
  }),
  metadata: z.null()
})

export type Schema = z.infer<typeof Schema>

The schema is a starting point. Refine it based on your actual business rules (see the section below).

Where to Use Zod Validation

API Response Validation

// Without Zod — any typing, no runtime guarantees
const user = (await fetch('/api/user').then(r => r.json())) as User

// With Zod — validated and typed
const UserSchema = z.object({ id: z.number(), name: z.string() })

async function fetchUser(id: number) {
  const raw = await fetch(`/api/users/${id}`).then(r => r.json())
  return UserSchema.parse(raw) // throws ZodError if shape doesn't match
}

tRPC and Next.js API Routes

// tRPC procedure input validation
import { z } from 'zod'
import { publicProcedure } from '../trpc'

const createUserInput = z.object({
  name: z.string().min(1).max(100),
  email: z.string().email(),
  role: z.enum(['admin', 'user']).default('user')
})

export const createUser = publicProcedure
  .input(createUserInput)
  .mutation(async ({ input }) => {
    // input is typed as { name: string; email: string; role: 'admin' | 'user' }
    return db.users.create(input)
  })

Form Validation with React Hook Form

import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { z } from 'zod'

const formSchema = z.object({
  username: z.string().min(3, 'At least 3 characters'),
  password: z.string().min(8, 'At least 8 characters'),
  confirmPassword: z.string()
}).refine(data => data.password === data.confirmPassword, {
  message: "Passwords don't match",
  path: ['confirmPassword']
})

type FormData = z.infer<typeof formSchema>

function SignupForm() {
  const { register, handleSubmit, formState: { errors } } = useForm<FormData>({
    resolver: zodResolver(formSchema)
  })
  // ...
}

Environment Variable Validation

import { z } from 'zod'

const envSchema = z.object({
  DATABASE_URL: z.string().url(),
  PORT: z.coerce.number().int().positive().default(3000),
  NODE_ENV: z.enum(['development', 'production', 'test']),
  API_SECRET: z.string().min(32)
})

export const env = envSchema.parse(process.env)
// Crashes at startup with a clear error if required env vars are missing or malformed

Refining Generated Schemas

The generator infers types from JSON values, but JSON can’t express all Zod constraints. Add these manually:

String Formats

// Generated
email: z.string()

// Refined
email: z.string().email()
url: z.string().url()
uuid: z.string().uuid()
isoDate: z.string().datetime()

Numeric Constraints

// Generated
age: z.number()

// Refined
age: z.number().int().min(0).max(150)
price: z.number().positive()
quantity: z.number().int().nonnegative()

Optional vs Required Fields

JSON samples only show values that exist. If a field can be absent:

// Generated (field existed in sample)
nickname: z.string()

// Refined
nickname: z.string().optional()        // undefined is allowed
nickname: z.string().nullable()        // null is allowed
nickname: z.string().nullish()         // null or undefined allowed

Unions and Enums

When a field can hold multiple types:

// status was "active" in the sample
status: z.string()

// Refined after examining all possible values
status: z.enum(['active', 'inactive', 'pending', 'archived'])

// Or a union with different shapes
result: z.union([
  z.object({ success: z.literal(true), data: DataSchema }),
  z.object({ success: z.literal(false), error: z.string() })
])

Array Items with Mixed Types

// Mixed array like [1, "hello", true]
items: z.array(z.union([z.number(), z.string(), z.boolean()]))

Handling null Values in JSON Samples

When a JSON field is null, the generator produces z.null(). In practice, null usually means the field is nullable, not always-null:

// Generated
metadata: z.null()

// More useful
metadata: z.string().nullable()        // string | null
metadata: z.record(z.unknown()).nullable()  // object | null

parse vs safeParse

Zod provides two parse methods with different error handling:

MethodOn failureReturns
.parse(data)Throws ZodErrorThe validated data
.safeParse(data)Returns { success: false, error }{ success, data | error }

Use .parse() at startup (env vars, config) where failure should crash. Use .safeParse() for API responses and user input where you want to handle errors gracefully.

// parse — throws on failure
const config = ConfigSchema.parse(process.env)

// safeParse — handle the error
const result = UserSchema.safeParse(apiResponse)
if (!result.success) {
  // result.error.issues contains structured error details
  return { error: result.error.format() }
}
// result.data is fully typed
return result.data

Try the Tool

Generate Zod schemas from JSON instantly →

Paste any JSON object—from an API response, a Postman collection, a Swagger example—and get a working Zod schema. Use it as a starting point, then add constraints specific to your domain.

Works with nested objects, arrays, null values, and mixed-type arrays. Output includes the z.infer type export so you have a TypeScript type without writing a separate interface.