4. Parsing, Error Formatting, Registries, JSON Schema & Static Types

Beyond schema construction, Zod v4 provides standalone and method-based execution engines (parse, safeParse, validate, encode, decode), a streamlined 11-member $ZodIssue discriminated union with built-in formatters (prettifyError, treeifyError, flattenError, formatError), strongly-typed WeakMap metadata registries ($ZodRegistry, globalRegistry), bidirectional JSON Schema conversion (toJSONSchema, fromJSONSchema), and static/runtime input-output projection utilities (z.infer, z.input, z.output).

04_parsing_errors_registries_jsonschema_1
Zod v4 Execution, Error Formatting, Metadata Registry, and JSON Schema Surface
Source Code
from drawlib import canvas
from drawlib.diagrams.architecture import ArchitectureDiagram, Node, NodeGroup, PhosphorIcon
from drawlib.styles import Styles

canvas.clear()
canvas.setup(width=188, height=96, dpi=150)

d = ArchitectureDiagram(
    node_style=Styles.Primary,
    node_text_style=Styles.DarkBold.patch(text_size=10.0),
    edge_style=Styles.DarkBold,
    edge_text_style=Styles.Dark.patch(text_size=10.0),
    node_card_style=Styles.Neutral,
    title="Zod v4 Execution, Error Formatting, Registry & JSON Schema Surface",
)

# Left Group: Parse & Codec Execution (node width=46 -> center x = 5 + 23 = 28)
exec_grp = d.add(
    NodeGroup(title="1. Parse & Codec Execution", padding=5.0),
    xy=(0.0, 0.0),
)
parse_node = exec_grp.add(
    Node("Forward Parse & Guard\nparse / safeParse\nparseAsync / validate", width=46, height=20, icon=PhosphorIcon.PLAY_CIRCLE, icon_size=6.5, card_style=Styles.PrimaryNeutral),
    xy=(28.0, 52.0),
)
codec_exec = exec_grp.add(
    Node("Bidirectional Codec API\ndecode / encode\nsafeDecode / safeEncode", width=46, height=20, icon=PhosphorIcon.ARROWS_LEFT_RIGHT, icon_size=6.5, card_style=Styles.Neutral),
    xy=(28.0, 15.0),
)

# Center Group: Core Schema & Error Engine (node width=40 -> center x = 5 + 20 = 25)
core_grp = d.add(
    NodeGroup(title="2. Core Schema & $ZodError", padding=5.0),
    xy=(66.0, 0.0),
)
schema_hub = core_grp.add(
    Node("ZodType<O, I>\n_zod.run(payload, ctx)\nz.infer / z.input / z.output", width=40, height=20, icon=PhosphorIcon.CUBE, icon_size=6.5, style=Styles.WhiteBold, card_style=Styles.PrimaryFlat, text_style=Styles.WhiteBold.patch(text_size=10.0)),
    xy=(25.0, 52.0),
)
err_hub = core_grp.add(
    Node("ZodError & Formatters\nprettifyError / treeifyError\nflattenError / formatError", width=40, height=20, icon=PhosphorIcon.WARNING_OCTAGON, icon_size=6.5, style=Styles.WhiteBold, card_style=Styles.AccentFlat, text_style=Styles.WhiteBold.patch(text_size=10.0)),
    xy=(25.0, 15.0),
)

# Right Group: Registries & JSON Schema (node width=46 -> center x = 5 + 23 = 28)
meta_grp = d.add(
    NodeGroup(title="3. Registries & JSON Schema", padding=5.0),
    xy=(126.0, 0.0),
)
reg_node = meta_grp.add(
    Node("Metadata Registries\n$ZodRegistry<Meta>\nz.globalRegistry / .meta()", width=46, height=20, icon=PhosphorIcon.BOOKMARK_SIMPLE, icon_size=6.5, card_style=Styles.Neutral),
    xy=(28.0, 52.0),
)
json_node = meta_grp.add(
    Node("JSON Schema Bridge\nz.toJSONSchema(schema)\nz.fromJSONSchema(json)", width=46, height=20, icon=PhosphorIcon.FILE_CODE, icon_size=6.5, card_style=Styles.PrimaryNeutral),
    xy=(28.0, 15.0),
)

# Connections
d.connect(parse_node, schema_hub, padding=1.5)
d.connect(codec_exec, schema_hub, padding=1.5)
d.connect(schema_hub, err_hub, label="issues[]", padding=1.5)
schema_hub.fork([reg_node, json_node], at_x=119.0, padding=1.5)
d.connect(reg_node, json_node, label="metadata", padding=1.5)

d.draw(xy=(94.0, 46.0), align="center")
canvas.save()

functionz.parse / z.safeParse / z.parseAsync / z.safeParseAsync#

Zod v4 exposes both standalone functions (z.parse(schema, data), z.safeParse(schema, data), z.validate(schema, data)) and instance methods (schema.parse(data), schema.safeParse(data), schema.validate(data)).

Parse & Type-Guard Execution Signatures#

Function / Method Return Type Throws on Invalid? Description
z.parse(schema, value, ctx?) / schema.parse(value, ctx?) z.output<typeof schema> Yes (ZodError) Synchronously validates value in forward direction. Throws $ZodAsyncError if any schema or check returns a Promise.
z.safeParse(schema, value, ctx?) / schema.safeParse(value, ctx?) SafeParseResult<z.output<typeof schema>> No (throws only on async) Returns { success: true, data: O } | { success: false, error: ZodError<I> }. The error property is a lazy getter that only finalizes issues on first read.
z.parseAsync(schema, value, ctx?) / schema.parseAsync(value, ctx?) Promise<z.output<typeof schema>> Yes (rejects with ZodError) Asynchronously validates value, awaiting any async refinements or transforms.
z.safeParseAsync(schema, value, ctx?) / schema.safeParseAsync(value, ctx?) / schema.spa(value, ctx?) Promise<SafeParseResult<z.output<typeof schema>>> No Asynchronous counterpart to safeParse. .spa is a zero-overhead prototype alias on ZodType.
z.validate(schema, value, ctx?) / schema.validate(value, ctx?) value is z.input<typeof schema> (boolean) No (throws only on async) High-speed boolean type-guard (abortEarly: true, skips output allocation and ZodError instantiation; uses compiled validator fast path when available).
z.validateAsync(schema, value, ctx?) / schema.validateAsync(value, ctx?) Promise<boolean> No Asynchronous boolean validation (abortEarly: true).

ParseContext Options (ParseContext)#

Option Type Default Description
error $ZodErrorMap undefined Per-call error map overriding schema-level and global locale error messages.
reportInput boolean false When true, preserves the raw input property on every finalized $ZodIssue (omitted by default to avoid retaining sensitive or large payloads).
jitless boolean false When true, bypasses eval / new Function JIT fast-path compilation for this parse call.
import * as z from "zod";

const UserSchema = z.object({
  id: z.uuid(),
  email: z.email(),
});

// 1. Safe parsing with discriminated result
const res = z.safeParse(UserSchema, rawPayload, { reportInput: true });
if (!res.success) {
  console.error(z.prettifyError(res.error));
} else {
  console.log(res.data.email);
}

// 2. Boolean type guard via z.validate()
if (z.validate(UserSchema, rawPayload)) {
  // rawPayload is narrowed to z.input<typeof UserSchema>
  console.log(rawPayload.id);
}

functionz.encode / z.decode / z.safeEncode / z.safeDecode#

While parse always executes in the forward direction (Input -> Output), the codec execution family explicitly pairs forward decode (Input -> Output) with backward encode (Output -> Input).

Codec Execution Signatures#

Function / Method Input Parameter Type Return Type Direction & Behavior
z.decode(schema, value, ctx?) / schema.decode(value, ctx?) z.input<T> z.output<T> Forward (direction: "forward"). Statically requires z.input<T> rather than unknown.
z.encode(schema, value, ctx?) / schema.encode(value, ctx?) z.output<T> z.input<T> Backward (direction: "backward"). Runs ZodCodec reverse transforms (encode). Throws $ZodEncodeError if the schema graph contains a unidirectional ZodTransform, ZodDefault, or ZodCatch.
z.safeDecode(schema, value, ctx?) / schema.safeDecode(value, ctx?) z.input<T> SafeParseResult<z.output<T>> Non-throwing forward decode.
z.safeEncode(schema, value, ctx?) / schema.safeEncode(value, ctx?) z.output<T> SafeParseResult<z.input<T>> Non-throwing backward encode (still throws $ZodEncodeError if an unencodable unidirectional transform is encountered).
z.decodeAsync / z.encodeAsync / z.safeDecodeAsync / z.safeEncodeAsync z.input<T> / z.output<T> Promise<...> Asynchronous counterparts for codecs with async decode or encode callbacks.

classZodError & $ZodError#

Validation failures produce an instance of ZodError<T> (in zod, extending native Error) or $ZodError<T> (in zod/v4/core). Note that z.safeParse() uses a lazy getter on { success: false, get error() } so ZodError is only instantiated if .error is actually accessed.

Properties & Methods of ZodError<T>#

Member Type Availability Description
error.issues $ZodIssue[] zod & zod/v4/core Canonical array of finalized validation issues.
error.message string zod & zod/v4/core Lazy getter returning pretty-printed JSON of error.issues (computed and cached on first read).
error.name "ZodError" | "$ZodError" zod & zod/v4/core Error class identifier (instanceof Error is true).
error.flatten(mapper?) $ZodFlattenedError<T, U> Classic zod (deprecated) Prefer standalone z.flattenError(error, mapper).
error.format(mapper?) $ZodFormattedError<T, U> Classic zod (deprecated) Prefer standalone z.formatError(error, mapper).

type$ZodIssue & Issue Subtypes#

Zod v4 consolidates issue codes into a clean 11-member discriminated union ($ZodIssue, keyed on issue.code), merging legacy v3 codes (invalid_string, invalid_ReturnValue, invalid_date, invalid_enum_value, invalid_literal) into unified structural types.

04_parsing_errors_registries_jsonschema_2
Discriminated Union Hierarchy of $ZodIssueBase and the 11 $ZodIssue Subtypes in Zod v4
Source Code
from drawlib import canvas
from drawlib.diagrams.class_diagram import ClassDiagram, ClassNode
from drawlib.fonts import FontRoboto, FontSourceCode
from drawlib.styles import Styles

canvas.clear()
canvas.setup(width=188, height=104, dpi=150)

cd = ClassDiagram(
    node_style=Styles.Neutral.patch(text_font=FontSourceCode.ROBOTO_MONO, text_size=9.5),
    header_style=Styles.PrimaryNeutral.patch(text_font=FontRoboto.ROBOTO_BOLD, text_size=10.5),
    edge_style=Styles.DarkBold,
    edge_text_style=Styles.Dark.patch(text_size=9.5),
    title="ZodIssue ($ZodIssueBase) — 11 Discriminated Issue Union Subtypes",
)

# Top Root Interface (ClassNode xy is Center!)
base_issue = cd.add(
    ClassNode("$ZodIssueBase", stereotype="interface", width=62, header_style=Styles.SecondaryNeutral.patch(text_font=FontRoboto.ROBOTO_BOLD, text_size=10.5))
    .add_attribute("code", "$ZodIssueCode", visibility="+")
    .add_attribute("path", "PropertyKey[]", visibility="+")
    .add_attribute("message", "string", visibility="+")
    .add_attribute("input", "unknown | undefined", visibility="+"),
    xy=(94.0, 74.0),
)

# Bottom Row 1: Type, Value & Custom Issues
grp_type = cd.add(
    ClassNode("Type, Value & Custom", width=44, header_style=Styles.PrimaryNeutral)
    .add_attribute("invalid_type", "{ expected }")
    .add_attribute("invalid_value", "{ values: Primitive[] }")
    .add_attribute("custom", "{ params?: Record }"),
    xy=(23.0, 22.0),
)

# Bottom Row 2: Range, Size & Divisibility Issues
grp_bounds = cd.add(
    ClassNode("Range & Divisibility", width=44, header_style=Styles.PrimaryNeutral)
    .add_attribute("too_big", "{ origin, maximum }")
    .add_attribute("too_small", "{ origin, minimum }")
    .add_attribute("not_multiple_of", "{ divisor: number }"),
    xy=(70.0, 22.0),
)

# Bottom Row 3: String Format & Object Key Issues
grp_format = cd.add(
    ClassNode("Format & Object Keys", width=44, header_style=Styles.PrimaryNeutral)
    .add_attribute("invalid_format", "{ format, pattern? }")
    .add_attribute("unrecognized_keys", "{ keys: string[] }"),
    xy=(117.0, 22.0),
)

# Bottom Row 4: Union & Collection Issues
grp_coll = cd.add(
    ClassNode("Union & Collections", width=44, header_style=Styles.PrimaryNeutral)
    .add_attribute("invalid_union", "{ errors, discriminator? }")
    .add_attribute("invalid_key", "{ origin, issues }")
    .add_attribute("invalid_element", "{ origin, key, issues }"),
    xy=(164.0, 22.0),
)

cd.connect(grp_type, base_issue, "inheritance", start_side="top", end_side="bottom")
cd.connect(grp_bounds, base_issue, "inheritance", start_side="top", end_side="bottom")
cd.connect(grp_format, base_issue, "inheritance", start_side="top", end_side="bottom")
cd.connect(grp_coll, base_issue, "inheritance", start_side="top", end_side="bottom")

cd.draw(xy=(94.0, 50.0), align="center")
canvas.save()

Complete $ZodIssue Union Reference#

issue.code TypeScript Interface Specific Properties Emitted By
"invalid_type" $ZodIssueInvalidType expected: $ZodInvalidTypeExpected ("string", "number", "int", "object", class name, etc.) All primitive/container type checks, z.instanceof(), z.nonoptional()
"too_big" $ZodIssueTooBig origin: "number" | "int" | "bigint" | "date" | "string" | "array" | "set" | "file", maximum: number | bigint, inclusive?: boolean, exact?: boolean lt, lte, maxLength, maxSize, length, size
"too_small" $ZodIssueTooSmall origin: ..., minimum: number | bigint, inclusive?: boolean, exact?: boolean gt, gte, minLength, minSize, length, size
"invalid_format" $ZodIssueInvalidStringFormat format: $ZodStringFormats | string, pattern?: string, prefix?: string, suffix?: string, includes?: string, algorithm?: string z.email(), z.uuid(), z.iso.*(), regex, startsWith, endsWith, includes, jwt
"not_multiple_of" $ZodIssueNotMultipleOf divisor: number multipleOf / .step()
"unrecognized_keys" $ZodIssueUnrecognizedKeys keys: string[] z.strictObject()
"invalid_union" $ZodIssueInvalidUnion errors: $ZodIssue[][], discriminator?: string, inclusive?: boolean, matches?: number[] z.union(), z.discriminatedUnion(), z.xor()
"invalid_key" $ZodIssueInvalidKey origin: "map" | "record", issues: $ZodIssue[] z.record(keySchema, ...), z.map(keySchema, ...)
"invalid_element" $ZodIssueInvalidElement origin: "map" | "set", key: unknown, issues: $ZodIssue[] z.map(..., valSchema), z.set(valSchema)
"invalid_value" $ZodIssueInvalidValue values: Primitive[] z.literal(), z.enum(), z.nativeEnum(), z.mime()
"custom" $ZodIssueCustom params?: Record<string, any> .refine(), .superRefine(), z.custom()

functionz.prettifyError / z.treeifyError / z.flattenError / z.formatError#

Zod v4 provides four tree-shakable top-level error formatting functions that transform a $ZodError (or StandardSchemaV1.FailureResult) into human-readable strings or structured UI error trees.

Error Formatter Comparison#

Function Signature Output Structure Best Use Case
z.prettifyError(error) (error: StandardSchemaV1.FailureResult) => string Multi-line string with ✖ <message> and → at <dot.path[0]> CLI output, server logs, test assertions, LLM prompt feedback
z.treeifyError(error, mapper?) <T, U = string>(error: $ZodError<T>, mapper?: (iss: $ZodIssue) => U) => $ZodErrorTree<T, U> Recursive { errors: U[], properties?: { [K]: Tree }, items?: Tree[] } Nested forms, complex JSON payloads (replaces legacy formatError)
z.flattenError(error, mapper?) <T, U = string>(error: $ZodError<T>, mapper?: (iss: $ZodIssue) => U) => $ZodFlattenedError<T, U> { formErrors: U[], fieldErrors: { [P in keyof T]?: U[] } } Single-level HTML forms and action handlers
z.formatError(error, mapper?) <T, U = string>(error: $ZodError<T>, mapper?: (iss: $ZodIssue) => U) => $ZodFormattedError<T, U> Legacy { _errors: U[], [key]: { _errors: U[] } } Backward compatibility with Zod v3 .format() consumers
import * as z from "zod";

const ProfileSchema = z.object({
  username: z.string().min(3),
  tags: z.array(z.string().min(2)),
});

const result = ProfileSchema.safeParse({ username: "ab", tags: ["ok", "x"] });
if (!result.success) {
  // 1. Human-readable CLI string
  console.log(z.prettifyError(result.error));
  // ✖ Too small: expected string to have >=3 characters
  //   → at username
  // ✖ Too small: expected string to have >=2 characters
  //   → at tags[1]

  // 2. Type-safe nested error tree
  const tree = z.treeifyError(result.error);
  console.log(tree.properties?.username?.errors); // ["Too small: ..."]
  console.log(tree.properties?.tags?.items?.[1]?.errors); // ["Too small: ..."]
}

functionz.config & z.locales#

Global runtime behavior and error message localization are managed through z.config(newConfig?) (backed by globalThis.__zod_globalConfig so CJS and ESM bundles share a single configuration instance) and z.locales.

$ZodConfig Options & Error Precedence#

Property Type Precedence / Effect
customError $ZodErrorMap | undefined Priority 3 (after schema-level error param [1] and parse-level ctx.error [2]). Global custom error map.
localeError $ZodErrorMap | undefined Priority 4 (lowest priority before fallback). Set via z.config(z.locales.ja()), z.locales.en(), etc.
jitless boolean | undefined Disables new Function / eval fast-path object & compilation shims globally for strict CSP environments (e.g., Cloudflare Workers / browser extensions).
memoizer $ZodMemoizer | undefined Enables cyclic object/array reference tracking during parsing (z.config({ memoizer: z.memoizer() })). Automatically initialized on Classic container schemas.
import * as z from "zod";

// Switch default error messages to Japanese globally
z.config(z.locales.ja());

// Override specific issue codes globally while delegating others to the locale
z.config({
  customError: (issue) => {
    if (issue.code === "invalid_type" && issue.input === undefined) {
      return "Required field is missing";
    }
    return undefined; // Fall through to localeError
  },
});

class$ZodRegistry & z.globalRegistry#

Zod v4 decouples schema metadata from schema instances using WeakMap-backed $ZodRegistry<Meta, Schema> containers. Because registry entries are held in a WeakMap<Schema, Meta>, ephemeral schemas are garbage-collected normally while registered schemas with an id are also indexed for $defs extraction in z.toJSONSchema().

$ZodRegistry API & Schema Helpers#

Method / Function Signature Description
z.registry<Meta, Schema>() registry<T extends object | undefined = object | undefined, S extends $ZodType = $ZodType>(): $ZodRegistry<T, S> Creates a new strongly-typed metadata registry constrained to metadata shape T and schema subtype S.
z.globalRegistry $ZodRegistry<GlobalMeta> Process-wide singleton registry (globalThis.__zod_globalRegistry) storing GlobalMeta ({ id?, title?, description?, deprecated?, [k: string]: unknown }).
reg.add(schema, meta) add<S extends Schema>(schema: S, meta: $replace<Meta, S>): this Associates meta with schema (mutates registry in place; schema.register(reg, meta) is the fluent chainable wrapper).
reg.get(schema) get<S extends Schema>(schema: S): $replace<Meta, S> | undefined Retrieves metadata for schema, automatically inheriting non-id metadata from schema._zod.parent clones.
reg.has(schema) / reg.remove(schema) / reg.clear() (schema: Schema) => boolean | this Checks existence, removes a single schema, or resets the registry.
schema.meta(data?) meta(): GlobalMeta | undefined / meta(data: GlobalMeta): this Reads metadata from z.globalRegistry (0 args) or clones schema and registers data in z.globalRegistry (1 arg).
schema.describe(description) (description: string) => this Clones schema and registers { description } in z.globalRegistry.

[!TIP] Use the phantom symbols z.$output and z.$input inside a custom registry's Meta type to constrain metadata fields to the registered schema's static output or input type (e.g., z.registry<{ examples: z.$output[] }>()).

import * as z from "zod";

// Custom registry with type-safe examples tied to each schema's output type
const docRegistry = z.registry<{
  id?: string;
  title: string;
  examples: z.$output[];
}>();

const PortSchema = z.int().min(1).max(65535).register(docRegistry, {
  id: "PortNumber",
  title: "TCP/UDP Port",
  examples: [80, 443, 8080], // Statically checked as number[]!
});

functionz.toJSONSchema & z.fromJSONSchema#

Zod v4 ships native, zero-dependency bidirectional conversion between Zod schemas/registries and JSON Schema (draft-2020-12, draft-07, draft-04, and openapi-3.0).

z.toJSONSchema & z.fromJSONSchema Signatures#

Function / Method Signature Description
z.toJSONSchema(schema, params?) / schema.toJSONSchema(params?) (schema: $ZodType, params?: ToJSONSchemaParams) => JSONSchema.BaseSchema Converts a single Zod schema into a JSON Schema document. Shared or cyclic subschemas are extracted into $defs (or definitions in Draft-04/07).
z.toJSONSchema(registry, params?) (registry: $ZodRegistry<{ id?: string }>, params?: RegistryToJSONSchemaParams) => { schemas: Record<string, JSONSchema.BaseSchema> } Converts all id-tagged schemas in registry into a cross-referenced map of JSON Schemas.
z.fromJSONSchema(jsonSchema, params?) (schema: JSONSchema, params?: { defaultTarget?: JSONSchemaVersion; registry?: $ZodRegistry }) => ZodType Parses a JSON Schema object (draft-2020-12, draft-7, draft-4, openapi-3.0) and constructs an equivalent runtime ZodType schema.

ToJSONSchemaParams Options#

Option Type Default Description
target "draft-2020-12" | "draft-07" | "draft-04" | "openapi-3.0" "draft-2020-12" Target JSON Schema specification version.
io "output" | "input" "output" Selects whether pipes, codecs, defaults, and coerced schemas emit their "output" or "input" representation.
metadata $ZodRegistry<Record<string, any>> z.globalRegistry Registry inspected to copy title, description, deprecated, examples, and extract named id definitions.
unrepresentable "throw" | "any" | UnrepresentableHandler "throw" Behavior when encountering types without a JSON Schema equivalent (z.date(), z.bigint(), z.map(), z.transform()): throw, emit {}, or invoke a custom handler.
cycles "ref" | "throw" "ref" Emits $ref pointers for recursive schemas ("ref") or throws an error ("throw").
reused "inline" | "ref" "inline" Controls whether non-cyclic schemas referenced multiple times are inlined or extracted to $defs.
override (ctx: { zodSchema, jsonSchema, path }) => void undefined Post-processing hook invoked on each converted node to mutate jsonSchema in place.
import * as z from "zod";

const UserSchema = z
  .object({
    id: z.uuid(),
    role: z.enum(["admin", "member"]).default("member"),
  })
  .meta({ id: "User", title: "Platform User" });

// Emit OpenAPI 3.0 input schema
const openApiInput = z.toJSONSchema(UserSchema, {
  target: "openapi-3.0",
  io: "input",
});

// Reconstruct a runtime Zod schema from a JSON Schema document
const DynamicValidator = z.fromJSONSchema(openApiInput);

typez.infer / z.input / z.output#

In Zod v4, z.input and z.output serve a dual role: they are static TypeScript type helpers (z.input<typeof S>, z.output<typeof S>, z.infer<typeof S>) AND runtime schema projection functions (z.input(schema), z.output(schema)).

Static Type & Runtime Schema Projection Reference#

Construct Kind Signature / Definition Description
z.infer<T> / z.output<T> Static Type T extends { _zod: { output: any } } ? T["_zod"]["output"] : unknown Extracts the decoded/parsed output type of schema T.
z.input<T> Static Type T extends { _zod: { input: any } } ? T["_zod"]["input"] : unknown Extracts the raw input type accepted by schema.parse() / z.decode().
z.input(schema) Runtime Function <T extends $ZodType>(schema: T) => ZodType<z.input<T>, z.input<T>> Traverses schema and replaces every ZodPipe / ZodCodec with its input side (def.in) and every ZodDefault with ZodOptional.
z.output(schema) Runtime Function <T extends $ZodType>(schema: T) => ZodType<z.output<T>, z.output<T>> Traverses schema and replaces every ZodPipe / ZodCodec with its output side (def.out, preserving pipe checks) and unwraps ZodPrefault.
import * as z from "zod";

const AccountCodec = z.object({
  createdAt: z.codec(z.iso.datetime(), z.date(), {
    decode: (s) => new Date(s),
    encode: (d) => d.toISOString(),
  }),
});

// 1. Static Type Extraction
type AccountWire = z.input<typeof AccountCodec>;   // { createdAt: string }
type AccountDomain = z.infer<typeof AccountCodec>; // { createdAt: Date }

// 2. Runtime Schema Projection (rewrites pipes/codecs to pure input or output schemas)
const WireOnlySchema = z.input(AccountCodec);     // Validates { createdAt: string } -> { createdAt: string }
const DomainOnlySchema = z.output(AccountCodec);  // Validates { createdAt: Date }   -> { createdAt: Date }