A single Zod schema gives you a static TypeScript type and a runtime validator. One source of truth. A class of bugs gone.

When a TypeScript type meets untrusted JSON - from a form, a third-party API, an LLM response - the type is a comment. The runtime knows nothing about it. The type checker can't reach past the network. So we end up with two definitions for the same shape: the type, and a hand-rolled validator. They drift. Bugs follow.
Zod fixes this by making one definition produce both.
The pattern
The schema is the type. The schema is the validator. There is no second source.
import { z } from "zod";
export const caseStudyFrontmatter = z.object({
title: z.string().min(3),
slug: z.string().regex(/^[a-z0-9-]+$/, "kebab-case"),
oneLine: z.string().min(10).max(200),
role: z.string(),
stack: z.array(z.string()).min(1),
liveUrl: z.string().url().optional(),
order: z.number().int().nonnegative(),
});
export type CaseStudyFrontmatter = z.infer<typeof caseStudyFrontmatter>;The static type comes from z.infer. The runtime validator comes from
.parse(). They cannot drift, because they are the same thing.
Where this earns its keep
Three places. All three are zones where untrusted JSON meets your types.
1. LLM output
You ask Gemini to return JSON. Gemini complies - sometimes. Sometimes it returns prose. Sometimes it returns JSON with one field misnamed. A schema catches all of these and lets you retry with a corrective system message before the bad data touches your domain logic.
2. Form payloads
A <form> is hostile input by default. Zod runs the same check on the server
that ran on the client, so a malicious actor sending a hand-crafted POST gets
the same rejection a real user gets - without a second hand-rolled validator.
3. Build-time content
MDX frontmatter is just YAML. YAML is not typed. A schema turns it into a typed value at build time, with line-and-column errors when a post is malformed.
A small but real example
Suppose your blog accepts cross-posts. Some posts are originals; some are republished from dev.to with a canonical pointing back. The schema describes both states clearly:
const post = z.object({
title: z.string().min(3),
externalCanonical: z.string().url().optional(),
syndicatedTo: z.array(
z.object({
platform: z.enum(["devto", "medium", "hashnode"]),
url: z.string().url(),
})
).optional(),
});If externalCanonical is set, your sitemap excludes the post (SEO weight goes
elsewhere). If syndicatedTo is set, your post page renders an "Also
published on" footer. These behaviors aren't documented in a comment - they're
enforced by the schema and consumed by typed code.
zod.dev
Zod - the docs
Comprehensive reference for the schema-as-validator-as-type pattern.
The takeaway
Treat schemas as contracts, not comments. Make the type and the validator the same artifact. Let the build break loudly when content is malformed. You'll remove a category of bug - drift between what your types claim and what your runtime accepts - that no amount of testing will fully cover.
Also published on

