|
|
@@ -8,6 +8,14 @@ import z from "zod"
|
|
|
*/
|
|
|
export const ZodOverride: unique symbol = Symbol.for("effect-zod/override")
|
|
|
|
|
|
+/**
|
|
|
+ * Annotation key for attaching a `z.preprocess(...)` coercion to the derived
|
|
|
+ * zod schema without changing the Effect Schema's runtime behaviour. Useful
|
|
|
+ * for LLM-facing JSON Schemas that need to accept looser inputs (e.g. string
|
|
|
+ * → number) than the underlying Schema admits.
|
|
|
+ */
|
|
|
+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
|
|
|
@@ -87,13 +95,18 @@ function bodyWithChecks(ast: SchemaAST.AST): z.ZodTypeAny {
|
|
|
// Schema.decodeTo / Schema.transform attach encoding to non-Declaration
|
|
|
// nodes, where we do apply the transform.
|
|
|
//
|
|
|
- // Schema.withDecodingDefault also attaches encoding, but we want `.default(v)`
|
|
|
- // on the inner Zod rather than a transform wrapper — so optional ASTs whose
|
|
|
- // encoding resolves a default from Option.none() route through body()/opt().
|
|
|
+ // Schema.withDecodingDefault and Schema.withDecodingDefaultTypeKey both
|
|
|
+ // attach encodings. For JSON Schema we want those as plain `.default(v)`
|
|
|
+ // annotations rather than transform wrappers, so ASTs whose encoding
|
|
|
+ // resolves a default from Option.none() route through body()/opt().
|
|
|
const hasEncoding = ast.encoding?.length && ast._tag !== "Declaration"
|
|
|
- const hasTransform = hasEncoding && !(SchemaAST.isOptional(ast) && extractDefault(ast) !== undefined)
|
|
|
- const base = hasTransform ? encoded(ast) : body(ast)
|
|
|
- return ast.checks?.length ? applyChecks(base, ast.checks, ast) : base
|
|
|
+ const fallback = hasEncoding ? extractDefault(ast) : undefined
|
|
|
+ const hasTransform = hasEncoding && fallback === undefined
|
|
|
+ const base = hasTransform ? encoded(ast) : body(ast, fallback)
|
|
|
+ const checked = ast.checks?.length ? applyChecks(base, ast.checks, ast) : base
|
|
|
+ const defaulted = fallback !== undefined && !SchemaAST.isOptional(ast) ? checked.default(fallback.value) : checked
|
|
|
+ const preprocess = (ast.annotations as { [ZodPreprocess]?: (val: unknown) => unknown } | undefined)?.[ZodPreprocess]
|
|
|
+ return preprocess ? z.preprocess(preprocess, defaulted) : defaulted
|
|
|
}
|
|
|
|
|
|
// Walk the encoded side and apply each link's decode to produce the decoded
|
|
|
@@ -227,8 +240,8 @@ function issueMessage(issue: any): string | undefined {
|
|
|
return undefined
|
|
|
}
|
|
|
|
|
|
-function body(ast: SchemaAST.AST): z.ZodTypeAny {
|
|
|
- if (SchemaAST.isOptional(ast)) return opt(ast)
|
|
|
+function body(ast: SchemaAST.AST, fallback?: { value: unknown }): z.ZodTypeAny {
|
|
|
+ if (SchemaAST.isOptional(ast)) return opt(ast, fallback)
|
|
|
|
|
|
switch (ast._tag) {
|
|
|
case "String":
|
|
|
@@ -261,7 +274,7 @@ function body(ast: SchemaAST.AST): z.ZodTypeAny {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-function opt(ast: SchemaAST.AST): z.ZodTypeAny {
|
|
|
+function opt(ast: SchemaAST.AST, fallback = extractDefault(ast)): z.ZodTypeAny {
|
|
|
if (ast._tag !== "Union") return fail(ast)
|
|
|
const items = ast.types.filter((item) => item._tag !== "Undefined")
|
|
|
const inner =
|
|
|
@@ -273,7 +286,6 @@ function opt(ast: SchemaAST.AST): z.ZodTypeAny {
|
|
|
// Schema.withDecodingDefault attaches an encoding `Link` whose transformation
|
|
|
// decode Getter resolves `Option.none()` to `Option.some(default)`. Invoke
|
|
|
// it to extract the default and emit `.default(...)` instead of `.optional()`.
|
|
|
- const fallback = extractDefault(ast)
|
|
|
if (fallback !== undefined) return inner.default(fallback.value)
|
|
|
return inner.optional()
|
|
|
}
|