On this page
An API response can be valid JSON and still be unusable. A price arrives as text, a required identifier disappears, or a status value changes without warning. TypeScript cannot catch those changes after compilation because its types do not inspect network traffic. A runtime schema makes that boundary explicit.
Put validation where data enters your application
Treat incoming JSON as unknown data until it passes a check. Writing a type assertion after response.json only changes the compiler's view; it does not examine the returned object. Validate once near the network boundary, then pass trusted values into application code rather than scattering defensive checks across components.
Use the JSON to TypeScript tool to explore a representative response and sketch its structure. Generated interfaces are a useful starting point, but one sample cannot reveal optional fields, complete enum values, or business constraints. Turn that sketch into a schema checked against the API contract and several real examples.
This workflow complements the broader material in JavaScript and TypeScript: static types describe code relationships, while runtime validation checks actual values. Neither replaces the other. Start with the fields your application consumes, and decide explicitly what should happen when the provider adds new ones.
Define a schema and infer the trusted type
Zod schemas are executable validators. They can check primitive values, object shapes, arrays, unions, and constraints such as minimum lengths. Infer the TypeScript type from the schema so that the validation rule and the trusted application type share a source of truth.
import { z } from "zod";
const ProductSchema = z.object({
id: z.string().min(1),
name: z.string().min(1),
priceCents: z.number().int().nonnegative(),
currency: z.enum(["USD", "EUR", "GBP"]),
imageUrl: z.url().nullable(),
});
const ProductListSchema = z.object({
items: z.array(ProductSchema),
nextCursor: z.string().nullable(),
});
type Product = z.infer<typeof ProductSchema>;
type ProductList = z.infer<typeof ProductListSchema>;These examples use Zod 4 syntax; check your installed major version before copying them. The official Zod documentation describes available validators and migration details. An integer price avoids accidental assumptions about decimal arithmetic, but the allowed currencies and monetary representation still need agreement with the server.
A nullable value accepts null. An optional value accepts undefined, including an omitted property. They are not interchangeable. If nextCursor must always exist but may indicate the end of pagination with null, keep it nullable rather than silently making it optional. This distinction makes missing fields visible instead of hiding contract drift.
Separate transport, decoding, and schema failures
A robust request has several failure boundaries: the network can fail, the status can be unsuccessful, JSON decoding can throw, and the decoded value can violate the schema. Report them separately so operators and interface code can respond appropriately.
class InvalidProductResponse extends Error {
constructor() {
super("Product service returned an invalid payload");
this.name = "InvalidProductResponse";
}
}
async function getProducts(signal?: AbortSignal): Promise<ProductList> {
const response = await fetch("/api/products", { signal });
if (!response.ok) {
throw new Error("Product request failed: " + response.status);
}
const payload: unknown = await response.json();
const result = ProductListSchema.safeParse(payload);
if (!result.success) {
console.warn("Product schema mismatch", result.error.issues.map(
issue => ({ path: issue.path, code: issue.code })
));
throw new InvalidProductResponse();
}
return result.data;
}safeParse returns a discriminated result instead of throwing for ordinary validation failures. parse is equally valid when your surrounding error handling expects exceptions. Neither converts failed HTTP statuses into network errors; the explicit status check still matters, as the Fetch guide explains.
Avoid logging complete payloads by default. Responses can contain personal data or credentials. Even schema paths may contain sensitive dynamic keys, so adapt telemetry to your data policy. Show users a stable recovery message rather than raw validation internals, and preserve enough structured diagnostics for maintainers to identify the failing endpoint.
Choose strictness and transformations deliberately
By default, Zod object schemas strip unrecognized keys from parsed output. Use strictObject when unexpected properties should fail, or looseObject when unknown fields should be retained. Unknown-key policy is an architectural choice: strictness helps detect internal contract mistakes, while tolerant parsing often suits independently evolving third-party APIs.
Do not assume the original payload and parsed output are identical. Defaults, coercion, transformations, and unknown-key handling can change the result. With transforms, z.input describes accepted input and z.output describes parsed output; z.infer corresponds to the output type.
Be especially careful with coercion. Converting a value to a number can turn an empty string into zero. Boolean coercion follows JavaScript truthiness, so the string false is truthy rather than the boolean false. Prefer an explicit accepted-value mapping when external strings represent domain values.
Use refinements for constraints such as an end date not preceding a start date. If a refinement performs asynchronous work, use parseAsync or safeParseAsync. Keep authorization and authoritative business decisions on the server; a client schema establishes usability, not permission or authenticity. See schema methods and parsing for the distinctions between synchronous and asynchronous validation.
Test the boundary without weakening the contract
Keep fixtures for a normal response, an empty collection, nullable fields, unknown enum values, and malformed nested values. Verify not just that invalid input fails, but that the correct path fails. A mistakenly broad schema can accept the fixture while leaving the original bug untouched.
For a product list, decide between whole-response rejection and partial acceptance. Rejecting the entire response preserves a strong invariant. Parsing each item separately can keep a catalog usable, but requires visible diagnostics and a policy for counts, pagination, and missing items. Never silently filter invalid financial records merely to keep a screen populated.
Format captured, sanitized fixtures with the JSON formatter before committing them. Include a test proving that a numeric string is rejected when the contract requires a number, and another proving that added keys follow your chosen policy. Those cases catch accidental weakening during future schema edits.
Validation has a cost proportional to the work requested. For large payloads, measure realistic responses and validate at sensible boundaries rather than repeatedly checking the same object in every render. Revalidate cached or persisted data when its provenance or schema version changes. A typed in-memory value and a JSON string restored next week do not have the same trust level.
Before release, run the same fixture tests against the schema used in production rather than a duplicate test-only definition. Add a regression fixture whenever an upstream change causes an incident. Record whether the provider violated a documented contract or your application inferred a guarantee that never existed. That distinction determines whether the right fix is a provider correction, a more accurate schema, or a deliberate interface fallback. For paginated APIs, also test a later page: providers sometimes return different nullability or metadata when no further results remain. A successful first-page request is not sufficient coverage of the boundary.
Key takeaways
- Treat API JSON as unknown until a schema accepts it.
- Infer trusted types from schemas instead of maintaining parallel definitions.
- Distinguish network, status, decoding, and validation failures.
- Choose nullable fields, unknown-key behavior, and coercion intentionally.
- Test realistic contract drift without logging sensitive payloads.
Reach designers & developers at work
Put your product next to the tools people use every day. One sponsor per placement, no tracking pixels.
Become a sponsor