Branch-by-branch audit of Zod v4.4.3's core/schemas.ts, core/checks.ts, and
core/api.ts (.references/zod/packages/zod/src/v4/core/) against the zodrs
implementation. Every runtime unit in those files is classified:
- essential: observable behavior zodrs must reproduce. The zodrs file(s)
carrying it and the
docs/CONTRACT.mdsection covering it are cited. - residue: deliberately cut, with the reason.
- gap: essential but currently missing or behaviorally wrong in zodrs.
These are listed in
## Gaps foundand become fix tasks.
| File | Runtime units walked | Composition |
|---|---|---|
core/schemas.ts |
98 | 74 $Zod* constructors + 3 exported format helpers (isValidBase64, isValidBase64URL, isValidJWT) + 21 internal handle*/normalizeDef/getTupleOptStart/mergeValues functions |
core/checks.ts |
23 | $ZodCheck base + 21 check classes + handleCheckPropertyResult |
core/api.ts |
120 functions + 1 const | 120 export function declarations (counting the _tuple ×3, _enum ×3, _literal ×3 overload signatures individually; 114 unique names) + the TimePrecision const |
| Total | 242 |
Interfaces and type aliases are type-level only (zero runtime) and are covered by the inference-parity contract rather than this walk; they are not counted as units. Self-check: every function/const name from the three files appears in the classification tables below.
zodrs location shorthand used in the tables:
| Tag | File |
|---|---|
| IR | packages/zodrs/src/core/nodes.ts + core/plan.ts (node kinds, plan emission) |
| INT | packages/zodrs/src/core/interpreter.ts (reference validator) |
| CG | packages/zodrs/src/core/codegen.ts (specialized-closure compiler) |
| CL | packages/zodrs/src/classic/schemas.ts (classic factories + $ZodType methods) |
| MINI | packages/zodrs/src/mini/mini.ts (mini projection) |
| FMT | packages/zodrs/src/core/formats.ts (string-format engines) |
| PARSE | packages/zodrs/src/core/parse.ts (16 entry points, encode/decode) |
| RS | crates/zodrs/src/{plan,compile,validate,formats,issue}.rs (Rust byte path) |
Behaviors zodrs does not reproduce, because reproducing them would carry a zod bug across.
Zod writes results with result[key] = value. For a shape key named
__proto__ that assignment reaches the inherited setter, so the field is
validated and then silently dropped: z.object({ __proto__: z.string() })
parsing {"__proto__":"s"} returns {}.
zodrs defines the property instead (defineValue), so the field it just
validated is the field it returns. Pinned by keeps a valid __proto__ field in the output in packages/zodrs/src/core/shape-access.test.ts.
Everything else about the key matches zod: a wrong value is rejected, and the
key is dropped when it arrives as an unknown key rather than a declared one.
A plan carrying a __proto__ shape key is not byte-path eligible, for the same
prototype-visibility reason as constructor and its siblings.
Essential behaviors missing or behaviorally wrong in zodrs at audit time. Each entry names the Zod unit, the evidence, and the zodrs behavior that should exist. (Codegen conformance was at 999/1306 and being ground to green in parallel; several of these are live conformance failures.)
-
$ZodFunctionis a bare typeof check. Zod (schemas.ts:4423–4519) validates{input, output}and produces a wrapper via.implement(fn)/.implementAsync, plus.args(), input/output getters, andthispreservation; vendoredclassic/tests/function.test.tspins all of it. zodrsfunction_(CL:631) emitsnode({kind:"function"})with no input/output and INT:237 only checkstypeof input === "function". Should exist: the full function schema of CONTRACT §13:.implement()returning a function that parses args against the input schema and the return value against the output schema,.args(), async variant. -
Encode direction is unimplemented. PARSE:94–115 sets
direction: "backward"on the context, but nothing consumes it: INT/CG never branch ondirection(grep: only the type declaration at INT:10). Consequences:z.encodeon a pipe runsa→bforward instead of reverse;z.codec(CL:667) builds a decode-only pipe (a → decode host → b), soz.encode(codec)runs the decode path;z.stringbool(CL:617) is a decode-only transform with notruthy[0]/falsy[0]encode; noZodEncodeErrorexists for unidirectional transforms on encode; fallback wrappers short-circuit in both directions instead of decode-only. Vendoredclassic/tests/codec.test.ts:36,49,118andcodec-examples.test.tspin reverse behavior. Should exist: CONTRACT §15–§16: pipes reverse (bthena), codecs runB(reverse) →encode()→A(reverse), stringbool encodes totruthy[0]/falsy[0], transforms throwZodEncodeErroron encode,.default/.prefault/.catchshort-circuit in decode direction only. -
.exactOptional()aliases.optional()(wrong semantics).$ZodExactOptional(schemas.ts:3537–3559) delegates to the inner parse with no undefined short-circuit: absent object keys pass (viaoptin/optout = "optional") but a presentundefinedis rejected. zodrs CL:212exactOptional()returnsthis.optional(), which accepts presentundefined. Vendoredclassic/tests/optional.test.ts:185–214pinssafeParse(undefined).success === false,unwrap(), and absent-key behavior. Should exist: anexactOptionalnode kind (or flag) with delegate-only parse andoptin/optout = "optional", no| undefinedin the output/input types. -
Under-length tuples don't emit the single
too_small.$ZodTuple+getTupleOptStart(schemas.ts:2666–2760): wheninput.length < optinStartand norest, onetoo_small {origin:"array", minimum: optStart, inclusive: true}issue and no element-level errors. INT tuple case only guards over-length; short input yields per-iteminvalid_typeissues onundefined. Vendoredclassic/tests/tuple.test.ts:215,377,410pins the singletoo_small. Should exist: the optStart length precheck in both INT and CG tuple paths. -
Record exhaustiveness and
partialRecord/looseRecordmissing.$ZodRecord(schemas.ts:2875–3028) readsdef.keyType._zod.valuesand requires every enum/literal key to be present (unless the key type is$partial); INT's record case only iterates input keys, so missing keys are never reported.z.partialRecordandz.looseRecordfactories exist in Zod classic (classic/schemas.ts:1814,1829) but nowhere in zodrs (grep: no matches). Vendoredclassic/tests/record.test.ts:297,487,510andto-json-schema.test.tspin them. Should exist: exhaustive enum/literal-key checking per CONTRACT §11, plus the two factories (partialRecordopts out of exhaustiveness;looseRecord= record without key-exhaustiveness guarantees). -
Enum extras missing.
$ZodEnum(schemas.ts:3220–3256) exposes.enum.NAMEmember access,.options,.extract([...]),.exclude([...]). zodrsenum_(CL:518–524) returns a bare schema with none of these (grep: noextract/excludein CL or MINI). Vendoredclassic/tests/enum.test.ts:138–173pins extract/exclude. Should exist: the member-access/extract/exclude surface of CONTRACT §4. -
z.hash()factory andZodCustomStringFormattype missing from classic. Zod classic exportshash(alg, {enc?})(classic/schemas.ts:1013); zodrs hashashonly in MINI (mini.ts:744) and noZodCustomStringFormattype anywhere (grep: no matches). Vendoredclassic/tests/hash.test.ts:2imports both from"zodrs". Should exist: classichash()dispatching to themd5…sha512format checks (CONTRACT §5) and the exportedZodCustomStringFormat<"alg_enc">type. -
z.success()missing from classic.$ZodSuccess(schemas.ts:3841–3869) parses the inner schema and returns a boolean. MINI implements it (mini.ts:1068, via a host transform node); classic has nosuccessfactory (grep: no matches in CL). Should exist: classicz.success(inner). -
z.invertCodec()missing from classic. Zod classic exportsinvertCodec(classic/schemas.ts:2420); zodrs has it only in MINI (mini.ts:1092). Should exist: classicinvertCodec(codec)swapping decode/encode directions (CONTRACT §15), once gap 2 lands. -
Discriminated-union discriminator traversal is literal/enum only.
$ZodDiscriminatedUnion(schemas.ts:2360–2449) resolves discriminators throughpropValues, which traverses optional/nullable/readonly/default wrappers and nested unions, supportsnull/boolean/number/bigint discriminants, and honors{ unionFallback: true }(fall back to a plain-union scan when no discriminator matches; also forced in the backward direction, schemas.ts:2420–2423). zodrsdiscriminantValues(CL:581–586) reads only directliteral/enumshape entries; nounionFallbackanywhere (grep: no matches). CONTRACT §9 claims the full set. Should exist: wrapper-traversing discriminator resolution andunionFallbackin CL + INT + CG discunion paths. -
whencheck param unimplemented. Every Zod check honors{ when: (payload) => boolean }(checks.tsonattach/run gating); zodrsRuntimeCheck(IR) hasabortbut nowhen, and INT'sapplyChecksSync/host execution never consults it. Vendoredclassic/tests/refine.test.ts:434,466pins gatedsuperRefine. Should exist:whenonRuntimeCheck, evaluated per parse before the check runs. -
safeExtendisextendwith no safety distinction. ZodextendthrowsCannot overwrite keys on object schemas containing refinements. Use .safeExtend()(util.ts:672), whilesafeExtend(util.ts:687) performs the merge unconditionally. zodrsextend(CL:328) never throws andsafeExtend(CL:339) just calls it. Should exist: the refinement-guard throw inextend, withsafeExtendbypassing it (CONTRACT §7). -
core.util.allowsEvalexport missing. Vendoredclassic/tests/jitless-allows-eval.test.ts:4importscore.util.allowsEvaland pins its jitless short-circuit (Zod util.ts:365–367). zodrscore/util.tshas noallowsEval(grep: no matches). With closure codegen (decision 1) no eval ever happens, so the export should be a memoized{ value: false }that still honors theconfig({jitless:true})short-circuit contract the test asserts. -
describe/metacore check factories not exported. api.ts:1696 and api.ts:1709 exportdescribe(s)/meta(obj)as check-producing core functions (used to attach registry metadata through.check()). zodrs has the.describe()/.meta()methods (CONTRACT §18) butzodrs/coreexports nodescribe/metafunctions (core/index.ts:2–37). Not pinned by the vendored corpus. Should exist:z.core.describe/z.core.metaequivalents, or a recorded decision to cut them as core-internal API.
- Codegen uses compiled closures, not
new Function. The repo'sjs-no-dynamic-evalrule forbids string eval.core/codegen.tscompiles each schema node into a specialized closure (CompiledNode = (input, context, path) => unknown | FAIL) over a per-schema cache, delegating un-specialized kinds to the interpreter. This preserves the plan's straight-line, zero-allocation-success-path and switch-dispatch properties without dynamic eval.CODEGEN_AVAILABLEis therefore the constanttrue; the interpreter remains the backend of record for node kinds codegen does not specialize and for theZODRS_BACKEND=interpreterconformance tier. - JSON Schema generation is TypeScript-side only.
toJSONSchemalives inpackages/zodrs/src/core/json-schema.tsandfromJSONSchemainpackages/zodrs/src/classic/from-json-schema.ts. The plan's Rustto_json_schema(plan, opts)was cut because theZODRS_LOADER=noneconformance tier must pass to-json-schema tests with no native backend, so one TS implementation serves all tiers.crates/zodrs/src/jsonschema.rsremains a 1-line placeholder slated for deletion (verified: file contains only a doc comment). - Crate renamed
zodrs-core→zodrsfor the crates.io publication requirement (verified:crates/zodrs/Cargo.tomlname = "zodrs").
zodrs has four validation tiers, all designed to produce identical observable
results. The loader (core/loader.ts) selects the first available:
| Tier | Backend | Loader key | Selection |
|---|---|---|---|
| Native | Rust napi cdylib (crates/zodrs-node/) |
ZODRS_LOADER=native (default) |
Synchronous; first choice |
| WASM | wasm32-wasip1-threads addon (packages/zodrs/wasm/) |
ZODRS_LOADER=wasm |
Async fire-and-forget; second choice |
| TS codegen | Compiled closures (core/codegen.ts) |
ZODRS_BACKEND=codegen (default) |
Fallback when no native/WASM |
| TS interpreter | Reference validator (core/interpreter.ts) |
ZODRS_BACKEND=interpreter |
Fallback when config().jitless or codegen unavailable |
pnpm -C packages/zodrs prepack builds and verifies two release artifacts:
native/zodrs_node.linux-x64-gnu.nodefor Linux x64 GNU;wasm/zodrs_node.wasm32-wasi.wasmwith its generated WASI loaders.
crates/zodrs-node/package.json is the N-API metadata source. It advertises
only those two build targets. The generated native loader uses the
zod-rs-node-* fallback package prefix. The npm package embeds both artifacts;
it does not claim separate native packages or other native platforms. On an
unsupported native platform, loader resolution continues to the embedded WASM
tier.
The prepack gate forces one native loader self-test and one WASM loader
self-test. scripts/verify-artifacts.mjs then checks versions, target names,
release files, loader references, and debug/self-test exclusions.
The TS codegen and interpreter share the same issue-construction code paths
(issue(), addIssue(), checkPayloadIssues() in interpreter.ts), so
payload/trace divergences between them are not expected. The native tier
constructs issues independently in Rust (issue.rs, validate.rs), then
serializes them as JSON for the TS side to finalize (parse.ts).
The native path produces raw issues (no message, no input) and the TS
side finalizes them:
- Rust
validate.rswalks asonic-rsDOM against the compiled plan arena, constructingIssuestructs with ordered(field, value)pairs and aPath(issue.rs). Fields are pushed in insertion order; theIssueWireserializer writes them in that order and collapses duplicates to their first position with their last value. Apathfield in the payload is never trusted: the realPathis written at that key's position, or appended last when the payload carries nopath. - Issues serialize as a JSON array string (
issues_to_json) in theVerdict.payload(validate.rs:36–43). - TS
parse.ts:parseRawIssues(line 183) parses the payload into$ZodRawIssue[]. - TS
parse.ts:backFillInput(line 207) walks each issue's path against the originalJSON.parse'd value to attach theinputfield — needed for message resolution ("received X") and stripped byfinalizeIssueunlessreportInputis set. - TS
errors.ts:finalizeNested(line 354) recursively finalizes nested union/key/element sub-issues, thenfinalizeIssue(line 336) resolvesmessagethrough the error-map precedence chain:issue.message → inst.error → context.error → global.customError → global.localeError → defaultError → "Invalid input".
The differential fuzz harness (packages/conformance/differential/) asserts
the two-backend invariant for random schemas and inputs.
schema.safeParseJson(bytes) (Rust) and
schema.safeParse(JSON.parse(bytes)) (TS) must have the same success flag.
Successful data uses exactDataEqual; issue arrays use
issuePayloadEqual (compare.ts:124–177).
Current status (2026-08-07): The gate FAILS. The fuzz found 12 distinct
root-cause divergence classes between the backends. Last survey (seed 24301,
20000 cases): 19535 compared, 13009 matched, 4810 known-skips, 1716 new
mismatches across 52 signatures, wall ~2s. KNOWN-MISMATCHES.json is
currently empty ("entries": []); previously recorded entries were cleared
after their fixes landed.
compare.ts:issuePayloadEqual treats issue keys with undefined values as
absent. This matches the TS and Rust issue-construction paths. In contrast,
exactDataEqual preserves every own enumerable key in parsed output. It
therefore reports {} and { a: undefined } as different successful results
(compare.ts:100–133).
Each class is grounded in the source files that produce the divergent payloads.
The fuzz harness classifies mismatches by caseKind|diffTag signatures; the
classes below map to the source-level root causes.
Rust issue.rs constructs issues with Issue::new("code", path).with("k", v)
— fields pushed in insertion order, path serialized last
(issue.rs:122–130). TS interpreter.ts:issue() (line 56) constructs
{...details, input, path, inst: {error: node.error}} — the key order depends
on the details object literal construction at each call site. The
issue.rs doc comment (line 64–67) states insertion order "mirrors the TS
interpreter's issue-construction sites exactly", and insert_before_code
(issue.rs:114) handles codes whose canonical key order has origin or
expected before code (e.g. too_small, invalid_format, invalid_type).
Divergences occur when a Rust issue-construction site pushes fields in a
different order than the corresponding TS details object literal.
Files: crates/zodrs/src/issue.rs:64–130, packages/zodrs/src/core/interpreter.ts:56–65
TS interpreter.ts includes a received field on invalid_type issues for
NaN ("NaN"), Infinity ("Infinity"), and invalid Date ("Invalid Date")
cases (lines 654, 697). Rust validate.rs:invalid_type (line 154) emits only
code and expected — no received field. Since compare.ts treats
undefined as absent, a missing received on the Rust side matches an absent
received on the TS side, but a present received on the TS side with no
corresponding Rust field is a mismatch.
Files: crates/zodrs/src/validate.rs:154–156, packages/zodrs/src/core/interpreter.ts:652–654,697
TS interpreter.ts conditionally includes origin: "string" on
invalid_format issues — present for regex-backed formats (line 284), absent
for procedural formats like jwt, ipv6, cidrv6, base64, base64url
(line 278). Rust validate.rs:format_issue (line 1061) includes origin when
a pattern is present but omits it for patternless formats (line 1068). The
two sides must agree on which formats are "procedural" (no origin) vs
"regex-backed" (with origin).
Files: crates/zodrs/src/validate.rs:1053–1072, packages/zodrs/src/core/interpreter.ts:259–288
Rust uses f64 and num_json() to serialize numbers in issue payload fields
(minimum, maximum, divisor). TS uses JavaScript numbers. Edge cases:
-0 serializes as 0 in Rust but -0 in JS. The shared comparator uses
Object.is for numbers, so it reports that difference (compare.ts:81–83).
Large integers beyond MAX_SAFE_INT (2^53-1) may lose precision in f64.
compare.ts:show preserves -0, NaN, and Infinity in mismatch reports,
so reports also retain these numeric edge cases.
Files: crates/zodrs/src/validate.rs:27,1029–1052, packages/conformance/differential/compare.ts:61–134
Both sides flatten union branch errors into invalid_union issues with an
errors array of per-branch issue arrays. TS interpreter.ts (lines 547,
554, 577) has three distinct paths: first-success-wins, zero-success
(errors: branchErrors), and multi-success (errors: [], inclusive: false).
It also surfaces a single non-aborted branch's own issues instead of
invalid_union (line 569–575). Rust validate.rs (lines 516, 985) has two
paths: zero-success and disc-union no-match. The non-aborted-branch surfacing
and inclusive: false multi-match paths may not have Rust equivalents.
Files: crates/zodrs/src/validate.rs:510–520,980–990, packages/zodrs/src/core/interpreter.ts:540–578
Rust scan.rs has a 128-entry cap for duplicate-key detection in the byte
scanner; beyond that, dirty_hint is set and the DOM walk handles it
(validate.rs Record arm uses HashMap for O(n) duplicate detection). TS
interpreter.ts uses Reflect.ownKeys which naturally de-duplicates (last
value wins). The issue sets may differ when the scanner's cap changes which
path processes the input, or when the DOM walk's HashMap-based detection
produces different unrecognized_keys issues than Reflect.ownKeys.
Files: crates/zodrs/src/scan.rs, crates/zodrs/src/validate.rs:360–390, packages/zodrs/src/core/interpreter.ts:766–818
Both sides include note fields on certain issues: "Integers must be within
the safe integer range." on too_big/too_small for int format checks
(Rust validate.rs:720,732; TS does not include this note — it's absent
from interpreter.ts number_format check, line 243–249), and "No matching
discriminator" on invalid_union for disc-union no-match (Rust
validate.rs:1020; TS interpreter.ts:525). The note field presence must
match on both sides.
Files: crates/zodrs/src/validate.rs:706–734,1018–1022, packages/zodrs/src/core/interpreter.ts:243–249,522–528
The native path back-fills input by walking the issue's path against the
original JSON.parse'd value (parse.ts:backFillInput, line 207). If the
Rust-constructed path differs from what the TS interpreter would have
constructed (e.g., different key order in a record, or a missing path segment
for a nested issue), the back-filled input will be the wrong value, causing
a data mismatch in the input field. Sub-issues inside errors/issues
carry paths relative to their parent, so the parent's path is prepended
(line 210–211).
Files: packages/zodrs/src/core/parse.ts:204–225, crates/zodrs/src/issue.rs:17–62
TS interpreter.ts emits invalid_value with values: check.v for MIME
type checks (line 314). Rust validate.rs must match this — emitting
invalid_value (not invalid_format) with the same values array. A
code-level mismatch here would produce a diffTag of issue:code in the
fuzz harness.
Files: crates/zodrs/src/validate.rs (MIME check path), packages/zodrs/src/core/interpreter.ts:314
TS interpreter.ts emits invalid_format with format: "template_literal"
and pattern: node.pattern.source (line 895). Rust validate.rs emits
invalid_format with format: "template_literal" but without a pattern
field (line 432–436). The missing pattern field is a payload divergence.
Files: crates/zodrs/src/validate.rs:428–437, packages/zodrs/src/core/interpreter.ts:895
TS interpreter.ts sets origin: typeof value (which is "number" or
"bigint") on not_multiple_of issues (line 235). Rust validate.rs sets
origin: "number" (line 686). For bigint inputs, the TS side produces
origin: "bigint" while the Rust side may produce origin: "number" or
omit origin entirely — a payload field mismatch.
Files: crates/zodrs/src/validate.rs:682–690, packages/zodrs/src/core/interpreter.ts:233–236
TS interpreter.ts emits the disc-union no-match invalid_union issue at
path [...path, node.key] (line 522) with fields errors: [], note,
discriminator, options (lines 523–528). Rust validate.rs emits at
path [...path, disc_key] (line 1016–1017) with errors, note,
discriminator, options (lines 1018–1022). The field sets match, but the
options array construction may differ: TS uses [...node.map.keys()]
(line 527), Rust uses Json::Array(options) from the compiled plan
(line 1022). If the plan's option ordering differs from the map's insertion
order, the options arrays will differ.
Files: crates/zodrs/src/validate.rs:989–1025, packages/zodrs/src/core/interpreter.ts:511–528
| Component | File | Role |
|---|---|---|
| Fuzz loop | differential/fuzz.test.ts |
Schema-per-50-cases, per-case sub-seeds, mismatch classification, summary |
| Schema generator | differential/genSchema.ts |
Random JSON-eligible plan IR: primitives, formats, objects, arrays, tuples, unions, records, enums, literals, wrappers |
| Input generator | differential/genInput.ts |
Valid, near-miss mutations, adversarial bytes (__proto__, lone surrogates, 1e400, 100-deep arrays, duplicate keys, BOM, NaN/Infinity) |
| Comparator | differential/compare.ts |
Runs both paths, deep-compares results, classifies first difference |
| Mismatch ledger | differential/ledger.ts + KNOWN-MISMATCHES.json |
Confirmed divergences with self-retiring skip rules |
| README | differential/README.md |
Status, knobs, layout |
Run: pnpm -C packages/conformance test:differential (100k cases, plan §4 CI count).
Each item from plan step 9, verified by grep over packages/zodrs/src
(2026-08-07). "Confirmed" = the symbol appears nowhere; the grep output is
recorded.
| Plan residue item | Verdict | Evidence |
|---|---|---|
src/v3/** (legacy major version) |
Confirmed. No v3 surface anywhere in zodrs. | packages/zodrs/package.json exports map has no v3 entry; ls packages/zodrs/src/classic/ shows no v3 tree. Corpus side: packages/conformance/EXCISIONS.md records the removal of core/tests/index.test.ts (the only file importing zod/v3). |
classic/compat.ts (v3 compat shims) |
Confirmed. | No compat.ts under packages/zodrs/src/classic/; grep -rn "compat" packages/zodrs/src matches only the words "compatible"/"compatibility" in comments. Compat symbols (ZodIssueCode, setErrorMap) are absent; EXCISIONS.md "compat-only symbols" section records the vendored tests that reference them. |
params.message alias (deprecated alias for error) |
Corrected: not fully cut. | The plan said cut; the implementation accepts it: errorMap() in classic/schemas.ts resolves params?.error ?? params?.message. Rationale: EXCISIONS.md documents that much of the vendored corpus uses { message: "..." } as a parameter, so the alias stays as a silent fallback to keep the conformance oracle meaningful. Only the v3-throw interaction test ("do not allow error and message together") is excised. invalid_type_error/required_error remain fully cut (grep → no matches). |
core/zsf.ts (type-only, dead) |
Confirmed. | grep -rn "zsf" packages/zodrs/src → no matches. |
core/doc.ts Doc builder (replaced by codegen) |
Confirmed. | grep -rn "Doc" packages/zodrs/src → no matches. Codegen is core/codegen.ts compiled closures (decision 1). |
util.jitless global flag (replaced by capability probing + interpreter) |
Confirmed with a correction. Zod 4.4.3 has no util.jitless symbol; the flag lives on globalConfig.jitless (Zod util.ts:367 reads globalConfig.jitless). |
grep -rn "util.jitless" packages/zodrs/src → no matches. The behavior is essential and present: core/config.ts:9 declares jitless?: boolean, classic/schemas.ts:152 selects the interpreter when config().jitless is set, and CODEGEN_AVAILABLE (CG) is the capability probe. Vendored jitless-allows-eval.test.ts pins the config path (see gap 13 for the missing allowsEval export). |
$ZodObject/$ZodObjectJIT split (one object implementation) |
Confirmed. | grep -rn "ZodObjectJIT" packages/zodrs/src → no matches. One "object" node kind in IR, validated by INT's object case and specialized by CG's object case. |
classic/mini duplicated hierarchies (one core, two projections) |
Confirmed. | mini/mini.ts:1–15 is a projection: it imports node/cloneNode from core/nodes.js, wraps the same SchemaNode graph, and inherits validation/parse from the classic $ZodType base. No second validator exists. |
Additional residue identified during the walk (beyond the plan's list):
| Residue | Reason |
|---|---|
ParsePayload wrapper object ({value, issues} per node) |
Replaced by direct value returns + FAIL sentinel and a lazy per-parse issue array (INT ValidationContext); zero-allocation success path. |
numericOriginMap (checks.ts:55) |
Internal lookup for issue origin strings; INT derives origin directly from the node kind in applyChecksSync. |
_$ZodTypeInternals lazy values/pattern/propValues machinery (util.defineLazy) |
zodrs computes discriminators at factory time (discriminantValues, CL) and template patterns at construction (templatePattern, CL); no per-instance lazy internals. |
Per-direction $ZodCodec/$ZodPipe handleCodec* direction dispatch |
Cut together with the unimplemented encode direction; tracked as gap 2, not silently dropped. |
| Unit | Class | zodrs | CONTRACT |
|---|---|---|---|
$ZodType (base init, checks attach, run) |
essential | IR NodeCommon; CL $ZodType base class (schemas.ts:132); INT runSync/applyChecksSync |
§1, §6 |
$ZodString |
essential | IR "string"; INT/CG string cases; CL string() |
§4 |
$ZodNumber (incl. NaN/±Infinity rejection) |
essential | IR "number"; INT/CG number cases; CL number() |
§4 |
$ZodNumberFormat |
essential | check number_format; FMT; CL int32() etc. |
§6 |
$ZodBoolean |
essential | IR "boolean"; CL boolean() |
§4 |
$ZodBigInt |
essential | IR "bigint"; CL bigint() |
§4 |
$ZodBigIntFormat |
essential | check bigint_format; FMT; CL int64()/uint64() |
§6 |
$ZodSymbol |
essential | IR "symbol"; CL symbol() |
§4 |
$ZodUndefined |
essential | IR "undefined"; CL undefined_() |
§4 |
$ZodNull |
essential | IR "null"; CL null_() |
§4 |
$ZodAny |
essential | IR "any"; CL any() |
§4 |
$ZodUnknown |
essential | IR "unknown"; CL unknown() |
§4 |
$ZodNever |
essential | IR "never"; CL never() |
§4 |
$ZodVoid |
essential | IR "void"; CL void_() |
§4 |
$ZodDate (invalid-Date rejection) |
essential | IR "date"; CL date() |
§4 |
$ZodNaN |
essential | IR "nan"; CL nan() |
§4 |
$ZodFile |
essential | IR "file"; CL file() |
§12 |
$ZodLiteral |
essential | IR "literal"; CL literal() |
§4 |
$ZodEnum (values, .enum access, .options, extract/exclude) |
essential, partially missing | IR "enum"; CL enum_(); member access/extract/exclude → gap 6 |
§4 |
All are format checks ({c:"format", v: FormatId}) over FMT engines, with
classic factories CL:706–741 and iso.* (CL:741); Rust mirrors them in
crates/zodrs/src/formats.rs (hand-written scanners for the four lookaround
formats). CONTRACT §5 covers every row.
| Unit | Class | zodrs |
|---|---|---|
$ZodStringFormat (base) |
essential | FMT testFormat; check {c:"format"} |
$ZodGUID / $ZodUUID (+v4/v6/v7 patterns) |
essential | FMT; CL guid()/uuid()/uuidv4/6/7() |
$ZodEmail (default lookaround + html5/rfc5322/unicode variants) |
essential | FMT; CL email(); RS hand scanner |
$ZodURL (new URL semantics, protocol/hostname params; httpUrl) |
essential | FMT; CL url()/httpUrl() |
$ZodEmoji |
essential | FMT; CL emoji() |
$ZodNanoID |
essential | FMT; CL nanoid() |
$ZodCUID / $ZodCUID2 |
essential | FMT; CL cuid()/cuid2() |
$ZodULID / $ZodXID / $ZodKSUID |
essential | FMT; CL ulid()/xid()/ksuid() |
$ZodISODateTime (precision/offset/local + semantic ranges) |
essential | FMT; CL iso.datetime(); RS via jiff |
$ZodISODate / $ZodISOTime |
essential | FMT; CL iso.date()/iso.time() |
$ZodISODuration (lookaround → hand scanner) |
essential | FMT; CL iso.duration(); RS hand scanner |
$ZodIPv4 / $ZodIPv6 |
essential | FMT; CL ipv4()/ipv6() |
$ZodMAC (delimiter param) |
essential | FMT; CL mac({delimiter?}) |
$ZodCIDRv4 / $ZodCIDRv6 |
essential | FMT; CL cidrv4()/cidrv6() |
isValidBase64 / $ZodBase64 |
essential | FMT base64 shape+length arithmetic; CL base64() |
isValidBase64URL / $ZodBase64URL |
essential | FMT; CL base64url() |
$ZodE164 |
essential | FMT; CL e164() |
isValidJWT / $ZodJWT (alg matching) |
essential | FMT; CL jwt() |
$ZodCustomStringFormat (named predicate/regex formats) |
essential, type export missing | CL stringFormat() (behavior); ZodCustomStringFormat type → gap 7 |
| Unit | Class | zodrs | CONTRACT |
|---|---|---|---|
$ZodArray + handleArrayResult |
essential | IR "array"; INT/CG array cases |
§8 |
$ZodObject |
essential | IR "object"; INT/CG object cases (strip/strict/passthrough/catchall, schema key order, __proto__ drop) |
§7, §22 |
$ZodObjectJIT |
residue | (none) | JIT split cut; one object implementation (see residue table) |
normalizeDef (catchall shorthand expansion) |
essential (behavior) | CL strict()/passthrough()/strip()/catchall() mode setters on the object node |
§7 |
handlePropertyResult |
essential | INT object per-key parse + path prefixing | §7 |
handleCatchall |
essential | INT object unknown-key catchall branch | §7 |
$ZodTuple |
essential, under-length precheck missing | IR "tuple"; INT tuple case; gap 4 for the single-too_small optStart precheck |
§8 |
getTupleOptStart |
essential, missing | gap 4 | §8 |
handleTupleResult / handleTupleResults (optional-tail truncation, default fill) |
essential | INT tuple loop runs max(input.length, items.length) so trailing .default()/.optional() items fill from undefined |
§8 |
| Unit | Class | zodrs | CONTRACT |
|---|---|---|---|
$ZodUnion + handleUnionResults (first success wins, per-option issue arrays) |
essential | IR "union"; INT/CG union cases |
§9 |
$ZodXor + handleExclusiveUnionResults (exactly-one semantics, inclusive:false multi-match) |
essential | CL xor() (schemas.ts:583–601, counts option successes via a host closure; poisons JSON eligibility, acceptable since xor is rare) |
§9 |
$ZodDiscriminatedUnion (map dispatch, no-match issue, wrapper traversal, unionFallback, backward-direction fallback) |
essential, partially missing | IR "discunion"; INT/CG discunion cases + CL discriminantValues; wrapper traversal/unionFallback → gap 10 |
§9 |
$ZodIntersection |
essential | IR "intersection"; INT intersection case |
§10 |
mergeValues (deep object merge, array element merge, primitive equality, Unmergable intersection throw) |
essential | INT intersection merge branch ({...left, ...right} object merge, Object.is primitives, plain-Error throw). Note: Zod merges nested objects recursively and arrays element-wise; zodrs merges top-level keys only. Deep-merge divergence risk → covered by vendored intersection tests; flagged here as a watch item, not a confirmed gap. |
§10 |
handleIntersectionResults (issue merge from both sides) |
essential | INT intersection case runs both sides against one context | §10 |
| Unit | Class | zodrs | CONTRACT |
|---|---|---|---|
$ZodRecord (key+value parse, parsed-key output) |
essential, exhaustiveness missing | IR "record"; INT record case; enum-key exhaustiveness + partialRecord/looseRecord → gap 5 |
§11 |
$ZodMap + handleMapResult (property-key prefixing vs invalid_key/invalid_element wrapping) |
essential | IR "map"; INT map case (same property-key split, verified line-by-line against Zod) |
§11 |
$ZodSet + handleSetResult |
essential | IR "set"; INT set case |
§11 |
| Unit | Class | zodrs | CONTRACT |
|---|---|---|---|
$ZodOptional + handleOptionalResult |
essential | IR "optional"; INT/CG optional cases |
§14 |
$ZodExactOptional |
essential, wrong semantics | CL .exactOptional() aliases .optional() → gap 3 |
§14 |
$ZodNullable |
essential | IR "nullable"; INT/CG |
§14 |
$ZodDefault + handleDefaultResult (short-circuit, dynamic values, decode-only) |
essential, decode-only short-circuit unenforced | IR "default"; INT default case; encode-side → gap 2 |
§14 |
$ZodPrefault (pre-parse default) |
essential | IR "prefault"; INT prefault case |
§14 |
$ZodNonOptional + handleNonOptionalResult |
essential | IR "nonoptional"; INT nonoptional case |
§14 |
$ZodSuccess |
essential, classic factory missing | MINI ZodMiniSuccess (host transform); classic → gap 8 |
§13/§14 |
$ZodCatch (failure substitution, ctx-aware dynamic values) |
essential | IR "catch"; INT catch case |
§14 |
$ZodReadonly + handleReadonlyResult (shallow Object.freeze) |
essential | IR "readonly"; INT readonly case (shallow freeze, matching Zod). CONTRACT §14 says "deep-freezes"; the contract wording is wrong; Zod 4.4.3 freezes shallowly (handleReadonlyResult, schemas.ts:4210). Recorded here as a contract correction. |
§14 |
$ZodTransform (fallback marking, ctx.addIssue, thrown-error propagation) |
essential | IR "host" op transform; INT host case |
§15 |
$ZodPipe + handlePipeResult |
essential, reverse missing | IR "pipe"; INT pipe case; encode direction → gap 2 |
§15 |
$ZodCodec + handleCodecAResult + handleCodecTxResult |
essential, encode half missing | CL codec() builds decode pipe only → gap 2; invertCodec classic → gap 9 |
§15 |
$ZodPreprocess |
essential | CL preprocess() = pipe(host transform, schema) |
§15 |
$ZodLazy |
essential | IR "lazy"; INT/CG lazy cases (reserved-id cycle handling in plan emission, core/plan.ts) |
§13 |
$ZodPromise (async-only) |
essential | IR "promise"; INT promise case (returns a Promise; sync contexts surface $ZodAsyncError from PARSE) |
§13 |
$ZodCustom + handleRefineResult |
essential | CL custom(); INT host case (refine/superRefine/check ops) |
§13, §6 |
$ZodTemplateLiteral (part-to-pattern compilation, stringification) |
essential, narrower input domain | IR "templateLiteral"; CL templatePattern builds the regex at construction; INT matches strings. Zod stringifies number/bigint inputs before matching; zodrs requires typeof "string". Vendored template-literal tests cover this; watch item, not a confirmed gap. |
§13 |
$ZodFunction |
essential, mostly missing | CL function_() bare kind; INT typeof check only → gap 1 |
§13 |
Type-level only; zero runtime. Covered by inference parity, not this walk.
All 21 check classes map to zodrs RuntimeCheck kinds executed by INT
applyChecksSync (+ CG specialized paths and RS validate.rs for the byte
path). Classic check factories: CL:667–703. CONTRACT §6 covers every row;
issue payload fields per §2.
| Unit | Class | zodrs |
|---|---|---|
$ZodCheck (base init, onattach) |
essential | IR RuntimeCheck; CL runtimeCheck() |
$ZodCheckLessThan (lt/lte, inclusive flag, numeric origins) |
essential | check {c:"lt"} incl. inclusive, bigint via string payload |
$ZodCheckGreaterThan (gt/gte) |
essential | check {c:"gt"} |
numericOriginMap |
residue | origin derived from node kind in INT (see residue table) |
$ZodCheckMultipleOf (float-safe, bigint) |
essential | check {c:"multiple_of"} (INT uses EPSILON-tolerant division; bigint via %) |
$ZodCheckNumberFormat (int32/uint32/float32/float64/safeint ranges) |
essential | check {c:"number_format"}; FMT; NUMBER_FORMAT_RANGES in core/util.ts |
$ZodCheckBigIntFormat (int64/uint64) |
essential | check {c:"bigint_format"}; BIGINT_FORMAT_RANGES in core/util.ts |
$ZodCheckMaxSize / $ZodCheckMinSize / $ZodCheckSizeEquals |
essential | checks max_size/min_size/size (Set/Map .size, exact:true on equals) |
$ZodCheckMaxLength / $ZodCheckMinLength / $ZodCheckLengthEquals |
essential | checks max_length/min_length/length |
$ZodCheckStringFormat (format dispatch, when gating) |
essential, when missing |
check {c:"format"} + FMT; when → gap 11 |
$ZodCheckRegex |
essential | check {c:"regex", src, flags} |
$ZodCheckLowerCase / $ZodCheckUpperCase |
essential | checks lowercase/uppercase |
$ZodCheckIncludes (position param) |
essential | check {c:"includes", position?} |
$ZodCheckStartsWith / $ZodCheckEndsWith |
essential | checks starts_with/ends_with |
handleCheckPropertyResult + $ZodCheckProperty |
essential | check {c:"property", key, schema} (INT property branch; issues at [..., key]) |
$ZodCheckMimeType |
essential | check {c:"mime"} (invalid_value against file.type) |
$ZodCheckOverwrite (trim/case/normalize/slugify/custom fn) |
essential | check {c:"overwrite", op, form?} + host_runtime op overwrite; INT applyOverwrite |
Every _foo is Zod's internal factory used to build classic/mini schemas.
The zodrs equivalent is the classic factory in CL (and its MINI projection);
behavior contract as cited. Grouped; overload signatures counted individually.
| Unit(s) | Class | zodrs | CONTRACT |
|---|---|---|---|
_string, _coercedString |
essential | CL string(), coerce.string() |
§4, §17 |
_number, _coercedNumber |
essential | CL number(), coerce.number() |
§4, §17 |
_boolean, _coercedBoolean |
essential | CL boolean(), coerce.boolean() |
§4, §17 |
_bigint, _coercedBigint (SyntaxError propagation) |
essential | CL bigint(), coerce.bigint() |
§4, §17 |
_date, _coercedDate |
essential | CL date(), coerce.date() |
§4, §17 |
_symbol, _undefined, _null, _any, _unknown, _never, _void, _nan |
essential | CL symbol(), undefined_(), null_(), any(), unknown(), never(), void_(), nan() |
§4 |
_file |
essential | CL file() |
§12 |
_literal (×3 overloads) |
essential | CL literal() |
§4 |
_enum (×3 overloads), _nativeEnum |
essential | CL enum_(), nativeEnum |
§4 (extras → gap 6) |
_array |
essential | CL array() |
§8 |
_union |
essential | CL union() |
§9 |
_xor |
essential | CL xor() |
§9 |
_discriminatedUnion |
essential, partial | CL discriminatedUnion(); traversal/unionFallback → gap 10 |
§9 |
_intersection |
essential | CL intersection() |
§10 |
_tuple (×3 overloads) |
essential | CL tuple() |
§8 (precheck → gap 4) |
_record |
essential, partial | CL record(); exhaustiveness/partialRecord/looseRecord → gap 5 |
§11 |
_map, _set |
essential | CL map(), set() |
§11 |
_transform |
essential | CL transform() |
§15 |
_optional, _nullable |
essential | CL optional(), nullable() |
§14 |
_default |
essential | CL .default()/node "default" |
§14 |
_nonoptional |
essential | CL nonoptional() |
§14 |
_success |
essential, missing in classic | MINI ZodMiniSuccess; → gap 8 |
§13 |
_catch |
essential | CL .catch()/node "catch" |
§14 |
_pipe |
essential | CL pipe()/.pipe() |
§15 (encode → gap 2) |
_readonly |
essential | CL readonly() |
§14 |
_templateLiteral |
essential | CL templateLiteral() |
§13 |
_lazy |
essential | CL lazy() |
§13 |
_promise |
essential | CL promise() |
§13 |
_custom |
essential | CL custom() |
§13 |
_refine, _superRefine, _check |
essential | CL .refine()/.superRefine()/.check() methods; MINI free refine/superRefine/check (mini.ts:1160–1170) |
§6 |
All essential; each maps to the CL formatted factory of the same name
(CL:706–741) over FMT, contract §5. when param support → gap 11 applies to
all of them.
_email, _guid, _uuid, _uuidv4, _uuidv6, _uuidv7, _url, _emoji,
_nanoid, _cuid, _cuid2, _ulid, _xid, _ksuid, _ipv4, _ipv6,
_mac, _cidrv4, _cidrv6, _base64, _base64url, _e164, _jwt,
_isoDateTime, _isoDate, _isoTime, _isoDuration, _stringFormat
(→ CL stringFormat; ZodCustomStringFormat type → gap 7).
TimePrecision (const), essential. CL:772 TimePrecision
({minute:-1, second:0, millisecond:3, microsecond:6}), used by
iso.datetime({precision})/iso.time({precision}). §5.
All essential; each maps to the CL check factory of the same name
(CL:667–703), contract §6. when → gap 11.
_lt, _lte, _gt, _gte, _positive, _negative, _nonpositive,
_nonnegative, _multipleOf, _maxSize, _minSize, _size, _maxLength,
_minLength, _length, _regex, _lowercase, _uppercase, _includes,
_startsWith, _endsWith, _property, _mime, _overwrite, _normalize,
_trim, _toLowerCase, _toUpperCase, _slugify.
Number/bigint format factories: _int, _float32, _float64, _int32,
_uint32, _int64, _uint64, essential. CL int(), float32(),
float64(), int32(), uint32(), int64(), uint64() (CL:682–688). §6.
| Unit | Class | zodrs | CONTRACT |
|---|---|---|---|
describe (api.ts:1696) |
essential, core export missing | .describe() method exists; free function → gap 14 |
§18 |
meta (api.ts:1709) |
essential, core export missing | .meta() method exists; free function → gap 14 |
§18, §19 |
_stringbool |
essential, decode-only | CL stringbool() (default truthy/falsy sets, case-insensitive, invalid_value on unrecognized); encode → gap 2 |
§16 |
Params, TypeParams, CheckParams, StringFormatParams,
CheckStringFormatParams, CheckTypeParams, and the per-factory *Params
aliases are type-level; zero runtime. The runtime-relevant param behavior
(unified error param, string shorthand, {error} object form) is essential
and lives in CL errorMap(); the deprecated message alias status is recorded
in the residue table. CONTRACT §3.
- Intersection deep merge: Zod
mergeValuesmerges nested objects recursively and arrays element-wise; INT merges top-level keys ({...left, ...right}). Pinned by vendored intersection tests; if those go green without recursive merge, record the shallow merge as an intentional divergence here. templateLiteralinput domain: Zod stringifies number/bigint inputs before matching; INT requires a string. Watch vendored template-literal tests.- CONTRACT wording corrections identified by this audit: §14 says readonly
"deep-freezes"; Zod and zodrs both freeze shallowly; §9 claims
discriminator features (wrapper traversal,
unionFallback) that are gap 10; §11 documentspartialRecord/looseRecordthat are gap 5; §19 says JSON Schema is "emitted by the Rust core", superseded by decision 2. z.json(): present in zodrs (CL:774) as a classic-surface factory. It is not a unit of the three walked files (it lives in Zod's classic layer), so it is outside this walk's scope; noted for completeness.