From 5b98f0f64f6b0a93cd5bd4bf274e67032a081ab3 Mon Sep 17 00:00:00 2001 From: Himanshu Rai Date: Wed, 9 Jul 2025 17:14:08 +0530 Subject: [PATCH] Fix string length validation inconsistency between zod and validator --- zod.go | 14 +++++++------- zod_test.go | 30 +++++++++++++++--------------- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/zod.go b/zod.go index c595dfe..d3c4f2f 100644 --- a/zod.go +++ b/zod.go @@ -947,27 +947,27 @@ func (c *Converter) validateString(validate string) string { // const FishEnum = z.enum(["Salmon", "Tuna", "Trout"]); validateStr.WriteString(fmt.Sprintf(".enum([\"%s\"] as const)", strings.Join(vals, "\", \""))) case "len": - validateStr.WriteString(fmt.Sprintf(".length(%s)", valValue)) + refines = append(refines, fmt.Sprintf(".refine((val) => [...val].length === %s, 'String must contain %s character(s)')", valValue, valValue)) case "min": - validateStr.WriteString(fmt.Sprintf(".min(%s)", valValue)) + refines = append(refines, fmt.Sprintf(".refine((val) => [...val].length >= %s, 'String must contain at least %s character(s)')", valValue, valValue)) case "max": - validateStr.WriteString(fmt.Sprintf(".max(%s)", valValue)) + refines = append(refines, fmt.Sprintf(".refine((val) => [...val].length <= %s, 'String must contain at most %s character(s)')", valValue, valValue)) case "gt": val, err := strconv.Atoi(valValue) if err != nil { panic("gt= must be followed by a number") } - validateStr.WriteString(fmt.Sprintf(".min(%d)", val+1)) + refines = append(refines, fmt.Sprintf(".refine((val) => [...val].length > %d, 'String must contain at least %d character(s)')", val, val+1)) case "gte": - validateStr.WriteString(fmt.Sprintf(".min(%s)", valValue)) + refines = append(refines, fmt.Sprintf(".refine((val) => [...val].length >= %s, 'String must contain at least %s character(s)')", valValue, valValue)) case "lt": val, err := strconv.Atoi(valValue) if err != nil { panic("lt= must be followed by a number") } - validateStr.WriteString(fmt.Sprintf(".max(%d)", val-1)) + refines = append(refines, fmt.Sprintf(".refine((val) => [...val].length < %d, 'String must contain at most %d character(s)')", val, val-1)) case "lte": - validateStr.WriteString(fmt.Sprintf(".max(%s)", valValue)) + refines = append(refines, fmt.Sprintf(".refine((val) => [...val].length <= %s, 'String must contain at most %s character(s)')", valValue, valValue)) case "contains": validateStr.WriteString(fmt.Sprintf(".includes(\"%s\")", valValue)) case "endswith": diff --git a/zod_test.go b/zod_test.go index c19decc..2aeed41 100644 --- a/zod_test.go +++ b/zod_test.go @@ -394,10 +394,10 @@ func TestNullableWithValidations(t *testing.T) { PtrInt2: z.number().gte(2).lte(5), PtrIntNullable: z.number().gte(2).lte(5).nullable(), PtrStringOptional1: z.string().optional(), - PtrStringOptional2: z.string().min(2).max(5).optional(), - PtrString1: z.string().min(2).max(5), - PtrString2: z.string().min(2).max(5), - PtrStringNullable: z.string().min(2).max(5).nullable(), + PtrStringOptional2: z.string().refine((val) => [...val].length >= 2, 'String must contain at least 2 character(s)').refine((val) => [...val].length <= 5, 'String must contain at most 5 character(s)').optional(), + PtrString1: z.string().refine((val) => [...val].length >= 2, 'String must contain at least 2 character(s)').refine((val) => [...val].length <= 5, 'String must contain at most 5 character(s)'), + PtrString2: z.string().refine((val) => [...val].length >= 2, 'String must contain at least 2 character(s)').refine((val) => [...val].length <= 5, 'String must contain at most 5 character(s)'), + PtrStringNullable: z.string().refine((val) => [...val].length >= 2, 'String must contain at least 2 character(s)').refine((val) => [...val].length <= 5, 'String must contain at most 5 character(s)').nullable(), }) export type User = z.infer @@ -487,7 +487,7 @@ export type OneOfSeparated = z.infer } assert.Equal(t, `export const LenSchema = z.object({ - Name: z.string().length(5), + Name: z.string().refine((val) => [...val].length === 5, 'String must contain 5 character(s)'), }) export type Len = z.infer @@ -499,7 +499,7 @@ export type Len = z.infer } assert.Equal(t, `export const MinSchema = z.object({ - Name: z.string().min(5), + Name: z.string().refine((val) => [...val].length >= 5, 'String must contain at least 5 character(s)'), }) export type Min = z.infer @@ -511,7 +511,7 @@ export type Min = z.infer } assert.Equal(t, `export const MaxSchema = z.object({ - Name: z.string().max(5), + Name: z.string().refine((val) => [...val].length <= 5, 'String must contain at most 5 character(s)'), }) export type Max = z.infer @@ -523,7 +523,7 @@ export type Max = z.infer } assert.Equal(t, `export const MinMaxSchema = z.object({ - Name: z.string().min(3).max(7), + Name: z.string().refine((val) => [...val].length >= 3, 'String must contain at least 3 character(s)').refine((val) => [...val].length <= 7, 'String must contain at most 7 character(s)'), }) export type MinMax = z.infer @@ -535,7 +535,7 @@ export type MinMax = z.infer } assert.Equal(t, `export const GtSchema = z.object({ - Name: z.string().min(6), + Name: z.string().refine((val) => [...val].length > 5, 'String must contain at least 6 character(s)'), }) export type Gt = z.infer @@ -547,7 +547,7 @@ export type Gt = z.infer } assert.Equal(t, `export const GteSchema = z.object({ - Name: z.string().min(5), + Name: z.string().refine((val) => [...val].length >= 5, 'String must contain at least 5 character(s)'), }) export type Gte = z.infer @@ -559,7 +559,7 @@ export type Gte = z.infer } assert.Equal(t, `export const LtSchema = z.object({ - Name: z.string().max(4), + Name: z.string().refine((val) => [...val].length < 5, 'String must contain at most 4 character(s)'), }) export type Lt = z.infer @@ -571,7 +571,7 @@ export type Lt = z.infer } assert.Equal(t, `export const LteSchema = z.object({ - Name: z.string().max(5), + Name: z.string().refine((val) => [...val].length <= 5, 'String must contain at most 5 character(s)'), }) export type Lte = z.infer @@ -1456,7 +1456,7 @@ export type Lte = z.infer } assert.Equal(t, `export const Dive1Schema = z.object({ - Map: z.record(z.string(), z.string().min(2)).nullable(), + Map: z.record(z.string(), z.string().refine((val) => [...val].length >= 2, 'String must contain at least 2 character(s)')).nullable(), }) export type Dive1 = z.infer @@ -1467,7 +1467,7 @@ export type Dive1 = z.infer } assert.Equal(t, `export const Dive2Schema = z.object({ - Map: z.record(z.string(), z.string().min(3)).refine((val) => Object.keys(val).length >= 2, 'Map too small').array(), + Map: z.record(z.string(), z.string().refine((val) => [...val].length >= 3, 'String must contain at least 3 character(s)')).refine((val) => Object.keys(val).length >= 2, 'Map too small').array(), }) export type Dive2 = z.infer @@ -1478,7 +1478,7 @@ export type Dive2 = z.infer } assert.Equal(t, `export const Dive3Schema = z.object({ - Map: z.record(z.string().min(3), z.string().max(4)).refine((val) => Object.keys(val).length >= 2, 'Map too small').array(), + Map: z.record(z.string().refine((val) => [...val].length >= 3, 'String must contain at least 3 character(s)'), z.string().refine((val) => [...val].length <= 4, 'String must contain at most 4 character(s)')).refine((val) => Object.keys(val).length >= 2, 'Map too small').array(), }) export type Dive3 = z.infer