From 611122331651855858b15000567917549114cd5a Mon Sep 17 00:00:00 2001 From: Alessandro Bellesia Date: Thu, 31 Jul 2025 12:00:47 +0200 Subject: [PATCH] New zod@4 defaultObjectBySchema fix: remove JSON Schema conversion for zod@4 `defaultObjectBySchema` --- src/utils.ts | 212 +++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 182 insertions(+), 30 deletions(-) diff --git a/src/utils.ts b/src/utils.ts index cf9e97a..b19060f 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -7,31 +7,74 @@ import { safeParseAsync as z4SafeParseAsync, formatError as z4FormatError, } from 'zod/v4/core' -import { toJSONSchema as z4toJSONSchema } from 'zod/v4' import type * as z3 from 'zod/v3' import type * as z4 from 'zod/v4/core' import type { FormSchema, InferSchema, VvZodError, ZodIssue } from './types' +// Helper function to determine the type of a value +function _getValueType(value: unknown) { + if (Array.isArray(value)) { + return 'array' + } + if (value === null) { + return 'null' + } + return typeof value +} + +// Helper function to check if a value matches a schema type +function _isValueCompatibleWithSchema(value: unknown, subSchema: z4.JSONSchema.JSONSchema): boolean { + const valueType = _getValueType(value) + + if (subSchema.type) { + return subSchema.type === valueType + || (subSchema.type === 'integer' && valueType === 'number' && Number.isInteger(value as number)) + } + + // If no type specified, assume compatibility + return true +} + export const isZod3Object = (value: z3.ZodTypeAny): value is z3.ZodObject => { return value._def.typeName === 'ZodObject' } +export const isZod4Object = (value: z4.$ZodType): value is z4.$ZodObject => { + return value._zod.def.type === 'object' +} + export const isZod3Default = (value: z3.ZodTypeAny): value is z3.ZodDefault => { return value._def.typeName === 'ZodDefault' } +export const isZod4Default = (value: z4.$ZodType): value is z4.$ZodDefault => { + return value._zod.def.type === 'default' +} + export const isZod3Nullable = (value: z3.ZodTypeAny): value is z3.ZodNullable => { return value._def.typeName === 'ZodNullable' } +export const isZod4Nullable = (value: z4.$ZodType): value is z4.$ZodNullable => { + return value._zod.def.type === 'nullable' +} + export const isZod3Record = (value: z3.ZodTypeAny): value is z3.ZodRecord => { return value._def.typeName === 'ZodRecord' } +export const isZod4Record = (value: z4.$ZodType): value is z4.$ZodRecord => { + return value._zod.def.type === 'record' +} + export const isZod3Array = (value: z3.ZodTypeAny): value is z3.ZodArray => { return value._def.typeName === 'ZodArray' } +export const isZod4Array = (value: z4.$ZodType): value is z4.$ZodArray => { + return value._zod.def.type === 'array' +} + export const isZod3Effects = (value: z3.ZodTypeAny): value is z3.ZodEffects => { return value._def.typeName === 'ZodEffects' } @@ -40,8 +83,21 @@ export const isZod3Optional = (value: z3.ZodTypeAny): value is z3.ZodOptional( +export const isZod4Optional = (value: z4.$ZodType): value is z4.$ZodOptional => { + return value._zod.def.type === 'optional' +} + +// replace of effercts +export const isZod4Pipe = (value: z4.$ZodType): value is z4.$ZodPipe => { + return value._zod.def.type === 'pipe' +} + +export const isZod4Transform = (value: z4.$ZodType): value is z4.$ZodTransform => { + return value._zod.def.type === 'transform' +} + +// Helper function to get the inner type of a Zod schema +export const getZod3SchemaInnerType = ( schema: | Type | z3.ZodEffects @@ -58,8 +114,30 @@ const getZod3SchemaInnerType = ( return toReturn } -// Helper function to check if a Zod v3 schema is optional -const isZod3SchemaOptional = ( +export const getZod4SchemaInnerType = ( + schema: + | Type + | z4.$ZodPipe + | z4.$ZodPipe + | z4.$ZodOptional, +) => { + let toReturn = schema + while (isZod4Pipe(toReturn)) { + if (isZod4Transform(toReturn._zod.def.out)) { + toReturn = toReturn._zod.def.in + } + else { + toReturn = toReturn._zod.def.out as Type + } + } + if (isZod4Optional(toReturn)) { + toReturn = toReturn._zod.def.innerType + } + return toReturn +} + +// Helper function to check if a Zod schema is optional +export const isZod3SchemaOptional = ( schema: | Type | z3.ZodEffects @@ -76,28 +154,26 @@ const isZod3SchemaOptional = ( return false } -// Helper function to determine the type of a value -function getValueType(value: unknown) { - if (Array.isArray(value)) { - return 'array' - } - if (value === null) { - return 'null' +export const isZod4SchemaOptional = ( + schema: + | Type + | z4.$ZodPipe + | z4.$ZodPipe + | z4.$ZodOptional, +) => { + let toReturn = schema + while (isZod4Pipe(toReturn)) { + if (isZod4Transform(toReturn._zod.def.out)) { + toReturn = toReturn._zod.def.in + } + else { + toReturn = toReturn._zod.def.out as Type + } } - return typeof value -} - -// Helper function to check if a value matches a schema type -function isValueCompatibleWithSchema(value: unknown, subSchema: z4.JSONSchema.JSONSchema): boolean { - const valueType = getValueType(value) - - if (subSchema.type) { - return subSchema.type === valueType - || (subSchema.type === 'integer' && valueType === 'number' && Number.isInteger(value as number)) + if (isZod4Optional(toReturn)) { + return true } - - // If no type specified, assume compatibility - return true + return false } export function defaultObjectByJSONSchema(schema: z4.JSONSchema.JSONSchema, original?: unknown): unknown { @@ -106,7 +182,7 @@ export function defaultObjectByJSONSchema(schema: z4.JSONSchema.JSONSchema, orig if (original !== undefined) { // First pass: find exact type match for (const subSchema of schema.anyOf) { - if (isValueCompatibleWithSchema(original, subSchema as z4.JSONSchema.JSONSchema)) { + if (_isValueCompatibleWithSchema(original, subSchema as z4.JSONSchema.JSONSchema)) { return defaultObjectByJSONSchema(subSchema as z4.JSONSchema.JSONSchema, original) } } @@ -183,12 +259,88 @@ export const isZod4Schema = (schema: z3.ZodTypeAny | z4.$ZodType): schema is z4. export function defaultObjectBySchema(schema: Schema, original: Partial> & Record = {}): Partial> { // zod v4 if (isZod4Schema(schema)) { - const jsonSchema = z4toJSONSchema(schema) - if (jsonSchema.type !== 'object' || !jsonSchema.properties) { + const innerType = getZod4SchemaInnerType(schema) + if (!isZod4Object(innerType)) { return original } - const parse = z4SafeParse(schema, original) - return defaultObjectByJSONSchema(jsonSchema, parse.success ? parse.data : original) as Partial> + const unknownKeys = !(!innerType._zod.def.catchall || innerType._zod.def.catchall?._zod.def.type === 'never') + + return { + ...(unknownKeys ? original : {}), + ...Object.fromEntries( + ('shape' in innerType._zod.def ? Object.entries(innerType._zod.def.shape) as [string, z4.$ZodType][] : []).map( + ([key, subSchema]) => { + const originalValue = original[key] + const isOptional = isZod4SchemaOptional(subSchema) + let innerType = getZod4SchemaInnerType(subSchema) + let defaultValue: Partial> | undefined + if (isZod4Default(innerType)) { + defaultValue = innerType._zod.def.defaultValue + innerType = innerType._zod.def.innerType + } + if ( + originalValue === null + && isZod4Nullable(innerType) + ) { + return [key, originalValue] + } + if ((originalValue === undefined || originalValue === null) && isOptional) { + return [key, defaultValue] + } + if (innerType) { + const parse = z4SafeParse(subSchema, originalValue) + if (parse.success) { + return [key, parse.data ?? defaultValue] + } + } + if ( + isZod4Array(innerType) + && Array.isArray(originalValue) + && originalValue.length + ) { + const arrayType = getZod4SchemaInnerType(innerType._zod.def.element) + if (isZod4Object(arrayType)) { + return [ + key, + originalValue.map((element: unknown) => + defaultObjectBySchema( + arrayType, + (element && typeof element === 'object' + ? element + : undefined) as Partial< + typeof arrayType + >, + ), + ), + ] + } + } + if (isZod4Record(innerType) && originalValue) { + const valueType = getZod4SchemaInnerType(innerType._zod.def.valueType) + if (isZod4Object(valueType)) { + return [key, Object.keys(originalValue).reduce((acc: Record, recordKey: string) => { + acc[recordKey] = defaultObjectBySchema(valueType, originalValue[recordKey]) + return acc + }, {})] + } + } + if (isZod4Object(innerType)) { + return [ + key, + defaultObjectBySchema( + innerType, + originalValue + && typeof originalValue === 'object' + ? originalValue + : defaultValue, + ), + ] + } + return [key, defaultValue] + }, + ), + ), + } as Partial> } // zod v3