Validate External Data at Runtime in TypeScript

Flags JSON.parse results, fetch responses, env vars, storage, and postMessage data that TypeScript trusts through a cast or generic instead of a schema.

What it flags

Flaggedsrc/billing/exchange-rates.ts
type Rates = { base: string; rates: Record<string, number> };

async function getJson<T>(url: string): Promise<T> {
  const res = await fetch(url);
  return (await res.json()) as T; // nothing checks that the body matches T
}

export async function convert(amount: number, to: string): Promise<number> {
  const data = await getJson<Rates>('https://api.example.com/latest?base=USD');
  return amount * data.rates[to];
}
Passessrc/billing/exchange-rates.ts
import { z } from 'zod';

const RatesSchema = z.object({
  base: z.string(),
  rates: z.record(z.string(), z.number()),
});

async function getJson<T>(url: string, schema: z.ZodType<T>): Promise<T> {
  const res = await fetch(url);
  if (!res.ok) {
    throw new Error(`GET ${url} failed with ${res.status}`);
  }
  return schema.parse(await res.json());
}

export async function convert(amount: number, to: string): Promise<number> {
  const data = await getJson('https://api.example.com/latest?base=USD', RatesSchema);
  const rate = data.rates[to];
  if (rate === undefined) {
    throw new Error(`No exchange rate for ${to}`);
  }
  return amount * rate;
}

Why it matters

TypeScript types are erased at compile time, so a response annotated with a specific type is whatever the server sent. When an API renames a field or returns an error body, the bad value travels until something reads a missing property, often in a different module or a background job. A schema at the boundary turns that into one clear error at the point of entry, and deriving the type from the schema keeps the two from drifting apart.

Agent instructions

cubic checks every pull request against these instructions. Paste them into AGENTS.md or Cursor rules and your AI coding tool follows them while it writes code.

AGENTS.md

Add this section to AGENTS.md at the repository root. Codex, Cursor, and other AI coding tools read it.

## Validate External Data at Runtime in TypeScript

Applies to `**/*.ts`, `**/*.tsx`, `**/*.mts`, `**/*.cts`.

Review TypeScript code that reads data from outside the program for types that are asserted instead of checked at runtime.

Flag changes that:
- Pass the result of `JSON.parse` (which returns `any`) or `response.json()` into typed code through an annotation, an `as` cast, or a generic parameter without validating it with a schema library such as Zod, Valibot, or ArkType, or a type guard that checks each field.
- Add or call generic helpers such as `getJson<T>(url)` that return `T` without taking a schema, so the type parameter works as an unchecked cast.
- Read `process.env` values with `as string`, `!`, or `Number(...)` at use sites instead of parsing required variables once at startup and failing fast when one is missing or malformed.
- Parse values from `localStorage`, `sessionStorage`, cookies, or URL search params and use them as typed objects without handling data written by an older app version or edited by the user.
- Read `event.data` in `window` `message` listeners without checking `event.origin` against an allowlist and validating the payload shape.
- Declare a TypeScript type for an external payload separately from its schema instead of deriving it with `z.infer` or its equivalent in the schema library.

Do not flag:
- Results from typed clients generated from a schema the team controls, such as ORM queries or end-to-end typed RPC within the same repository.
- Values validated earlier in the same flow and passed along with their inferred type.
- Test fixtures and mocks.

Checks files matching **/*.ts, **/*.tsx, **/*.mts, **/*.cts.

More TypeScript agents

See all TypeScript code review standards

Run this agent on every pull request

cubic reviews every pull request against the agents you turn on and comments on the lines that break them.

Catch this in your code