New blogMaking an AI Agent You Can Trust With Money

★ Zod · from JSON

JSON to Zod Converter

Zod gives you the TypeScript type and the runtime check from one definition. Paste a response and get schemas in dependency order, each with its z.infer type, plus format checks for the strings that look like emails, UUIDs, URLs and timestamps.

  • Runs in your browser
  • No signup

Strictness

Your JSON

Zod

3 types · 16 fields
import { z } from "zod";

export const TeamSchema = z.object({
  id: z.string().uuid(),
  name: z.string(),
  website: z.string().url(),
});
export type Team = z.infer<typeof TeamSchema>;

export const InvoiceSchema = z.object({
  number: z.string(),
  amount: z.number(),
  paid: z.boolean(),
  paidAt: z.string().datetime({ offset: true }).nullable(),
  note: z.string().optional(),
});
export type Invoice = z.infer<typeof InvoiceSchema>;

export const RootSchema = z.object({
  id: z.number().int(),
  email: z.string().email(),
  "display-name": z.string(),
  createdAt: z.string().datetime({ offset: true }),
  plan: z.null(),
  tags: z.array(z.string()),
  team: TeamSchema,
  invoices: z.array(InvoiceSchema),
});
export type Root = z.infer<typeof RootSchema>;

Typing APIs by hand?

I set up end-to-end type safety (DB → API → UI) so this never happens again.

Fix my types
01

How the Zod mapping works

Each JSON value is read, merged with every other sample of the same key, and then mapped to the closest Zod type:

JSON to Zod mapping
In your JSONZod
stringz.string()
"2026-11-07T09:30:00Z"z.string().datetime({ offset: true })
"ada@example.com" / uuid / https URL.email() / .uuid() / .url()
integerz.number().int()
decimalz.number()
null alongside a value.nullable()
key missing in some samples.optional()
nested objectits own Schema constant
mixed valuesz.union([...])
02

Worked example

A user record with a nested team, a list of invoices (one missing a note, one with a null payment date) and an awkward display-name key:

{
  "id": 1042,
  "email": "ada@example.com",
  "display-name": "Ada",
  "createdAt": "2026-11-07T09:30:00Z",
  "plan": null,
  "tags": ["beta", "founder"],
  "team": {
    "id": "7f3c9a2e-1b4d-4c8e-9f2a-6d5e4c3b2a19",
    "name": "Analytical Engines",
    "website": "https://example.com"
  },
  "invoices": [
    { "number": "INV-001", "amount": 49.5, "paid": true, "paidAt": "2026-10-01T00:00:00Z" },
    { "number": "INV-002", "amount": 49, "paid": false, "paidAt": null, "note": "Card declined" }
  ]
}

Becomes this Zod, generated by the same engine that runs the converter above (3 types, 16 fields):

import { z } from "zod";

export const TeamSchema = z.object({
  id: z.string().uuid(),
  name: z.string(),
  website: z.string().url(),
});
export type Team = z.infer<typeof TeamSchema>;

export const InvoiceSchema = z.object({
  number: z.string(),
  amount: z.number(),
  paid: z.boolean(),
  paidAt: z.string().datetime({ offset: true }).nullable(),
  note: z.string().optional(),
});
export type Invoice = z.infer<typeof InvoiceSchema>;

export const RootSchema = z.object({
  id: z.number().int(),
  email: z.string().email(),
  "display-name": z.string(),
  createdAt: z.string().datetime({ offset: true }),
  plan: z.null(),
  tags: z.array(z.string()),
  team: TeamSchema,
  invoices: z.array(InvoiceSchema),
});
export type Root = z.infer<typeof RootSchema>;
03

Zod gotchas

.optional() vs .nullable()

.optional() accepts undefined (key absent). .nullable() accepts null. A key that is sometimes missing and sometimes null needs both: .nullable().optional().

Datetime offsets

z.string().datetime() rejects "+05:30" offsets by default. The generated code passes { offset: true } so real-world timestamps validate.

Strict or not

z.object() strips unknown keys by default. Add .strict() if extra keys should be an error, or .passthrough() to keep them.

04

Questions developers actually ask

Why are schemas ordered children first?
A schema constant must exist before another schema references it, so nested schemas come first and the root comes last.
How do I validate an API response with this?
Call RootSchema.parse(data) to throw on bad data, or RootSchema.safeParse(data) to get a result object you can branch on.
Is the email check strict?
Zod's .email() is a reasonable format check, not proof the address exists. Drop it if your data has unusual addresses you still want to accept.
Is my JSON sent to a server?
No. Parsing and type generation run in your browser, so you can paste real API responses with tokens or customer data. Nothing leaves the tab.
How does it know a field is optional?
Paste an array of objects, or a response that contains one. Every element is read and merged: keys missing from some elements become optional, and keys that are sometimes null become nullable.
05

Other languages

06

More from the workbench

Fine print

Tools: on the house.
Engineer: by the hour.

A codebase that doesn't need this tool is the upgrade. I set up end-to-end type safety so your API, database and UI agree by construction.

I'm Himanshu, an AI and full-stack engineer. I build MVPs, AI agents and the growth plumbing around them for early-stage startups.