feat(effect-zod): add ZodPreprocess annotation for pre-parse transforms (#23222)
This commit is contained in:
@@ -8,6 +8,43 @@ import z from "zod"
|
||||
*/
|
||||
export const ZodOverride: unique symbol = Symbol.for("effect-zod/override")
|
||||
|
||||
/**
|
||||
* Annotation key for a pre-parse transform that runs on the raw input before
|
||||
* the derived Zod schema validates it. The walker emits
|
||||
* `z.preprocess(fn, inner)` when this annotation is present.
|
||||
*
|
||||
* Models zod's `z.preprocess(fn, schema)` pattern — useful when the schema
|
||||
* needs to inspect the user's raw input (e.g. to capture insertion order)
|
||||
* before `Schema.Struct` canonicalises the object.
|
||||
*
|
||||
* TODO: This exists to paper over a missing Effect Schema feature. The
|
||||
* parser canonicalises open struct output (known fields first in
|
||||
* declaration order, then catchall fields) before any user-defined
|
||||
* transform sees the value, and there is no pre-parse hook — so the
|
||||
* user's original property insertion order is gone by the time
|
||||
* `Schema.decodeTo` or `middlewareDecoding` runs.
|
||||
*
|
||||
* That canonicalisation is a reasonable default, but `config/permission.ts`
|
||||
* encodes rule precedence in the user's JSON key order (`evaluate.ts`
|
||||
* uses `findLast`, so later entries win), which the canonicalisation
|
||||
* silently destroys.
|
||||
*
|
||||
* The cleanest upstream fix would be either:
|
||||
*
|
||||
* 1. A `preserveInputOrder` option on `Schema.Struct` /
|
||||
* `Schema.StructWithRest` that keeps the input's insertion order in
|
||||
* the parsed object (opt-in; canonical order stays default).
|
||||
* 2. A generic pre-parse hook (`Schema.preprocess(schema, fn)` or a
|
||||
* transformation whose decode receives the raw `unknown`).
|
||||
*
|
||||
* Either of those would let us delete `ZodPreprocess` and the
|
||||
* `__originalKeys` hack. Alternatively, the permission model could move
|
||||
* to specificity-based precedence (exact keys beat wildcards) or an
|
||||
* explicit ordered array of rules, which removes the ordering
|
||||
* dependency at the data-model level.
|
||||
*/
|
||||
export const ZodPreprocess: unique symbol = Symbol.for("effect-zod/preprocess")
|
||||
|
||||
// AST nodes are immutable and frequently shared across schemas (e.g. a single
|
||||
// Schema.Class embedded in multiple parents). Memoizing by node identity
|
||||
// avoids rebuilding equivalent Zod subtrees and keeps derived children stable
|
||||
@@ -47,7 +84,9 @@ function walkUncached(ast: SchemaAST.AST): z.ZodTypeAny {
|
||||
const hasEncoding = ast.encoding?.length && ast._tag !== "Declaration"
|
||||
const hasTransform = hasEncoding && !(SchemaAST.isOptional(ast) && extractDefault(ast) !== undefined)
|
||||
const base = hasTransform ? encoded(ast) : body(ast)
|
||||
const out = ast.checks?.length ? applyChecks(base, ast.checks, ast) : base
|
||||
const checked = ast.checks?.length ? applyChecks(base, ast.checks, ast) : base
|
||||
const preprocess = (ast.annotations as { [ZodPreprocess]?: (val: unknown) => unknown } | undefined)?.[ZodPreprocess]
|
||||
const out = preprocess ? z.preprocess(preprocess, checked) : checked
|
||||
const desc = SchemaAST.resolveDescription(ast)
|
||||
const ref = SchemaAST.resolveIdentifier(ast)
|
||||
const described = desc ? out.describe(desc) : out
|
||||
|
||||
Reference in New Issue
Block a user