Tools · Zod

# JSON to Zod schema

Paste JSON and get the z.object() that accepts it — one schema per nested object, .nullable() where the sample has null, the inferred type at the end. Zod 3 syntax, nothing to set.

no signup · runs entirely in your browser · ⌘V works anywhere on this page

## What you get

- Writes the API-response sample as zod 3 · 7 schemas · 34 fields
- Puts every nested object in its own z.object(), children before the schema that names them, and the root last with export type X = z.infer<typeof X>
- .nullable() where the sample has null, .optional() where a key is missing from some elements — the paginated list marks failure_message optional
- .datetime() and .int() only when every value seen earns them: one plain string, or one number written with a point — 149.00 — and the field is z.string() or z.number()
- Names the root after the file — clean-order-example.json → CleanOrderExample — and a list’s element from its key: items → Item

## What it will not do

- Write Zod 4, Valibot or io-ts. Zod 3 syntax — z.string().datetime(), z.number().int() — which Zod 4 still reads.
- Add .min(), .email(), .url() or an enum. A string is a string until you say otherwise.
- Guess a null’s type beyond string: a field that is only ever null is z.string().nullable(), said so here.
- Send your text anywhere. The page is static and the engine runs in this tab — open the network panel and watch it stay empty.

## What you get

{"id": 1, "status": "paid"}import { z } from "zod"; export const Order = z.object({ id: z.number().int(), status: z.string(), }); export type Order = z.infer<typeof Order>;

[{"id":1,"note":"x"},{"id":2}]import { z } from "zod"; export const OrderItem = z.object({ id: z.number().int(), note: z.string().optional(), }); export const Order = z.array(OrderItem); export type Order = z.infer<typeof Order>;

[{"id":1,"ship":null},{"id":2,"ship":"air"}]import { z } from "zod"; export const OrderItem = z.object({ id: z.number().int(), ship: z.string().nullable(), }); export const Order = z.array(OrderItem); export type Order = z.infer<typeof Order>;

{"created_at": "2026-04-21T14:32:08Z", "qty": 2, "total": 149.00}import { z } from "zod"; export const Order = z.object({ created_at: z.string().datetime(), qty: z.number().int(), total: z.number(), }); export type Order = z.infer<typeof Order>;

total stays z.number(): 149.00 was written with a point, whatever it parses to.

## The sample

What the first sample opens — a real file, not an illustration written for this page.

clean-order-example.json → Zod, as Convert writes it — zod 3 · 7 schemas · 34 fields · 1.2 kb

```
import { z } from "zod";

export const Customer = z.object({
  id: z.string(),
  email: z.string(),
  name: z.string(),
  verified: z.boolean(),
  loyalty_tier: z.string(),
});

export const Item = z.object({
  sku: z.string(),
…
```

## in VS Code

Convert, then Zod — the schemas land in a new .ts editor beside the JSON.

## Questions

### Zod 3 or Zod 4?

Zod 3 syntax. Zod 4 reads z.string().datetime() and z.number().int() as written; only the deprecation notes differ.

### Why is the root schema last?

A const has to be declared before another refers to it, so each z.object() lands after the ones it uses. The root names them all, so it comes last — and the file type-checks as written.

### What happens when a field is only ever null?

It is z.string().nullable(). A lone null says nothing about the value, so this is the one guess the schema makes — and it guesses string.

## Behind this page

## Try it on the file that broke your afternoon.

free · no signup

---

Canonical: https://unstringify.com/tools/json-to-zod
