From 6cb16a44796e2c32d557e72d2d591f699178b706 Mon Sep 17 00:00:00 2001 From: Mendy Landa Date: Mon, 9 Feb 2026 16:19:57 +0200 Subject: [PATCH] fix: add Zod 4 compatibility for prefault, transform pipes, and refined schema omit - Handle Zod 4's `prefault` type in `unwrapZodType` (same unwrap logic as `default`) - In `pipe` handling, check if `def.out` is a `transform` type and follow `def.in` instead, since `.transform().superRefine()` in Zod 4 creates a pipe where `def.out` is an opaque transform - Wrap `.omit()` in try/catch in `getRequestBodyObject` to handle refined schemas that throw in Zod 4, falling back to manual shape filtering Fixes #153 --- src/generator/schema.ts | 16 +++++++++++++++- src/utils/zod.ts | 8 ++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/generator/schema.ts b/src/generator/schema.ts index fb9bb1d3..a278ccc2 100644 --- a/src/generator/schema.ts +++ b/src/generator/schema.ts @@ -136,7 +136,21 @@ export const getRequestBodyObject = ( mask[pathParameter] = true; }); const o = schema.meta(); - const dedupedSchema = schema.omit(mask).meta({ + // Use omit() when possible, fall back to manual shape filtering for refined schemas + let dedupedSchema; + try { + dedupedSchema = schema.omit(mask); + } catch { + // Zod 4 throws on .omit() for schemas with refinements - rebuild from shape + const filteredShape: Record = {}; + for (const [key, value] of Object.entries(schema.shape)) { + if (!mask[key]) { + filteredShape[key] = value as z.ZodType; + } + } + dedupedSchema = z.object(filteredShape); + } + dedupedSchema = dedupedSchema.meta({ ...(o?.title ? { title: o?.title } : {}), ...(o?.description ? { description: o?.description } : {}), ...(o?.examples ? { examples: o?.examples } : {}), diff --git a/src/utils/zod.ts b/src/utils/zod.ts index 1ec8f13e..ae58f180 100644 --- a/src/utils/zod.ts +++ b/src/utils/zod.ts @@ -49,10 +49,18 @@ export const unwrapZodType = (type: $ZodType, unwrapPreprocess: boolean): ZodTyp if (instanceofZodTypeKind(type, 'default')) { return unwrapZodType((type as z.ZodDefault<$ZodTypes>).unwrap(), unwrapPreprocess); } + if (instanceofZodTypeKind(type, 'prefault')) { + return unwrapZodType((type as z.ZodDefault<$ZodTypes>).unwrap(), unwrapPreprocess); + } if (instanceofZodTypeKind(type, 'lazy')) { return unwrapZodType((type as z.ZodLazy<$ZodTypes>).def.getter(), unwrapPreprocess); } if (instanceofZodTypeKind(type, 'pipe') && unwrapPreprocess) { + // For .transform() pipes in Zod 4, def.out is a 'transform' type - + // use def.in (the input schema shape) instead + if (instanceofZodTypeKind((type as z.ZodPipe<$ZodTypes>)._zod.def.out, 'transform')) { + return unwrapZodType((type as z.ZodPipe<$ZodTypes>)._zod.def.in, unwrapPreprocess); + } return unwrapZodType((type as z.ZodPipe<$ZodTypes>).def.out, unwrapPreprocess); } return type as ZodType;