Skip to content

2026-08-10 · 8 min read

Typing JSON in TypeScript: samples, Zod, and trust boundaries

Why inferred interfaces from sample JSON are a starting point, not a contract, and how Zod (or another parser) belongs at every trust boundary.

JSON has types. TypeScript does not see them until you say so

`JSON.parse` returns `any` in older typings and `unknown` if you tighten it. Either way, the compiler does not know that `data.user.email` is a string. You can assert `as User` and hope. That works until a field is missing, a number arrives as a string, or an extra property carries an unexpected meaning. Sample-based generation (JSON → interface) is the fastest way to get a compiling client from a real payload. It is also how teams accidentally freeze a single happy-path response as the API.

Infer from arrays, not from one object

If you paste one user object, every key looks required. If you paste an array of users from production, optional fields show themselves. PureDevKit merges object shapes in arrays and marks missing keys optional. Unions appear when a field is sometimes a string and sometimes null. That is still only the sample. Production will invent a third shape on a Friday. Generated types belong in a PR with a note: “from fixture X, tighten before shipping.”

Zod at the boundary

Compile-time types disappear at runtime. Anything crossing a trust boundary — HTTP, `localStorage`, query strings, `postMessage` — should be parsed. Zod, Valibot, ArkType, and similar libraries fail loud on bad data and give you `z.infer<typeof Schema>` for the compiler. Generate a Zod schema from JSON when you need a head start, then add `.email()`, `.uuid()`, min/max, and branded IDs. Share schemas between client and server if you can; generate from OpenAPI if you already have it. Do not `as` your way through `fetch`.

Dates, IDs, and wire vs domain

JSON has no Date type. ISO strings should stay strings on the wire and become `Date` only inside your app. IDs that are numbers in JSON will collide with other numbers in TypeScript unless you brand them. Extra properties are stripped or rejected depending on your parser config; decide explicitly. The JSON formatter on PureDevKit is the place to clean a payload; the converter is the place to sketch types; your repo is the place to make them true.

Related tools