Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
212 changes: 182 additions & 30 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<any> => {
return value._def.typeName === 'ZodObject'
}

export const isZod4Object = (value: z4.$ZodType): value is z4.$ZodObject<any> => {
return value._zod.def.type === 'object'
}

export const isZod3Default = (value: z3.ZodTypeAny): value is z3.ZodDefault<any> => {
return value._def.typeName === 'ZodDefault'
}

export const isZod4Default = (value: z4.$ZodType): value is z4.$ZodDefault<any> => {
return value._zod.def.type === 'default'
}

export const isZod3Nullable = (value: z3.ZodTypeAny): value is z3.ZodNullable<any> => {
return value._def.typeName === 'ZodNullable'
}

export const isZod4Nullable = (value: z4.$ZodType): value is z4.$ZodNullable<any> => {
return value._zod.def.type === 'nullable'
}

export const isZod3Record = (value: z3.ZodTypeAny): value is z3.ZodRecord<any, any> => {
return value._def.typeName === 'ZodRecord'
}

export const isZod4Record = (value: z4.$ZodType): value is z4.$ZodRecord<any, any> => {
return value._zod.def.type === 'record'
}

export const isZod3Array = (value: z3.ZodTypeAny): value is z3.ZodArray<any> => {
return value._def.typeName === 'ZodArray'
}

export const isZod4Array = (value: z4.$ZodType): value is z4.$ZodArray<any> => {
return value._zod.def.type === 'array'
}

export const isZod3Effects = (value: z3.ZodTypeAny): value is z3.ZodEffects<any> => {
return value._def.typeName === 'ZodEffects'
}
Expand All @@ -40,8 +83,21 @@ export const isZod3Optional = (value: z3.ZodTypeAny): value is z3.ZodOptional<an
return value._def.typeName === 'ZodOptional'
}

// Helper function to get the inner type of a Zod v3 schema
const getZod3SchemaInnerType = <Type extends z3.ZodTypeAny>(
export const isZod4Optional = (value: z4.$ZodType): value is z4.$ZodOptional<any> => {
return value._zod.def.type === 'optional'
}

// replace of effercts
export const isZod4Pipe = (value: z4.$ZodType): value is z4.$ZodPipe<any> => {
return value._zod.def.type === 'pipe'
}

export const isZod4Transform = (value: z4.$ZodType): value is z4.$ZodTransform<any> => {
return value._zod.def.type === 'transform'
}

// Helper function to get the inner type of a Zod schema
export const getZod3SchemaInnerType = <Type extends z3.ZodTypeAny>(
schema:
| Type
| z3.ZodEffects<Type>
Expand All @@ -58,8 +114,30 @@ const getZod3SchemaInnerType = <Type extends z3.ZodTypeAny>(
return toReturn
}

// Helper function to check if a Zod v3 schema is optional
const isZod3SchemaOptional = <Type extends z3.ZodTypeAny>(
export const getZod4SchemaInnerType = <Type extends z4.$ZodType>(
schema:
| Type
| z4.$ZodPipe<Type>
| z4.$ZodPipe<any, Type>
| z4.$ZodOptional<Type>,
) => {
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 = <Type extends z3.ZodTypeAny>(
schema:
| Type
| z3.ZodEffects<Type>
Expand All @@ -76,28 +154,26 @@ const isZod3SchemaOptional = <Type extends z3.ZodTypeAny>(
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 = <Type extends z4.$ZodType>(
schema:
| Type
| z4.$ZodPipe<Type>
| z4.$ZodPipe<any, Type>
| z4.$ZodOptional<Type>,
) => {
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 {
Expand All @@ -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)
}
}
Expand Down Expand Up @@ -183,12 +259,88 @@ export const isZod4Schema = (schema: z3.ZodTypeAny | z4.$ZodType): schema is z4.
export function defaultObjectBySchema<Schema extends FormSchema>(schema: Schema, original: Partial<InferSchema<Schema>> & Record<string, unknown> = {}): Partial<InferSchema<Schema>> {
// 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<InferSchema<Schema>>
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<InferSchema<Schema>> | 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<string, unknown>, 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<InferSchema<Schema>>
}

// zod v3
Expand Down