Skip to main content

AOT Optimization

The zod-to-form walker knows both the schema constraints and which component renders each field. That joint knowledge enables ahead-of-time (AOT) validation optimization — progressive rewrites of per-field validation that work in both the runtime <ZodForm> and the z2f generate codegen path.

The same three optimization levels apply to both paths: runtime applies them to the FormField[] IR at walk time; codegen bakes the optimized output into the emitted .tsx.

Three levels of optimization​

Level 1 — Decompose tree​

  • Inlines all per-field Zod calls as atomic safeParse on hoisted schema fragments (created once at module load, not per keystroke).
  • Eliminates the zodResolver wrapper entirely.
  • Object-level refines and transforms are collected into a schemaLite and called via safeParse on submit.

The benefit is twofold: no per-render validator allocation, and a much smaller submit-time schema (only the object-level checks RHF can't express natively).

Level 2 — Native rules​

L2 replaces inlined Zod calls with native React Hook Form register() rules wherever the check maps cleanly:

  • min, max, minLength, maxLength
  • pattern — including extracted email / uuid / url regexes
  • required
  • step (numeric)

Error messages are extracted from the Zod schema at build time. Component-enforced types (enum, boolean, literal) need no validation at all — the <Select> simply cannot produce an out-of-range value.

After L2, Zod calls only remain for fields that carry refine() or transform(). If none do and the schemaLite is empty, the zod import can be dropped from the codegen output entirely.

Level 3 — Cross-field UX​

L3 converts analyzable cross-field superRefine — both field-inline and top-level object — to watch() + validate so the user gets real-time feedback instead of a submit-time error. Static analysis of refine function bodies is currently conservative; unanalyzable refines fall back to schemaLite submit validation.

How to enable​

Pass an optimization block to walkSchema:

import { walkSchema } from '@zod-to-form/core';

const fields = walkSchema(schema, {
optimization: { level: 2 },
});

For codegen, the CLI uses the default level from your z2f.config.ts — see Core Config for the optimization block.

Custom optimizers​

The walker dispatches each field through a chained optimizer registry: 1:N optimizers per Zod type. Optimizers run in order, each one free to refine the FormField's validation strategy. Register your own for components that enforce constraints the library doesn't know about — e.g. a DateRangePicker that guarantees a valid start/end range on its own.

import type { FormOptimizer } from '@zod-to-form/core';
import { walkSchema } from '@zod-to-form/core';

const dateRangeOptimizer: FormOptimizer = (schema, ctx, field) => {
if (field.component === 'DateRangePicker') {
field.validation = { mode: 'component-enforced' };
field.zodSchema = undefined;
}
};

walkSchema(schema, {
optimization: {
level: 2,
optimizers: { date: [dateRangeOptimizer] },
},
});

Setting mode: 'component-enforced' and clearing zodSchema tells every subsequent stage — L1/L2 rewrites, codegen emission, schemaLite collection — that no validation needs to be emitted for this field. The component is the contract.

Typed recursive FieldConfig​

fields and arrayItems on FieldConfig are typed against your Zod schema shape. Your editor autocompletes field names at every level of nesting, and typos become compile errors — not runtime mysteries.

import { defineConfig } from '@zod-to-form/core';

export default defineConfig({
components: { source: './ui' },
schemas: {
userSchema: {
fields: {
name: { component: 'Input', order: 1 },
addresses: {
arrayItems: {
fields: {
street: { component: 'Input' },
city: { component: 'Combobox', props: { options: cities } },
},
},
},
// typo: { ... } ← TypeScript error: 'typo' does not exist on UserSchema
},
},
},
});

The nested arrayItems.fields shape is derived from z.array(...)'s element type, so the same autocomplete works inside repeaters, discriminated unions, and intersected objects.

When AOT optimization matters​

Without optimization, an uncontrolled Zod-backed form runs safeParse on the entire schema on every keystroke — allocation plus traversal, multiplied by field count. L1 cuts that to safeParse on a hoisted fragment per field. L2 eliminates the safeParse entirely for anything RHF can express natively. L3 pushes cross-field feedback from submit-time into the render loop.

For codegen the wins compound: when L2 removes every Zod call for a form and schemaLite is empty, the emitted .tsx can drop the zod import entirely — one less dependency in the generated output.

Independent Zod compilation​

Requires Zod 4.6+. Set the compiler flag separately from the optimization level:

defaults: { optimization: { compileZod: true } } // whole-schema validation
defaults: { optimization: { level: 2 } } // native rules, compilation off
defaults: { optimization: { level: 2, compileZod: true } } // compile remaining Zod targets

Runtime <ZodForm> and useZodForm accept the same optimization object directly. With no level, compile-only keeps the full resolver. With a level, compilation prepares final per-field escape hatches and SchemaLite after collection. Native rules need no compilation. Runtime targets are cached by original schema identity; generated targets are hoisted outside renders and validation callbacks. Registration and walking continue to use original schemas.

Compilation is opt-in and has setup cost. Zod may leave unsupported targets uncompiled, and asynchronous execution retains its runtime path. Compilation uses dynamic code generation; environments with restrictive CSP must allow that mechanism or leave the flag off. z2f uses explicit target compilation rather than the global zod/compile side-effect import. See Zod's compilation guide and the measured benchmark report.

Escape hatches retain safeParse: their error messages and parsed output are part of the form contract. z.validate answers only a boolean and is measured separately; it does not replace those callbacks. The form-value normalization step still runs before parsing.