I ran one API response through two JSON-to-Zod converters. One silently turned every field into z.string().

javascript dev.to

You have an API response. You want a Zod schema. So you paste the JSON into a JSON-to-Zod converter, copy the output, and ship it.

Here's the trap: a lot of those converters infer basic types only. Your email, your uuid, your url, your ISO timestamp — they all come out as z.string(). The schema compiles, the types look right, and your validator quietly accepts "not-an-email", "ftp://nope", and "2026-99-99" forever.

I wanted to see exactly how much gets lost, so I ran the same payload through two tools and diffed the output. Everything below is real, copy-pasteable output — nothing edited.

The input

A pretty ordinary user object:

{"id":"3f2a9c1e-5b7d-4e8a-9f1c-2d3e4f5a6b7c","email":"ada@example.com","website":"https://ada.dev","age":34,"rating":4.7,"created_at":"2026-03-04T10:15:30Z","is_active":true,"address":{"city":"Lyon","zip":"69001"},"tags":["early-adopter","beta"]}
Enter fullscreen mode Exit fullscreen mode

Tool 1 — json-to-zod (npm)

const user = z.object({
  id: z.string(),
  email: z.string(),
  website: z.string(),
  age: z.number(),
  rating: z.number(),
  created_at: z.string(),
  is_active: z.boolean(),
  address: z.object({ city: z.string(), zip: z.string() }),
  tags: z.array(z.string()),
});
Enter fullscreen mode Exit fullscreen mode

Structurally correct. But every meaningful field is a bare z.string() / z.number(). This schema will happily validate email: "lol" and created_at: "yesterday".

Tool 2 — TypeMorph

import { z } from "zod";

export const userAddressSchema = z.object({
  city: z.string(),
  zip: z.string().regex(/^[A-Z0-9][A-Z0-9\s\-]{1,8}[A-Z0-9]$/i),
});
export type UserAddress = z.infer<typeof userAddressSchema>;

export const userSchema = z.object({
  id: z.uuid(),
  email: z.email(),
  website: z.url(),
  age: z.number().int().min(0).max(150),
  rating: z.number().min(0).max(5),
  created_at: z.iso.datetime(),
  is_active: z.boolean(),
  address: userAddressSchema,
  tags: z.array(z.string()),
});
export type User = z.infer<typeof userSchema>;
Enter fullscreen mode Exit fullscreen mode

The honest diff

Field Value json-to-zod TypeMorph
id a UUID z.string() z.uuid()
email an email z.string() z.email()
website a URL z.string() z.url()
created_at ISO datetime z.string() z.iso.datetime()
age 34 z.number() z.number().int()…

Two things worth being honest about:

  1. The format detection is real and value-derived. z.uuid(), z.email(), z.url(), z.iso.datetime() come from looking at the actual values — they're facts, not guesses.
  2. The numeric ranges and the zip regex are name-based guesses. age.min(0).max(150), rating.min(0).max(5) are inferred from field names, not values, so they can be wrong (a rating could be 0–10). TypeMorph lets you turn that "smart" layer off and keep only the facts. I'd rather it be upfront about which is which than pretend a guess is a fact.

Why this matters

A Zod schema's whole job is to reject bad data. z.string() where you meant z.email() is a validator that passes the exact inputs you wrote it to catch. You get the feeling of validation with none of the coverage — which is worse than no schema, because you stop checking.

When json-to-zod is still fine

It's a tiny, zero-dependency function you can call inside your own code. If you just want a rough scaffold and you'll add .email() / .uuid() / enums by hand, it does the job. It's also basically unmaintained at this point (last publish was years ago), so don't expect Zod v4 output or fixes.

Try the comparison yourself

npx typemorph-cli zod your-response.json --root User
Enter fullscreen mode Exit fullscreen mode

I'm building TypeMorph in public — a JSON/OpenAPI → Zod/TypeScript/Go/Prisma converter that tries to detect what your data actually is instead of dumping everything to z.string(). Feedback (and "your inference got this wrong") very welcome.

Source: dev.to

arrow_back Back to Tutorials