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

An object → z.object(), named from the file
{"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>;
A key missing in some items → .optional()
[{"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>;
null in some items → .nullable()
[{"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>;
An RFC 3339 string → .datetime(), a number written whole → .int()
{"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.

unstringify for VS Code14-day trial · Free and Pro

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.

Try it on the file that broke your afternoon.

free · no signup