← Back to UltraToolkit | All Posts

JSON to Zod: Why Every TypeScript Developer Needs Runtime Validation

Why TypeScript types alone are not enough β€” and how Zod schema validation bridges the gap between compile-time safety and runtime data integrity.

TypeScript catches type errors at compile time. But APIs, databases, and user forms return data at runtime β€” where TypeScript's static guarantees simply do not apply. Zod fills this critical gap.

The Compile-Time vs Runtime Gap

You define a TypeScript interface for an API response. The interface says userId is a number. At runtime, the API returns userId as a string. TypeScript cannot catch this. Your application crashes or silently processes wrong data. This is the gap Zod closes.

What Zod Adds

Runtime validation: data is checked against the schema when it arrives, not when the code is compiled. Detailed error messages: when data fails validation, Zod tells you exactly which field failed and why. TypeScript type inference: z.infer extracts the TypeScript type from the schema automatically β€” no duplicate type definitions.

Convert any JSON object to a complete Zod schema in seconds with the JSON to Zod Converter. Paste an API response and get production-ready TypeScript instantly β€” including email, URL, and date detection.

The Three Killer Integrations

tRPC uses Zod schemas for procedure input/output β€” one schema serves TypeScript inference, runtime validation, and API documentation. React Hook Form with zodResolver connects form validation directly to a Zod schema. Next.js API routes validate request bodies against Zod schemas before processing.

What Zod Actually Does β€” Beyond the Basics

Zod is a TypeScript-first schema declaration and validation library. At its simplest, you define a schema once and use it for two purposes simultaneously: validating runtime data (does this API response actually match what we expect?) and inferring TypeScript types (what TypeScript type does this data have?). This dual purpose is what makes Zod genuinely valuable rather than just another validation library β€” it eliminates the duplication between your TypeScript interfaces and your validation logic.

Consider a typical TypeScript API integration without Zod. You define a TypeScript interface describing the expected API response shape. You write validation logic separately to check that the response actually has the expected fields. You maintain both in sync as the API changes. With Zod, you define the schema once. TypeScript types are automatically inferred from it using z.infer. Validation runs at runtime against the actual data. One source of truth serves all three needs.

Core Schema Primitives

Zod's primitive schemas correspond to TypeScript's primitive types. z.string() validates strings and accepts chainable refinements: .min(n), .max(n), .length(n), .email(), .url(), .uuid(), .regex(pattern). z.number() validates numbers: .int(), .positive(), .negative(), .min(n), .max(n). z.boolean() validates booleans. z.date() validates Date objects. z.undefined(), z.null(), z.any(), z.unknown() cover edge cases.

Object schemas use z.object({}) with a shape describing each field. By default, all fields are required. Mark optional fields with .optional() or .nullable(). Nest object schemas inside each other for hierarchical data structures. Arrays of any type use z.array(schema). Tuples with specific types at specific positions use z.tuple([schema1, schema2, ...]). Union types β€” value can be one of several schemas β€” use z.union([schema1, schema2]) or the shorter .or() syntax.

Runtime Validation in Practice

The most common use of Zod is validating data at application boundaries β€” places where data enters your application from external sources. API responses are the primary case: after fetching data from an external API, parse it through a Zod schema before using it in your application. Form submissions are another: validate user input on both client (immediate feedback) and server (security) sides using the same schema. Environment variables at application startup: validate that all required environment variables are present and correctly typed before the application starts serving requests.

The parse vs safeParse distinction is important for production code. schema.parse(data) throws a ZodError if validation fails β€” appropriate for cases where failure is truly exceptional, like validating an application's own configuration. schema.safeParse(data) returns {success: true, data: parsedData} or {success: false, error: ZodError} β€” appropriate for validating user input or external API data where failure is expected and should be handled gracefully rather than crashing the application.

Integration with tRPC and Next.js

tRPC, the end-to-end typesafe API library for TypeScript, uses Zod as its schema system for defining procedure inputs and outputs. A tRPC procedure with a Zod input schema gets full TypeScript type safety from client to server without writing any additional type definitions. The Zod schema serves as both the runtime validator and the TypeScript type source simultaneously. This is the most compelling demonstration of Zod's dual-purpose value.

In Next.js API routes, validate the request body at the top of every handler using schema.safeParse(req.body). Return a 400 error with the Zod error details if validation fails. Process the validated data knowing it matches your expected types. This pattern eliminates an entire category of runtime errors from your API handlers and provides clear error messages to API clients when they send incorrectly formatted requests.

Error Messages and User Experience

ZodError contains an issues array where each issue has a path (the location of the error in the data structure), a code (the type of error), and a message (human-readable description). The path is particularly useful β€” issues[0].path = ['address', 'postcode'] immediately tells you exactly which nested field failed validation. For form validation, error.flatten() converts the nested error structure into a flat object where field paths map to arrays of error messages β€” the format most form libraries expect.

Custom error messages override Zod's defaults using the message option in refinements or as a second argument to refine(). Internationalised error messages can be provided by replacing the default error map using z.setErrorMap(). For applications with specific user experience requirements around error messages β€” consistent tone, specific vocabulary, translated messages β€” the error map is the right extension point rather than manipulating ZodError instances after the fact.

Generate a starting Zod schema from any JSON object with the JSON to Zod Schema Converter. Paste an API response and get production-ready TypeScript in seconds.

Zod's true value becomes apparent when you use it consistently across an entire application. Define your schemas in a dedicated schemas/ directory or alongside the code that uses them. Import and reuse schemas across your application β€” a UserSchema defined once serves as the basis for API validation, form validation, and TypeScript type inference everywhere the concept of a User appears. When the User definition changes (a new required field, a renamed property, a changed type), updating the schema immediately surfaces every location in the codebase that is affected through TypeScript's type system. This propagation of change through the type system is the productivity benefit that makes Zod worthwhile.

The community around Zod has produced excellent extensions. zod-to-json-schema converts Zod schemas to JSON Schema format, enabling integration with OpenAPI documentation generation and validation libraries that expect JSON Schema. @hookform/resolvers/zod provides the zodResolver for React Hook Form integration. drizzle-zod generates Zod schemas automatically from Drizzle ORM table definitions. These integrations reduce the amount of manual schema writing required and keep Zod schemas in sync with other sources of truth automatically.

Open JSON to Zod Converter

Free, browser-based, no signup.

Convert JSON to Zod →
← Back to UltraToolkit All Posts →