From c6ab0668d8bca78736b65119f40e4767da20e054 Mon Sep 17 00:00:00 2001 From: Satvik Choudhary Date: Mon, 23 Mar 2026 18:37:31 +0530 Subject: [PATCH 01/35] Make Zod v4 the default output --- README.md | 18 +- zod.go | 481 +++++++++++++++++++++++++++++++++++----------------- zod_test.go | 252 +++++++++++++++++++++++---- 3 files changed, 563 insertions(+), 188 deletions(-) diff --git a/README.md b/README.md index 0dd095d..9798070 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,8 @@ Converts Go structs with [go-validator](https://github.com/go-playground/validat Zen supports self-referential types and generic types. Other cyclic types (apart from self referential types) are not supported as they are not supported by zod itself. +Zen emits Zod v4 schemas by default. Use `zen.WithZodV3()` if you need the previous output style for snapshot compatibility or incremental migration. + ## Usage ```go @@ -58,6 +60,19 @@ c.AddType(PairMap[string, int, bool]{}) fmt.Print(c.Export()) ``` +Legacy v3-compatible output is still available: + +```go +fmt.Print(zen.StructToZodSchema(User{}, zen.WithZodV3())) +``` + +The main migration differences are: + +- string format tags such as `email`, `http_url`, `ipv4`, `uuid4`, and `md5` now use Zod v4 helpers like `z.email()`, `z.httpUrl()`, `z.ipv4()`, `z.uuid({ version: "v4" })`, and `z.hash("md5")` +- `ip` and `ip_addr` now emit `z.union([z.ipv4(), z.ipv6()])` +- embedded anonymous structs now expand through `.shape` spreads instead of `.merge(...)` +- enum-like map keys now emit `z.partialRecord(...)` + Outputs: ```typescript @@ -267,7 +282,8 @@ export const RequestSchema = z.object({ end: z.number().gt(0).optional(), }).refine((val) => !val.start || !val.end || val.start < val.end, 'Start should be less than end'), search: z.string().refine((val) => !val || /^[a-z0-9_]*$/.test(val), 'Invalid search identifier').optional(), -}).merge(SortParamsSchema.extend({field: z.enum(['title', 'address', 'age', 'dob'])})) + ...SortParamsSchema.extend({field: z.enum(['title', 'address', 'age', 'dob'])}).shape, +}) export type Request = z.infer ``` diff --git a/zod.go b/zod.go index 84c627b..a0c7aae 100644 --- a/zod.go +++ b/zod.go @@ -49,6 +49,13 @@ func WithIgnoreTags(ignores ...string) Opt { } } +// Emits legacy Zod v3-compatible schemas instead of the default Zod v4 output. +func WithZodV3() Opt { + return func(c *Converter) { + c.zodV3 = true + } +} + // NewConverterWithOpts initializes and returns a new converter instance. func NewConverterWithOpts(opts ...Opt) *Converter { c := &Converter{ @@ -159,11 +166,26 @@ type meta struct { selfRef bool } +type stringSchemaParts struct { + base string + chain string + enumLike bool + isIPUnion bool +} + +type stringSchemaChunk struct { + kind string + text string + v4Base string + legacyChain string +} + type Converter struct { prefix string customTypes map[string]CustomFn customTags map[string]CustomFn ignoreTags []string + zodV3 bool structs int outputs map[string]entry stack []meta @@ -288,12 +310,10 @@ func (c *Converter) getStructShape(input reflect.Type, indent int) string { optional := isOptional(field) nullable := isNullable(field) - line, shouldMerge := c.convertField(field, indent+1, optional, nullable) - - if !shouldMerge { - output.WriteString(line) + if field.Anonymous { + output.WriteString(c.convertEmbeddedFieldSpread(field, indent+1)) } else { - output.WriteString(fmt.Sprintf("%s...%s.shape,\n", indentation(indent+1), schemaName(c.prefix, typeName(field.Type)))) + output.WriteString(c.convertNamedField(field, indent+1, optional, nullable)) } } @@ -310,6 +330,7 @@ func (c *Converter) convertStruct(input reflect.Type, indent int) string { `) merges := []string{} + embeddedFields := []string{} fields := input.NumField() for i := 0; i < fields; i++ { @@ -317,12 +338,25 @@ func (c *Converter) convertStruct(input reflect.Type, indent int) string { optional := isOptional(field) nullable := isNullable(field) - line, shouldMerge := c.convertField(field, indent+1, optional, nullable) + if field.Anonymous { + if c.zodV3 { + line, shouldMerge := c.convertEmbeddedFieldMerge(field, indent+1) + if shouldMerge { + merges = append(merges, line) + } else { + output.WriteString(line) + } + } else { + embeddedFields = append(embeddedFields, c.convertEmbeddedFieldSpread(field, indent+1)) + } + } else { + output.WriteString(c.convertNamedField(field, indent+1, optional, nullable)) + } + } - if !shouldMerge { + if !c.zodV3 { + for _, line := range embeddedFields { output.WriteString(line) - } else { - merges = append(merges, line) } } @@ -490,9 +524,16 @@ func (c *Converter) ConvertType(t reflect.Type, validate string, indent int) str if validate != "" { switch zodType { case "string": - validateStr = c.validateString(validate) - if strings.Contains(validateStr, ".enum(") { - return "z" + validateStr + stringParts := c.validateString(validate) + switch { + case stringParts.enumLike: + return stringParts.base + stringParts.chain + case stringParts.isIPUnion: + return stringParts.base + case stringParts.base != "": + return stringParts.base + stringParts.chain + default: + return "z.string()" + stringParts.chain } case "number": validateStr = c.validateNumber(validate) @@ -538,12 +579,12 @@ func (c *Converter) getType(t reflect.Type, indent int) string { return zodType } -func (c *Converter) convertField(f reflect.StructField, indent int, optional, nullable bool) (string, bool) { +func (c *Converter) convertNamedField(f reflect.StructField, indent int, optional, nullable bool) string { name := fieldName(f) // fields named `-` are not exported to JSON so don't export zod types if name == "-" { - return "", false + return "" } // because nullability is processed before custom types, this makes sure @@ -561,24 +602,37 @@ func (c *Converter) convertField(f reflect.StructField, indent int, optional, nu } t := c.ConvertType(f.Type, f.Tag.Get("validate"), indent) - if !f.Anonymous { - return fmt.Sprintf( - "%s%s: %s%s%s,\n", - indentation(indent), - name, - t, - optionalCall, - nullableCall), false - } else { - typeName := typeName(f.Type) - entry, ok := c.outputs[typeName] - if ok && entry.selfRef { - // Since we are spreading shape, we won't be able to support any validation tags on the embedded field - return fmt.Sprintf("%s...%s,\n", indentation(indent), shapeName(c.prefix, typeName)), false - } + return fmt.Sprintf( + "%s%s: %s%s%s,\n", + indentation(indent), + name, + t, + optionalCall, + nullableCall) +} + +func (c *Converter) convertEmbeddedFieldMerge(f reflect.StructField, indent int) (string, bool) { + t := c.ConvertType(f.Type, f.Tag.Get("validate"), indent) + typeName := typeName(f.Type) + entry, ok := c.outputs[typeName] + if ok && entry.selfRef { + // Since we are spreading shape, we won't be able to support any validation tags on the embedded field + return fmt.Sprintf("%s...%s,\n", indentation(indent), shapeName(c.prefix, typeName)), false + } + + return fmt.Sprintf(".merge(%s)", t), true +} - return fmt.Sprintf(".merge(%s)", t), true +func (c *Converter) convertEmbeddedFieldSpread(f reflect.StructField, indent int) string { + t := c.ConvertType(f.Type, f.Tag.Get("validate"), indent) + typeName := typeName(f.Type) + entry, ok := c.outputs[typeName] + if ok && entry.selfRef { + // Since we are spreading shape, we won't be able to support any validation tags on the embedded field + return fmt.Sprintf("%s...%s,\n", indentation(indent), shapeName(c.prefix, typeName)) } + + return fmt.Sprintf("%s...%s.shape,\n", indentation(indent), t) } func (c *Converter) getTypeField(f reflect.StructField, indent int, optional, nullable bool) (string, bool) { @@ -716,9 +770,16 @@ func (c *Converter) convertKeyType(t reflect.Type, validate string) string { if validate != "" { switch zodType { case "string": - validateStr = c.validateString(validate) - if strings.Contains(validateStr, ".enum(") { - return "z" + validateStr + stringParts := c.validateString(validate) + switch { + case stringParts.enumLike: + return stringParts.base + stringParts.chain + case stringParts.isIPUnion: + return stringParts.base + case stringParts.base != "": + return stringParts.base + stringParts.chain + default: + return "z.string()" + stringParts.chain } case "number": validateStr = c.validateNumber(validate) @@ -787,8 +848,15 @@ forParts: validateStr.WriteString(refine) } - return fmt.Sprintf(`z.record(%s, %s)%s`, - c.convertKeyType(t.Key(), getValidateKeys(validate)), + keySchema := c.convertKeyType(t.Key(), getValidateKeys(validate)) + recordFn := "z.record" + if !c.zodV3 && isPartialRecordKeySchema(keySchema) { + recordFn = "z.partialRecord" + } + + return fmt.Sprintf(`%s(%s, %s)%s`, + recordFn, + keySchema, c.ConvertType(t.Elem(), getValidateValues(validate), indent), validateStr.String()) } @@ -923,29 +991,41 @@ func (c *Converter) validateNumber(validate string) string { return validateStr.String() } -func (c *Converter) validateString(validate string) string { - var validateStr strings.Builder +func (c *Converter) validateString(validate string) stringSchemaParts { + var chunks []stringSchemaChunk var refines []string parts := strings.Split(validate, ",") - for _, part := range parts { - valName, valValue, done := c.preprocessValidationTagPart(part, &refines, &validateStr) - if done { + for _, rawPart := range parts { + valName, valValue, skip := c.parseValidationTagPart(rawPart) + if skip { + continue + } + + if h, ok := c.customTags[valName]; ok { + v := h(c, reflect.TypeOf(0), valValue, 0) + if strings.HasPrefix(v, ".refine") { + refines = append(refines, v) + } else { + chunks = append(chunks, stringSchemaChunk{kind: "chain", text: v}) + } continue } if valValue != "" { switch valName { case "oneof": - vals := splitParamsRegex.FindAllString(part[6:], -1) + vals := splitParamsRegex.FindAllString(rawPart[6:], -1) for i := 0; i < len(vals); i++ { vals[i] = strings.Replace(vals[i], "'", "", -1) } if len(vals) == 0 { panic("oneof= must be followed by a list of values") } - // const FishEnum = z.enum(["Salmon", "Tuna", "Trout"]); - validateStr.WriteString(fmt.Sprintf(".enum([\"%s\"] as const)", strings.Join(vals, "\", \""))) + chunks = append(chunks, stringSchemaChunk{ + kind: "enum", + text: fmt.Sprintf("z.enum([\"%s\"] as const)", strings.Join(vals, "\", \"")), + }) case "len": refines = append(refines, fmt.Sprintf(".refine((val) => [...val].length === %s, 'String must contain %s character(s)')", valValue, valValue)) case "min": @@ -969,137 +1049,215 @@ func (c *Converter) validateString(validate string) string { case "lte": 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)) + chunks = append(chunks, stringSchemaChunk{kind: "chain", text: fmt.Sprintf(".includes(\"%s\")", valValue)}) case "endswith": - validateStr.WriteString(fmt.Sprintf(".endsWith(\"%s\")", valValue)) + chunks = append(chunks, stringSchemaChunk{kind: "chain", text: fmt.Sprintf(".endsWith(\"%s\")", valValue)}) case "startswith": - validateStr.WriteString(fmt.Sprintf(".startsWith(\"%s\")", valValue)) + chunks = append(chunks, stringSchemaChunk{kind: "chain", text: fmt.Sprintf(".startsWith(\"%s\")", valValue)}) case "eq": refines = append(refines, fmt.Sprintf(".refine((val) => val === \"%s\")", valValue)) case "ne": refines = append(refines, fmt.Sprintf(".refine((val) => val !== \"%s\")", valValue)) - default: - panic(fmt.Sprintf("unknown validation: %s", part)) + panic(fmt.Sprintf("unknown validation: %s", rawPart)) } - } else { - switch part { - case "omitempty": - case "required": - validateStr.WriteString(".min(1)") - case "email": - // email is more readable than copying the regex in regexes.go but could be incompatible - // Also there is an open issue https://github.com/go-playground/validator/issues/517 - // https://github.com/puellanivis/pedantic-regexps/blob/master/email.go - // solution is there in the comments but not implemented yet - validateStr.WriteString(".email()") - case "url": - // url is more readable than copying the regex in regexes.go but could be incompatible - validateStr.WriteString(".url()") - case "ipv4": - validateStr.WriteString(".ip({ version: \"v4\" })") - case "ip4_addr": - validateStr.WriteString(".ip({ version: \"v4\" })") - case "ipv6": - validateStr.WriteString(".ip({ version: \"v6\" })") - case "ip6_addr": - validateStr.WriteString(".ip({ version: \"v6\" })") - case "ip": - validateStr.WriteString(".ip()") - case "ip_addr": - validateStr.WriteString(".ip()") - case "http_url": - // url is more readable than copying the regex in regexes.go but could be incompatible - validateStr.WriteString(".url()") - case "url_encoded": - validateStr.WriteString(fmt.Sprintf(".regex(/%s/)", uRLEncodedRegexString)) - case "alpha": - validateStr.WriteString(fmt.Sprintf(".regex(/%s/)", alphaRegexString)) - case "alphanum": - validateStr.WriteString(fmt.Sprintf(".regex(/%s/)", alphaNumericRegexString)) - case "alphanumunicode": - validateStr.WriteString(fmt.Sprintf(".regex(/%s/)", alphaUnicodeNumericRegexString)) - case "alphaunicode": - validateStr.WriteString(fmt.Sprintf(".regex(/%s/)", alphaUnicodeRegexString)) - case "ascii": - validateStr.WriteString(fmt.Sprintf(".regex(/%s/)", aSCIIRegexString)) - case "boolean": - validateStr.WriteString(".enum(['true', 'false'])") - case "lowercase": - refines = append(refines, ".refine((val) => val === val.toLowerCase())") - case "number": - validateStr.WriteString(fmt.Sprintf(".regex(/%s/)", numberRegexString)) - case "numeric": - validateStr.WriteString(fmt.Sprintf(".regex(/%s/)", numericRegexString)) - case "uppercase": - refines = append(refines, ".refine((val) => val === val.toUpperCase())") - case "base64": - validateStr.WriteString(fmt.Sprintf(".regex(/%s/)", base64RegexString)) - case "mongodb": - validateStr.WriteString(fmt.Sprintf(".regex(/%s/)", mongodbRegexString)) - case "datetime": - validateStr.WriteString(".datetime()") - case "hexadecimal": - validateStr.WriteString(fmt.Sprintf(".regex(/%s/)", hexadecimalRegexString)) - case "json": - // TODO: Better error messages with this - // const literalSchema = z.union([z.string(), z.number(), z.boolean(), z.null()]); - //type Literal = z.infer; - //type Json = Literal | { [key: string]: Json } | Json[]; - //const jsonSchema: z.ZodType = z.lazy(() => - // z.union([literalSchema, z.array(jsonSchema), z.record(jsonSchema)]) - //); - // - //jsonSchema.parse(data); - - refines = append(refines, ".refine((val) => { try { JSON.parse(val); return true } catch { return false } })") - case "jwt": - validateStr.WriteString(fmt.Sprintf(".regex(/%s/)", jWTRegexString)) - case "latitude": - validateStr.WriteString(fmt.Sprintf(".regex(/%s/)", latitudeRegexString)) - case "longitude": - validateStr.WriteString(fmt.Sprintf(".regex(/%s/)", longitudeRegexString)) - case "uuid": - validateStr.WriteString(fmt.Sprintf(".regex(/%s/)", uUIDRegexString)) - case "uuid3": - validateStr.WriteString(fmt.Sprintf(".regex(/%s/)", uUID3RegexString)) - case "uuid3_rfc4122": - validateStr.WriteString(fmt.Sprintf(".regex(/%s/)", uUID3RFC4122RegexString)) - case "uuid4": - validateStr.WriteString(fmt.Sprintf(".regex(/%s/)", uUID4RegexString)) - case "uuid4_rfc4122": - validateStr.WriteString(fmt.Sprintf(".regex(/%s/)", uUID4RFC4122RegexString)) - case "uuid5": - validateStr.WriteString(fmt.Sprintf(".regex(/%s/)", uUID5RegexString)) - case "uuid5_rfc4122": - validateStr.WriteString(fmt.Sprintf(".regex(/%s/)", uUID5RFC4122RegexString)) - case "uuid_rfc4122": - validateStr.WriteString(fmt.Sprintf(".regex(/%s/)", uUIDRFC4122RegexString)) - case "md4": - validateStr.WriteString(fmt.Sprintf(".regex(/%s/)", md4RegexString)) - case "md5": - validateStr.WriteString(fmt.Sprintf(".regex(/%s/)", md5RegexString)) - case "sha256": - validateStr.WriteString(fmt.Sprintf(".regex(/%s/)", sha256RegexString)) - case "sha384": - validateStr.WriteString(fmt.Sprintf(".regex(/%s/)", sha384RegexString)) - case "sha512": - validateStr.WriteString(fmt.Sprintf(".regex(/%s/)", sha512RegexString)) + continue + } - default: - panic(fmt.Sprintf("unknown validation: %s", part)) - } + switch valName { + case "omitempty": + case "required": + chunks = append(chunks, stringSchemaChunk{kind: "chain", text: ".min(1)"}) + case "email": + chunks = append(chunks, stringSchemaChunk{kind: "format", v4Base: "z.email()", legacyChain: ".email()"}) + case "url": + chunks = append(chunks, stringSchemaChunk{kind: "format", v4Base: "z.url()", legacyChain: ".url()"}) + case "ipv4", "ip4_addr": + chunks = append(chunks, stringSchemaChunk{kind: "format", v4Base: "z.ipv4()", legacyChain: `.ip({ version: "v4" })`}) + case "ipv6", "ip6_addr": + chunks = append(chunks, stringSchemaChunk{kind: "format", v4Base: "z.ipv6()", legacyChain: `.ip({ version: "v6" })`}) + case "ip", "ip_addr": + chunks = append(chunks, stringSchemaChunk{kind: "ip", legacyChain: ".ip()"}) + case "http_url": + chunks = append(chunks, stringSchemaChunk{kind: "format", v4Base: "z.httpUrl()", legacyChain: ".url()"}) + case "url_encoded": + chunks = append(chunks, stringSchemaChunk{kind: "chain", text: fmt.Sprintf(".regex(/%s/)", uRLEncodedRegexString)}) + case "alpha": + chunks = append(chunks, stringSchemaChunk{kind: "chain", text: fmt.Sprintf(".regex(/%s/)", alphaRegexString)}) + case "alphanum": + chunks = append(chunks, stringSchemaChunk{kind: "chain", text: fmt.Sprintf(".regex(/%s/)", alphaNumericRegexString)}) + case "alphanumunicode": + chunks = append(chunks, stringSchemaChunk{kind: "chain", text: fmt.Sprintf(".regex(/%s/)", alphaUnicodeNumericRegexString)}) + case "alphaunicode": + chunks = append(chunks, stringSchemaChunk{kind: "chain", text: fmt.Sprintf(".regex(/%s/)", alphaUnicodeRegexString)}) + case "ascii": + chunks = append(chunks, stringSchemaChunk{kind: "chain", text: fmt.Sprintf(".regex(/%s/)", aSCIIRegexString)}) + case "boolean": + chunks = append(chunks, stringSchemaChunk{kind: "enum", text: "z.enum(['true', 'false'])"}) + case "lowercase": + refines = append(refines, ".refine((val) => val === val.toLowerCase())") + case "number": + chunks = append(chunks, stringSchemaChunk{kind: "chain", text: fmt.Sprintf(".regex(/%s/)", numberRegexString)}) + case "numeric": + chunks = append(chunks, stringSchemaChunk{kind: "chain", text: fmt.Sprintf(".regex(/%s/)", numericRegexString)}) + case "uppercase": + refines = append(refines, ".refine((val) => val === val.toUpperCase())") + case "base64": + chunks = append(chunks, stringSchemaChunk{kind: "format", v4Base: "z.base64()", legacyChain: fmt.Sprintf(".regex(/%s/)", base64RegexString)}) + case "mongodb": + chunks = append(chunks, stringSchemaChunk{kind: "chain", text: fmt.Sprintf(".regex(/%s/)", mongodbRegexString)}) + case "datetime": + chunks = append(chunks, stringSchemaChunk{kind: "format", v4Base: "z.iso.datetime()", legacyChain: ".datetime()"}) + case "hexadecimal": + chunks = append(chunks, stringSchemaChunk{kind: "format", v4Base: "z.hex()", legacyChain: fmt.Sprintf(".regex(/%s/)", hexadecimalRegexString)}) + case "json": + refines = append(refines, ".refine((val) => { try { JSON.parse(val); return true } catch { return false } })") + case "jwt": + chunks = append(chunks, stringSchemaChunk{kind: "format", v4Base: "z.jwt()", legacyChain: fmt.Sprintf(".regex(/%s/)", jWTRegexString)}) + case "latitude": + chunks = append(chunks, stringSchemaChunk{kind: "chain", text: fmt.Sprintf(".regex(/%s/)", latitudeRegexString)}) + case "longitude": + chunks = append(chunks, stringSchemaChunk{kind: "chain", text: fmt.Sprintf(".regex(/%s/)", longitudeRegexString)}) + case "uuid": + chunks = append(chunks, stringSchemaChunk{kind: "format", v4Base: "z.uuid()", legacyChain: fmt.Sprintf(".regex(/%s/)", uUIDRegexString)}) + case "uuid3": + chunks = append(chunks, stringSchemaChunk{kind: "format", v4Base: `z.uuid({ version: "v3" })`, legacyChain: fmt.Sprintf(".regex(/%s/)", uUID3RegexString)}) + case "uuid3_rfc4122": + chunks = append(chunks, stringSchemaChunk{kind: "format", v4Base: `z.uuid({ version: "v3" })`, legacyChain: fmt.Sprintf(".regex(/%s/)", uUID3RFC4122RegexString)}) + case "uuid4": + chunks = append(chunks, stringSchemaChunk{kind: "format", v4Base: `z.uuid({ version: "v4" })`, legacyChain: fmt.Sprintf(".regex(/%s/)", uUID4RegexString)}) + case "uuid4_rfc4122": + chunks = append(chunks, stringSchemaChunk{kind: "format", v4Base: `z.uuid({ version: "v4" })`, legacyChain: fmt.Sprintf(".regex(/%s/)", uUID4RFC4122RegexString)}) + case "uuid5": + chunks = append(chunks, stringSchemaChunk{kind: "format", v4Base: `z.uuid({ version: "v5" })`, legacyChain: fmt.Sprintf(".regex(/%s/)", uUID5RegexString)}) + case "uuid5_rfc4122": + chunks = append(chunks, stringSchemaChunk{kind: "format", v4Base: `z.uuid({ version: "v5" })`, legacyChain: fmt.Sprintf(".regex(/%s/)", uUID5RFC4122RegexString)}) + case "uuid_rfc4122": + chunks = append(chunks, stringSchemaChunk{kind: "format", v4Base: "z.uuid()", legacyChain: fmt.Sprintf(".regex(/%s/)", uUIDRFC4122RegexString)}) + case "md4": + chunks = append(chunks, stringSchemaChunk{kind: "chain", text: fmt.Sprintf(".regex(/%s/)", md4RegexString)}) + case "md5": + chunks = append(chunks, stringSchemaChunk{kind: "format", v4Base: `z.hash("md5")`, legacyChain: fmt.Sprintf(".regex(/%s/)", md5RegexString)}) + case "sha256": + chunks = append(chunks, stringSchemaChunk{kind: "format", v4Base: `z.hash("sha256")`, legacyChain: fmt.Sprintf(".regex(/%s/)", sha256RegexString)}) + case "sha384": + chunks = append(chunks, stringSchemaChunk{kind: "format", v4Base: `z.hash("sha384")`, legacyChain: fmt.Sprintf(".regex(/%s/)", sha384RegexString)}) + case "sha512": + chunks = append(chunks, stringSchemaChunk{kind: "format", v4Base: `z.hash("sha512")`, legacyChain: fmt.Sprintf(".regex(/%s/)", sha512RegexString)}) + default: + panic(fmt.Sprintf("unknown validation: %s", rawPart)) } } for _, refine := range refines { - validateStr.WriteString(refine) + chunks = append(chunks, stringSchemaChunk{kind: "chain", text: refine}) } - return validateStr.String() + return c.lowerStringSchemaChunks(chunks) } -func (c *Converter) preprocessValidationTagPart(part string, refines *[]string, validateStr *strings.Builder) (string, string, bool) { +func (c *Converter) lowerStringSchemaChunks(chunks []stringSchemaChunk) stringSchemaParts { + schemaParts := stringSchemaParts{} + enumIdx := -1 + firstFormatIdx := -1 + firstIPIdx := -1 + hasNonIPFormat := false + + for i, chunk := range chunks { + switch chunk.kind { + case "enum": + if enumIdx == -1 { + enumIdx = i + } + case "format": + if firstFormatIdx == -1 { + firstFormatIdx = i + } + hasNonIPFormat = true + case "ip": + if firstIPIdx == -1 { + firstIPIdx = i + } + } + } + + if enumIdx != -1 { + schemaParts.base = chunks[enumIdx].text + schemaParts.enumLike = true + for i := enumIdx + 1; i < len(chunks); i++ { + if chunks[i].kind == "chain" && strings.HasPrefix(chunks[i].text, ".refine") { + schemaParts.chain += chunks[i].text + } + } + return schemaParts + } + + if c.zodV3 { + for _, chunk := range chunks { + schemaParts.chain += legacyStringSchemaChunk(chunk) + } + return schemaParts + } + + if firstIPIdx != -1 { + if hasNonIPFormat || hasChainBeforeStringSchemaChunk(chunks, firstIPIdx) { + for _, chunk := range chunks { + schemaParts.chain += legacyStringSchemaChunk(chunk) + } + return schemaParts + } + + armChain := "" + for _, chunk := range chunks { + if chunk.kind == "chain" { + armChain += chunk.text + } + } + schemaParts.base = fmt.Sprintf("z.union([z.ipv4()%s, z.ipv6()%s])", armChain, armChain) + schemaParts.isIPUnion = true + return schemaParts + } + + if firstFormatIdx == -1 || hasChainBeforeStringSchemaChunk(chunks, firstFormatIdx) { + for _, chunk := range chunks { + schemaParts.chain += legacyStringSchemaChunk(chunk) + } + return schemaParts + } + + schemaParts.base = chunks[firstFormatIdx].v4Base + for i := firstFormatIdx + 1; i < len(chunks); i++ { + schemaParts.chain += legacyStringSchemaChunk(chunks[i]) + } + return schemaParts +} + +func legacyStringSchemaChunk(chunk stringSchemaChunk) string { + switch chunk.kind { + case "chain": + return chunk.text + case "format", "ip": + return chunk.legacyChain + default: + return "" + } +} + +func hasChainBeforeStringSchemaChunk(chunks []stringSchemaChunk, idx int) bool { + for i := 0; i < idx; i++ { + if chunks[i].kind == "chain" { + return true + } + } + return false +} + +func isPartialRecordKeySchema(schema string) bool { + schema = strings.TrimSpace(schema) + return strings.HasPrefix(schema, "z.enum(") || strings.HasPrefix(schema, "z.literal(") +} + +func (c *Converter) parseValidationTagPart(part string) (string, string, bool) { part = strings.TrimSpace(part) if part == "" { return "", "", true @@ -1123,6 +1281,15 @@ func (c *Converter) preprocessValidationTagPart(part string, refines *[]string, return "", "", true } + return valName, valValue, false +} + +func (c *Converter) preprocessValidationTagPart(part string, refines *[]string, validateStr *strings.Builder) (string, string, bool) { + valName, valValue, done := c.parseValidationTagPart(part) + if done { + return "", "", true + } + if h, ok := c.customTags[valName]; ok { v := h(c, reflect.TypeOf(0), valValue, 0) if strings.HasPrefix(v, ".refine") { diff --git a/zod_test.go b/zod_test.go index dea0857..cdc85ba 100644 --- a/zod_test.go +++ b/zod_test.go @@ -94,7 +94,7 @@ func TestStructSimpleWithOmittedField(t *testing.T) { export type User = z.infer `, - StructToZodSchema(User{})) + StructToZodSchema(User{}, WithZodV3())) } func TestStructSimplePrefix(t *testing.T) { @@ -144,7 +144,7 @@ export const UserSchema = z.object({ export type User = z.infer `, - StructToZodSchema(User{})) + StructToZodSchema(User{}, WithZodV3())) } func TestStringArray(t *testing.T) { @@ -643,7 +643,7 @@ export type Required = z.infer export type Email = z.infer `, - StructToZodSchema(Email{})) + StructToZodSchema(Email{}, WithZodV3())) type URL struct { Name string `validate:"url"` @@ -655,7 +655,7 @@ export type Email = z.infer export type URL = z.infer `, - StructToZodSchema(URL{})) + StructToZodSchema(URL{}, WithZodV3())) type IPv4 struct { Name string `validate:"ipv4"` @@ -667,7 +667,7 @@ export type URL = z.infer export type IPv4 = z.infer `, - StructToZodSchema(IPv4{})) + StructToZodSchema(IPv4{}, WithZodV3())) type IPv6 struct { Name string `validate:"ipv6"` @@ -679,7 +679,7 @@ export type IPv4 = z.infer export type IPv6 = z.infer `, - StructToZodSchema(IPv6{})) + StructToZodSchema(IPv6{}, WithZodV3())) type IP4Addr struct { Name string `validate:"ip4_addr"` @@ -691,7 +691,7 @@ export type IPv6 = z.infer export type IP4Addr = z.infer `, - StructToZodSchema(IP4Addr{})) + StructToZodSchema(IP4Addr{}, WithZodV3())) type IP6Addr struct { Name string `validate:"ip6_addr"` @@ -703,7 +703,7 @@ export type IP4Addr = z.infer export type IP6Addr = z.infer `, - StructToZodSchema(IP6Addr{})) + StructToZodSchema(IP6Addr{}, WithZodV3())) type IP struct { Name string `validate:"ip"` @@ -715,7 +715,7 @@ export type IP6Addr = z.infer export type IP = z.infer `, - StructToZodSchema(IP{})) + StructToZodSchema(IP{}, WithZodV3())) type IPAddr struct { Name string `validate:"ip_addr"` @@ -727,7 +727,7 @@ export type IP = z.infer export type IPAddr = z.infer `, - StructToZodSchema(IPAddr{})) + StructToZodSchema(IPAddr{}, WithZodV3())) type HttpURL struct { Name string `validate:"http_url"` @@ -739,7 +739,7 @@ export type IPAddr = z.infer export type HttpURL = z.infer `, - StructToZodSchema(HttpURL{})) + StructToZodSchema(HttpURL{}, WithZodV3())) type URLEncoded struct { Name string `validate:"url_encoded"` @@ -883,7 +883,7 @@ export type Uppercase = z.infer export type Base64 = z.infer `, base64RegexString), - StructToZodSchema(Base64{})) + StructToZodSchema(Base64{}, WithZodV3())) type mongodb struct { Name string `validate:"mongodb"` @@ -907,7 +907,7 @@ export type mongodb = z.infer export type datetime = z.infer `, - StructToZodSchema(datetime{})) + StructToZodSchema(datetime{}, WithZodV3())) type Hexadecimal struct { Name string `validate:"hexadecimal"` @@ -919,7 +919,7 @@ export type datetime = z.infer export type Hexadecimal = z.infer `, hexadecimalRegexString), - StructToZodSchema(Hexadecimal{})) + StructToZodSchema(Hexadecimal{}, WithZodV3())) type json struct { Name string `validate:"json"` @@ -967,7 +967,7 @@ export type Longitude = z.infer export type UUID = z.infer `, uUIDRegexString), - StructToZodSchema(UUID{})) + StructToZodSchema(UUID{}, WithZodV3())) type UUID3 struct { Name string `validate:"uuid3"` @@ -979,7 +979,7 @@ export type UUID = z.infer export type UUID3 = z.infer `, uUID3RegexString), - StructToZodSchema(UUID3{})) + StructToZodSchema(UUID3{}, WithZodV3())) type UUID3RFC4122 struct { Name string `validate:"uuid3_rfc4122"` @@ -991,7 +991,7 @@ export type UUID3 = z.infer export type UUID3RFC4122 = z.infer `, uUID3RFC4122RegexString), - StructToZodSchema(UUID3RFC4122{})) + StructToZodSchema(UUID3RFC4122{}, WithZodV3())) type UUID4 struct { Name string `validate:"uuid4"` @@ -1003,7 +1003,7 @@ export type UUID3RFC4122 = z.infer export type UUID4 = z.infer `, uUID4RegexString), - StructToZodSchema(UUID4{})) + StructToZodSchema(UUID4{}, WithZodV3())) type UUID4RFC4122 struct { Name string `validate:"uuid4_rfc4122"` @@ -1015,7 +1015,7 @@ export type UUID4 = z.infer export type UUID4RFC4122 = z.infer `, uUID4RFC4122RegexString), - StructToZodSchema(UUID4RFC4122{})) + StructToZodSchema(UUID4RFC4122{}, WithZodV3())) type UUID5 struct { Name string `validate:"uuid5"` @@ -1027,7 +1027,7 @@ export type UUID4RFC4122 = z.infer export type UUID5 = z.infer `, uUID5RegexString), - StructToZodSchema(UUID5{})) + StructToZodSchema(UUID5{}, WithZodV3())) type UUID5RFC4122 struct { Name string `validate:"uuid5_rfc4122"` @@ -1039,7 +1039,7 @@ export type UUID5 = z.infer export type UUID5RFC4122 = z.infer `, uUID5RFC4122RegexString), - StructToZodSchema(UUID5RFC4122{})) + StructToZodSchema(UUID5RFC4122{}, WithZodV3())) type UUIDRFC4122 struct { Name string `validate:"uuid_rfc4122"` @@ -1051,7 +1051,7 @@ export type UUID5RFC4122 = z.infer export type UUIDRFC4122 = z.infer `, uUIDRFC4122RegexString), - StructToZodSchema(UUIDRFC4122{})) + StructToZodSchema(UUIDRFC4122{}, WithZodV3())) type MD4 struct { Name string `validate:"md4"` @@ -1075,7 +1075,7 @@ export type MD4 = z.infer export type MD5 = z.infer `, md5RegexString), - StructToZodSchema(MD5{})) + StructToZodSchema(MD5{}, WithZodV3())) type SHA256 struct { Name string `validate:"sha256"` @@ -1087,7 +1087,7 @@ export type MD5 = z.infer export type SHA256 = z.infer `, sha256RegexString), - StructToZodSchema(SHA256{})) + StructToZodSchema(SHA256{}, WithZodV3())) type SHA384 struct { Name string `validate:"sha384"` @@ -1099,7 +1099,7 @@ export type SHA256 = z.infer export type SHA384 = z.infer `, sha384RegexString), - StructToZodSchema(SHA384{})) + StructToZodSchema(SHA384{}, WithZodV3())) type SHA512 struct { Name string `validate:"sha512"` @@ -1111,7 +1111,7 @@ export type SHA384 = z.infer export type SHA512 = z.infer `, sha512RegexString), - StructToZodSchema(SHA512{})) + StructToZodSchema(SHA512{}, WithZodV3())) type Bad2 struct { Name string `validate:"bad2"` @@ -1121,6 +1121,198 @@ export type SHA512 = z.infer }) } +func TestZodV4Defaults(t *testing.T) { + t.Run("embedded structs use shape spreads", func(t *testing.T) { + type HasID struct { + ID string + } + type HasName struct { + Name string `json:"name"` + } + type User struct { + HasID + HasName + Tags []string + } + + assert.Equal(t, `export const HasIDSchema = z.object({ + ID: z.string(), +}) +export type HasID = z.infer + +export const HasNameSchema = z.object({ + name: z.string(), +}) +export type HasName = z.infer + +export const UserSchema = z.object({ + Tags: z.string().array().nullable(), + ...HasIDSchema.shape, + ...HasNameSchema.shape, +}) +export type User = z.infer + +`, StructToZodSchema(User{})) + }) + + t.Run("string formats use zod v4 builders", func(t *testing.T) { + type Payload struct { + Email string `validate:"email"` + Link string `validate:"http_url"` + Base64 string `validate:"base64"` + ID string `validate:"uuid4"` + Checksum string `validate:"md5"` + } + + assert.Equal(t, `export const PayloadSchema = z.object({ + Email: z.email(), + Link: z.httpUrl(), + Base64: z.base64(), + ID: z.uuid({ version: "v4" }), + Checksum: z.hash("md5"), +}) +export type Payload = z.infer + +`, StructToZodSchema(Payload{})) + }) + + t.Run("string tag order is preserved around v4 format helpers", func(t *testing.T) { + type Payload struct { + TrimmedThenEmail string `validate:"trim,email"` + EmailThenTrimmed string `validate:"email,trim"` + } + + customTagHandlers := map[string]CustomFn{ + "trim": func(c *Converter, t reflect.Type, validate string, i int) string { + return ".trim()" + }, + } + + assert.Equal(t, `export const PayloadSchema = z.object({ + TrimmedThenEmail: z.string().trim().email(), + EmailThenTrimmed: z.email().trim(), +}) +export type Payload = z.infer + +`, NewConverterWithOpts(WithCustomTags(customTagHandlers)).Convert(Payload{})) + }) + + t.Run("ip unions inherit generic string constraints", func(t *testing.T) { + type Payload struct { + Address string `validate:"ip,required,max=45"` + } + + assert.Equal(t, `export const PayloadSchema = z.object({ + Address: z.union([z.ipv4().min(1).refine((val) => [...val].length <= 45, 'String must contain at most 45 character(s)'), z.ipv6().min(1).refine((val) => [...val].length <= 45, 'String must contain at most 45 character(s)')]), +}) +export type Payload = z.infer + +`, StructToZodSchema(Payload{})) + }) + + t.Run("oneof takes precedence over ip specialization", func(t *testing.T) { + type Payload struct { + Address string `validate:"oneof='127.0.0.1' '::1',ip"` + } + + assert.Equal(t, `export const PayloadSchema = z.object({ + Address: z.enum(["127.0.0.1", "::1"] as const), +}) +export type Payload = z.infer + +`, StructToZodSchema(Payload{})) + }) + + t.Run("ip mixed with another format falls back to legacy chain semantics", func(t *testing.T) { + type Payload struct { + Address string `validate:"email,ip"` + } + + assert.Equal(t, `export const PayloadSchema = z.object({ + Address: z.string().email().ip(), +}) +export type Payload = z.infer + +`, StructToZodSchema(Payload{})) + }) + + t.Run("enum keyed maps become partial records", func(t *testing.T) { + type Payload struct { + Metadata map[string]string `validate:"dive,keys,oneof=draft published,endkeys"` + } + + assert.Equal(t, `export const PayloadSchema = z.object({ + Metadata: z.partialRecord(z.enum(["draft", "published"] as const), z.string()).nullable(), +}) +export type Payload = z.infer + +`, StructToZodSchema(Payload{})) + }) + + t.Run("recursive embedded shapes preserve encounter order for duplicate keys", func(t *testing.T) { + type Base struct { + ID string `json:"id"` + } + + type Node struct { + Base + ID int `json:"id"` + Next *Node `json:"next"` + } + + assert.Equal(t, `export const BaseSchema = z.object({ + id: z.string(), +}) +export type Base = z.infer + +export type Node = Base & { + id: number, + next: Node | null, +} +const NodeSchemaShape = { + ...BaseSchema.shape, + id: z.number(), + next: z.lazy(() => NodeSchema).nullable(), +} +export const NodeSchema: z.ZodType = z.object(NodeSchemaShape) + +`, StructToZodSchema(Node{})) + }) + + t.Run("recursive embedded shapes keep named fields before spreads", func(t *testing.T) { + type TreeNode struct { + Value string + CreatedAt time.Time + Children *[]TreeNode + } + + type Tree struct { + TreeNode + UpdatedAt time.Time + } + + assert.Equal(t, `export type TreeNode = { + Value: string, + CreatedAt: Date, + Children: TreeNode[] | null, +} +const TreeNodeSchemaShape = { + Value: z.string(), + CreatedAt: z.coerce.date(), + Children: z.lazy(() => TreeNodeSchema).array().nullable(), +} +export const TreeNodeSchema: z.ZodType = z.object(TreeNodeSchemaShape) + +export const TreeSchema = z.object({ + UpdatedAt: z.coerce.date(), + ...TreeNodeSchemaShape, +}) +export type Tree = z.infer + +`, StructToZodSchema(Tree{})) + }) +} + func TestNumberValidations(t *testing.T) { type User1 struct { Age int `validate:"gte=18,lte=60"` @@ -2182,7 +2374,7 @@ export const RequestSchema = z.object({ }).merge(SortParamsSchema.extend({field: z.enum(['title', 'address', 'age', 'dob'])})) export type Request = z.infer -`, NewConverterWithOpts(WithCustomTags(customTagHandlers)).Convert(Request{})) +`, NewConverterWithOpts(WithCustomTags(customTagHandlers), WithZodV3()).Convert(Request{})) } func TestRecursiveEmbeddedStruct(t *testing.T) { @@ -2213,7 +2405,7 @@ func TestRecursiveEmbeddedStruct(t *testing.T) { ItemE } - c := NewConverterWithOpts() + c := NewConverterWithOpts(WithZodV3()) c.AddType(ItemA{}) c.AddType(ItemB{}) c.AddType(ItemC{}) @@ -2294,7 +2486,7 @@ export const TreeSchema = z.object({ }) export type Tree = z.infer -`, StructToZodSchema(Tree{})) +`, StructToZodSchema(Tree{}, WithZodV3())) }) t.Run("embedded struct with pointer to self and date", func(t *testing.T) { @@ -2327,6 +2519,6 @@ export const ArticleSchema = z.object({ }) export type Article = z.infer -`, StructToZodSchema(Article{})) +`, StructToZodSchema(Article{}, WithZodV3())) }) } From cb9a2f1b87d59d405911a7c3f15596af128bfedf Mon Sep 17 00:00:00 2001 From: Ramil Amparo Date: Wed, 1 Apr 2026 17:24:25 +0400 Subject: [PATCH 02/35] Add v4 support and make v3 opt-in. Add golden test files --- Makefile | 6 +- go.mod | 3 +- go.sum | 13 + testdata/TestConvertArray/multi.golden | 5 + testdata/TestConvertArray/single.golden | 5 + testdata/TestConvertSlice.golden | 17 + .../dive1.golden | 5 + .../dive2.golden | 5 + .../TestConvertSliceWithValidations/eq.golden | 5 + .../TestConvertSliceWithValidations/gt.golden | 5 + .../gte.golden | 5 + .../len.golden | 5 + .../TestConvertSliceWithValidations/lt.golden | 5 + .../lte.golden | 5 + .../max.golden | 5 + .../min.golden | 5 + .../TestConvertSliceWithValidations/ne.golden | 5 + .../required.golden | 5 + testdata/TestCustom.golden | 6 + testdata/TestCustomTag/v3.golden | 15 + testdata/TestCustomTag/v4.golden | 16 + testdata/TestDuration.golden | 5 + testdata/TestEverything.golden | 41 + testdata/TestEverythingWithValidations.golden | 42 + testdata/TestGenerics.golden | 17 + testdata/TestInterfaceAny.golden | 6 + testdata/TestInterfaceEmptyAny.golden | 6 + testdata/TestInterfacePointerAny.golden | 6 + testdata/TestInterfacePointerEmptyAny.golden | 6 + testdata/TestMapStringToInterface.golden | 6 + testdata/TestMapStringToString.golden | 6 + .../TestMapWithNonStringKey/float_key.golden | 6 + .../TestMapWithNonStringKey/int_key.golden | 6 + .../TestMapWithNonStringKey/time_key.golden | 6 + testdata/TestMapWithStruct.golden | 10 + testdata/TestMapWithValidations/dive1.golden | 5 + testdata/TestMapWithValidations/dive2.golden | 5 + testdata/TestMapWithValidations/dive3.golden | 5 + testdata/TestMapWithValidations/eq.golden | 5 + testdata/TestMapWithValidations/gt.golden | 5 + testdata/TestMapWithValidations/gte.golden | 5 + testdata/TestMapWithValidations/len.golden | 5 + testdata/TestMapWithValidations/lt.golden | 5 + testdata/TestMapWithValidations/lte.golden | 5 + testdata/TestMapWithValidations/max.golden | 5 + testdata/TestMapWithValidations/min.golden | 5 + testdata/TestMapWithValidations/minmax.golden | 5 + testdata/TestMapWithValidations/ne.golden | 5 + .../TestMapWithValidations/required.golden | 5 + testdata/TestNestedStruct/v3.golden | 15 + testdata/TestNestedStruct/v4.golden | 17 + testdata/TestNullableWithValidations.golden | 35 + testdata/TestNumberValidations/eq.golden | 5 + testdata/TestNumberValidations/gt_lt.golden | 5 + testdata/TestNumberValidations/gte_lte.golden | 5 + testdata/TestNumberValidations/len.golden | 5 + testdata/TestNumberValidations/min_max.golden | 5 + testdata/TestNumberValidations/ne.golden | 5 + testdata/TestNumberValidations/oneof.golden | 5 + testdata/TestRecursive1/v3.golden | 18 + testdata/TestRecursive1/v4.golden | 18 + testdata/TestRecursive2/v3.golden | 15 + testdata/TestRecursive2/v4.golden | 15 + .../TestRecursiveEmbeddedStruct/v3.golden | 39 + .../TestRecursiveEmbeddedStruct/v4.golden | 40 + .../v3.golden | 18 + .../v4.golden | 18 + .../v3.golden | 18 + .../v4.golden | 18 + testdata/TestSliceFields.golden | 11 + testdata/TestStringArray.golden | 5 + testdata/TestStringArrayNullable.golden | 6 + testdata/TestStringNestedArray.golden | 5 + testdata/TestStringNullable.golden | 6 + testdata/TestStringOptional.golden | 6 + testdata/TestStringOptionalNotNullable.golden | 6 + testdata/TestStringOptionalNullable.golden | 6 + testdata/TestStringValidations/alpha.golden | 5 + .../TestStringValidations/alphanum.golden | 5 + .../alphanumunicode.golden | 5 + .../TestStringValidations/alphaunicode.golden | 5 + testdata/TestStringValidations/ascii.golden | Bin 0 -> 128 bytes .../TestStringValidations/base64/v3.golden | 5 + .../TestStringValidations/base64/v4.golden | 5 + testdata/TestStringValidations/boolean.golden | 5 + .../TestStringValidations/contains.golden | 5 + .../TestStringValidations/datetime/v3.golden | 5 + .../TestStringValidations/datetime/v4.golden | 5 + .../TestStringValidations/email/v3.golden | 5 + .../TestStringValidations/email/v4.golden | 5 + .../TestStringValidations/endswith.golden | 5 + testdata/TestStringValidations/eq.golden | 5 + testdata/TestStringValidations/gt.golden | 5 + testdata/TestStringValidations/gte.golden | 5 + .../hexadecimal/v3.golden | 5 + .../hexadecimal/v4.golden | 5 + .../TestStringValidations/http_url/v3.golden | 5 + .../TestStringValidations/http_url/v4.golden | 5 + testdata/TestStringValidations/ip/v3.golden | 5 + testdata/TestStringValidations/ip/v4.golden | 5 + .../TestStringValidations/ip4_addr/v3.golden | 5 + .../TestStringValidations/ip4_addr/v4.golden | 5 + .../TestStringValidations/ip6_addr/v3.golden | 5 + .../TestStringValidations/ip6_addr/v4.golden | 5 + .../TestStringValidations/ip_addr/v3.golden | 5 + .../TestStringValidations/ip_addr/v4.golden | 5 + testdata/TestStringValidations/ipv4/v3.golden | 5 + testdata/TestStringValidations/ipv4/v4.golden | 5 + testdata/TestStringValidations/ipv6/v3.golden | 5 + testdata/TestStringValidations/ipv6/v4.golden | 5 + testdata/TestStringValidations/json.golden | 5 + .../TestStringValidations/latitude.golden | 5 + testdata/TestStringValidations/len.golden | 5 + .../TestStringValidations/longitude.golden | 5 + .../TestStringValidations/lowercase.golden | 5 + testdata/TestStringValidations/lt.golden | 5 + testdata/TestStringValidations/lte.golden | 5 + testdata/TestStringValidations/max.golden | 5 + testdata/TestStringValidations/md4.golden | 5 + testdata/TestStringValidations/md5/v3.golden | 5 + testdata/TestStringValidations/md5/v4.golden | 5 + testdata/TestStringValidations/min.golden | 5 + testdata/TestStringValidations/minmax.golden | 5 + testdata/TestStringValidations/mongodb.golden | 5 + testdata/TestStringValidations/ne.golden | 5 + testdata/TestStringValidations/number.golden | 5 + testdata/TestStringValidations/numeric.golden | 5 + testdata/TestStringValidations/oneof.golden | 5 + .../oneof_separated.golden | 5 + .../TestStringValidations/required.golden | 5 + .../TestStringValidations/sha256/v3.golden | 5 + .../TestStringValidations/sha256/v4.golden | 5 + .../TestStringValidations/sha384/v3.golden | 5 + .../TestStringValidations/sha384/v4.golden | 5 + .../TestStringValidations/sha512/v3.golden | 5 + .../TestStringValidations/sha512/v4.golden | 5 + .../TestStringValidations/startswith.golden | 5 + .../TestStringValidations/uppercase.golden | 5 + testdata/TestStringValidations/url/v3.golden | 5 + testdata/TestStringValidations/url/v4.golden | 5 + .../TestStringValidations/url_encoded.golden | 5 + testdata/TestStringValidations/uuid/v3.golden | 5 + testdata/TestStringValidations/uuid/v4.golden | 5 + .../TestStringValidations/uuid3/v3.golden | 5 + .../TestStringValidations/uuid3/v4.golden | 5 + .../uuid3_rfc4122/v3.golden | 5 + .../uuid3_rfc4122/v4.golden | 5 + .../TestStringValidations/uuid4/v3.golden | 5 + .../TestStringValidations/uuid4/v4.golden | 5 + .../uuid4_rfc4122/v3.golden | 5 + .../uuid4_rfc4122/v4.golden | 5 + .../TestStringValidations/uuid5/v3.golden | 5 + .../TestStringValidations/uuid5/v4.golden | 5 + .../uuid5_rfc4122/v3.golden | 5 + .../uuid5_rfc4122/v4.golden | 5 + .../uuid_rfc4122/v3.golden | 5 + .../uuid_rfc4122/v4.golden | 5 + testdata/TestStructSimple.golden | 7 + testdata/TestStructSimplePrefix.golden | 7 + .../TestStructSimpleWithOmittedField.golden | 7 + testdata/TestStructSlice.golden | 7 + testdata/TestStructSliceOptional.golden | 7 + .../TestStructSliceOptionalNullable.golden | 7 + testdata/TestStructTime.golden | 6 + testdata/TestTimeWithRequired.golden | 5 + .../embedded_structs_use_shape_spreads.golden | 17 + ...m_keyed_maps_become_partial_records.golden | 5 + ...alls_back_to_legacy_chain_semantics.golden | 5 + ..._inherit_generic_string_constraints.golden | 5 + ...s_precedence_over_ip_specialization.golden | 5 + ...es_keep_named_fields_before_spreads.golden | 18 + ..._encounter_order_for_duplicate_keys.golden | 16 + .../string_formats_use_zod_v4_builders.golden | 9 + ..._preserved_around_v4_format_helpers.golden | 6 + zod.go | 29 +- zod_test.go | 2507 +++++------------ 176 files changed, 2087 insertions(+), 1738 deletions(-) create mode 100644 testdata/TestConvertArray/multi.golden create mode 100644 testdata/TestConvertArray/single.golden create mode 100644 testdata/TestConvertSlice.golden create mode 100644 testdata/TestConvertSliceWithValidations/dive1.golden create mode 100644 testdata/TestConvertSliceWithValidations/dive2.golden create mode 100644 testdata/TestConvertSliceWithValidations/eq.golden create mode 100644 testdata/TestConvertSliceWithValidations/gt.golden create mode 100644 testdata/TestConvertSliceWithValidations/gte.golden create mode 100644 testdata/TestConvertSliceWithValidations/len.golden create mode 100644 testdata/TestConvertSliceWithValidations/lt.golden create mode 100644 testdata/TestConvertSliceWithValidations/lte.golden create mode 100644 testdata/TestConvertSliceWithValidations/max.golden create mode 100644 testdata/TestConvertSliceWithValidations/min.golden create mode 100644 testdata/TestConvertSliceWithValidations/ne.golden create mode 100644 testdata/TestConvertSliceWithValidations/required.golden create mode 100644 testdata/TestCustom.golden create mode 100644 testdata/TestCustomTag/v3.golden create mode 100644 testdata/TestCustomTag/v4.golden create mode 100644 testdata/TestDuration.golden create mode 100644 testdata/TestEverything.golden create mode 100644 testdata/TestEverythingWithValidations.golden create mode 100644 testdata/TestGenerics.golden create mode 100644 testdata/TestInterfaceAny.golden create mode 100644 testdata/TestInterfaceEmptyAny.golden create mode 100644 testdata/TestInterfacePointerAny.golden create mode 100644 testdata/TestInterfacePointerEmptyAny.golden create mode 100644 testdata/TestMapStringToInterface.golden create mode 100644 testdata/TestMapStringToString.golden create mode 100644 testdata/TestMapWithNonStringKey/float_key.golden create mode 100644 testdata/TestMapWithNonStringKey/int_key.golden create mode 100644 testdata/TestMapWithNonStringKey/time_key.golden create mode 100644 testdata/TestMapWithStruct.golden create mode 100644 testdata/TestMapWithValidations/dive1.golden create mode 100644 testdata/TestMapWithValidations/dive2.golden create mode 100644 testdata/TestMapWithValidations/dive3.golden create mode 100644 testdata/TestMapWithValidations/eq.golden create mode 100644 testdata/TestMapWithValidations/gt.golden create mode 100644 testdata/TestMapWithValidations/gte.golden create mode 100644 testdata/TestMapWithValidations/len.golden create mode 100644 testdata/TestMapWithValidations/lt.golden create mode 100644 testdata/TestMapWithValidations/lte.golden create mode 100644 testdata/TestMapWithValidations/max.golden create mode 100644 testdata/TestMapWithValidations/min.golden create mode 100644 testdata/TestMapWithValidations/minmax.golden create mode 100644 testdata/TestMapWithValidations/ne.golden create mode 100644 testdata/TestMapWithValidations/required.golden create mode 100644 testdata/TestNestedStruct/v3.golden create mode 100644 testdata/TestNestedStruct/v4.golden create mode 100644 testdata/TestNullableWithValidations.golden create mode 100644 testdata/TestNumberValidations/eq.golden create mode 100644 testdata/TestNumberValidations/gt_lt.golden create mode 100644 testdata/TestNumberValidations/gte_lte.golden create mode 100644 testdata/TestNumberValidations/len.golden create mode 100644 testdata/TestNumberValidations/min_max.golden create mode 100644 testdata/TestNumberValidations/ne.golden create mode 100644 testdata/TestNumberValidations/oneof.golden create mode 100644 testdata/TestRecursive1/v3.golden create mode 100644 testdata/TestRecursive1/v4.golden create mode 100644 testdata/TestRecursive2/v3.golden create mode 100644 testdata/TestRecursive2/v4.golden create mode 100644 testdata/TestRecursiveEmbeddedStruct/v3.golden create mode 100644 testdata/TestRecursiveEmbeddedStruct/v4.golden create mode 100644 testdata/TestRecursiveEmbeddedWithPointersAndDates/embedded_struct_with_pointer_to_self_and_date/v3.golden create mode 100644 testdata/TestRecursiveEmbeddedWithPointersAndDates/embedded_struct_with_pointer_to_self_and_date/v4.golden create mode 100644 testdata/TestRecursiveEmbeddedWithPointersAndDates/recursive_struct_with_pointer_field_and_date/v3.golden create mode 100644 testdata/TestRecursiveEmbeddedWithPointersAndDates/recursive_struct_with_pointer_field_and_date/v4.golden create mode 100644 testdata/TestSliceFields.golden create mode 100644 testdata/TestStringArray.golden create mode 100644 testdata/TestStringArrayNullable.golden create mode 100644 testdata/TestStringNestedArray.golden create mode 100644 testdata/TestStringNullable.golden create mode 100644 testdata/TestStringOptional.golden create mode 100644 testdata/TestStringOptionalNotNullable.golden create mode 100644 testdata/TestStringOptionalNullable.golden create mode 100644 testdata/TestStringValidations/alpha.golden create mode 100644 testdata/TestStringValidations/alphanum.golden create mode 100644 testdata/TestStringValidations/alphanumunicode.golden create mode 100644 testdata/TestStringValidations/alphaunicode.golden create mode 100644 testdata/TestStringValidations/ascii.golden create mode 100644 testdata/TestStringValidations/base64/v3.golden create mode 100644 testdata/TestStringValidations/base64/v4.golden create mode 100644 testdata/TestStringValidations/boolean.golden create mode 100644 testdata/TestStringValidations/contains.golden create mode 100644 testdata/TestStringValidations/datetime/v3.golden create mode 100644 testdata/TestStringValidations/datetime/v4.golden create mode 100644 testdata/TestStringValidations/email/v3.golden create mode 100644 testdata/TestStringValidations/email/v4.golden create mode 100644 testdata/TestStringValidations/endswith.golden create mode 100644 testdata/TestStringValidations/eq.golden create mode 100644 testdata/TestStringValidations/gt.golden create mode 100644 testdata/TestStringValidations/gte.golden create mode 100644 testdata/TestStringValidations/hexadecimal/v3.golden create mode 100644 testdata/TestStringValidations/hexadecimal/v4.golden create mode 100644 testdata/TestStringValidations/http_url/v3.golden create mode 100644 testdata/TestStringValidations/http_url/v4.golden create mode 100644 testdata/TestStringValidations/ip/v3.golden create mode 100644 testdata/TestStringValidations/ip/v4.golden create mode 100644 testdata/TestStringValidations/ip4_addr/v3.golden create mode 100644 testdata/TestStringValidations/ip4_addr/v4.golden create mode 100644 testdata/TestStringValidations/ip6_addr/v3.golden create mode 100644 testdata/TestStringValidations/ip6_addr/v4.golden create mode 100644 testdata/TestStringValidations/ip_addr/v3.golden create mode 100644 testdata/TestStringValidations/ip_addr/v4.golden create mode 100644 testdata/TestStringValidations/ipv4/v3.golden create mode 100644 testdata/TestStringValidations/ipv4/v4.golden create mode 100644 testdata/TestStringValidations/ipv6/v3.golden create mode 100644 testdata/TestStringValidations/ipv6/v4.golden create mode 100644 testdata/TestStringValidations/json.golden create mode 100644 testdata/TestStringValidations/latitude.golden create mode 100644 testdata/TestStringValidations/len.golden create mode 100644 testdata/TestStringValidations/longitude.golden create mode 100644 testdata/TestStringValidations/lowercase.golden create mode 100644 testdata/TestStringValidations/lt.golden create mode 100644 testdata/TestStringValidations/lte.golden create mode 100644 testdata/TestStringValidations/max.golden create mode 100644 testdata/TestStringValidations/md4.golden create mode 100644 testdata/TestStringValidations/md5/v3.golden create mode 100644 testdata/TestStringValidations/md5/v4.golden create mode 100644 testdata/TestStringValidations/min.golden create mode 100644 testdata/TestStringValidations/minmax.golden create mode 100644 testdata/TestStringValidations/mongodb.golden create mode 100644 testdata/TestStringValidations/ne.golden create mode 100644 testdata/TestStringValidations/number.golden create mode 100644 testdata/TestStringValidations/numeric.golden create mode 100644 testdata/TestStringValidations/oneof.golden create mode 100644 testdata/TestStringValidations/oneof_separated.golden create mode 100644 testdata/TestStringValidations/required.golden create mode 100644 testdata/TestStringValidations/sha256/v3.golden create mode 100644 testdata/TestStringValidations/sha256/v4.golden create mode 100644 testdata/TestStringValidations/sha384/v3.golden create mode 100644 testdata/TestStringValidations/sha384/v4.golden create mode 100644 testdata/TestStringValidations/sha512/v3.golden create mode 100644 testdata/TestStringValidations/sha512/v4.golden create mode 100644 testdata/TestStringValidations/startswith.golden create mode 100644 testdata/TestStringValidations/uppercase.golden create mode 100644 testdata/TestStringValidations/url/v3.golden create mode 100644 testdata/TestStringValidations/url/v4.golden create mode 100644 testdata/TestStringValidations/url_encoded.golden create mode 100644 testdata/TestStringValidations/uuid/v3.golden create mode 100644 testdata/TestStringValidations/uuid/v4.golden create mode 100644 testdata/TestStringValidations/uuid3/v3.golden create mode 100644 testdata/TestStringValidations/uuid3/v4.golden create mode 100644 testdata/TestStringValidations/uuid3_rfc4122/v3.golden create mode 100644 testdata/TestStringValidations/uuid3_rfc4122/v4.golden create mode 100644 testdata/TestStringValidations/uuid4/v3.golden create mode 100644 testdata/TestStringValidations/uuid4/v4.golden create mode 100644 testdata/TestStringValidations/uuid4_rfc4122/v3.golden create mode 100644 testdata/TestStringValidations/uuid4_rfc4122/v4.golden create mode 100644 testdata/TestStringValidations/uuid5/v3.golden create mode 100644 testdata/TestStringValidations/uuid5/v4.golden create mode 100644 testdata/TestStringValidations/uuid5_rfc4122/v3.golden create mode 100644 testdata/TestStringValidations/uuid5_rfc4122/v4.golden create mode 100644 testdata/TestStringValidations/uuid_rfc4122/v3.golden create mode 100644 testdata/TestStringValidations/uuid_rfc4122/v4.golden create mode 100644 testdata/TestStructSimple.golden create mode 100644 testdata/TestStructSimplePrefix.golden create mode 100644 testdata/TestStructSimpleWithOmittedField.golden create mode 100644 testdata/TestStructSlice.golden create mode 100644 testdata/TestStructSliceOptional.golden create mode 100644 testdata/TestStructSliceOptionalNullable.golden create mode 100644 testdata/TestStructTime.golden create mode 100644 testdata/TestTimeWithRequired.golden create mode 100644 testdata/TestZodV4Defaults/embedded_structs_use_shape_spreads.golden create mode 100644 testdata/TestZodV4Defaults/enum_keyed_maps_become_partial_records.golden create mode 100644 testdata/TestZodV4Defaults/ip_mixed_with_another_format_falls_back_to_legacy_chain_semantics.golden create mode 100644 testdata/TestZodV4Defaults/ip_unions_inherit_generic_string_constraints.golden create mode 100644 testdata/TestZodV4Defaults/oneof_takes_precedence_over_ip_specialization.golden create mode 100644 testdata/TestZodV4Defaults/recursive_embedded_shapes_keep_named_fields_before_spreads.golden create mode 100644 testdata/TestZodV4Defaults/recursive_embedded_shapes_preserve_encounter_order_for_duplicate_keys.golden create mode 100644 testdata/TestZodV4Defaults/string_formats_use_zod_v4_builders.golden create mode 100644 testdata/TestZodV4Defaults/string_tag_order_is_preserved_around_v4_format_helpers.golden diff --git a/Makefile b/Makefile index ff90a84..2ddee02 100644 --- a/Makefile +++ b/Makefile @@ -12,7 +12,11 @@ lint: linters-install test: $(GOCMD) test -cover -race ./... +test-update: + GOLDEN_UPDATE=true $(GOCMD) test ./... + + bench: $(GOCMD) test -bench=. -benchmem ./... -.PHONY: test lint linters-install +.PHONY: test test-update lint linters-install bench diff --git a/go.mod b/go.mod index bc34ef1..f8564ec 100644 --- a/go.mod +++ b/go.mod @@ -2,10 +2,11 @@ module github.com/hypersequent/zen go 1.23 -require github.com/stretchr/testify v1.8.3 +require github.com/stretchr/testify v1.9.0 require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/xorcare/golden v0.8.3 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 57c201b..f7850cd 100644 --- a/go.sum +++ b/go.sum @@ -1,10 +1,23 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/xorcare/golden v0.8.3 h1:0sFBpM6/ju8YzhN2akrsPTgm6YEuIwuh0JaeAk5Ne3g= +github.com/xorcare/golden v0.8.3/go.mod h1:lRw6LV+0Pp37EBDMR4sXIz4Y7r75dDZ6bYm0ILDpIHY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/testdata/TestConvertArray/multi.golden b/testdata/TestConvertArray/multi.golden new file mode 100644 index 0000000..fe8da34 --- /dev/null +++ b/testdata/TestConvertArray/multi.golden @@ -0,0 +1,5 @@ +export const MultiArraySchema = z.object({ + Arr: z.string().array().length(30).array().length(20).array().length(10), +}) +export type MultiArray = z.infer + diff --git a/testdata/TestConvertArray/single.golden b/testdata/TestConvertArray/single.golden new file mode 100644 index 0000000..4864938 --- /dev/null +++ b/testdata/TestConvertArray/single.golden @@ -0,0 +1,5 @@ +export const ArraySchema = z.object({ + Arr: z.string().array().length(10), +}) +export type Array = z.infer + diff --git a/testdata/TestConvertSlice.golden b/testdata/TestConvertSlice.golden new file mode 100644 index 0000000..37ce66d --- /dev/null +++ b/testdata/TestConvertSlice.golden @@ -0,0 +1,17 @@ +export const FooSchema = z.object({ + Bar: z.string(), + Baz: z.string(), + Quz: z.string(), +}) +export type Foo = z.infer + +export const ZipSchema = z.object({ + Zap: FooSchema.nullable(), +}) +export type Zip = z.infer + +export const WhimSchema = z.object({ + Wham: FooSchema.nullable(), +}) +export type Whim = z.infer + diff --git a/testdata/TestConvertSliceWithValidations/dive1.golden b/testdata/TestConvertSliceWithValidations/dive1.golden new file mode 100644 index 0000000..562945f --- /dev/null +++ b/testdata/TestConvertSliceWithValidations/dive1.golden @@ -0,0 +1,5 @@ +export const Dive1Schema = z.object({ + Slice: z.string().array().array().nullable(), +}) +export type Dive1 = z.infer + diff --git a/testdata/TestConvertSliceWithValidations/dive2.golden b/testdata/TestConvertSliceWithValidations/dive2.golden new file mode 100644 index 0000000..71b0b45 --- /dev/null +++ b/testdata/TestConvertSliceWithValidations/dive2.golden @@ -0,0 +1,5 @@ +export const Dive2Schema = z.object({ + Slice: z.string().array().min(1).array(), +}) +export type Dive2 = z.infer + diff --git a/testdata/TestConvertSliceWithValidations/eq.golden b/testdata/TestConvertSliceWithValidations/eq.golden new file mode 100644 index 0000000..876d632 --- /dev/null +++ b/testdata/TestConvertSliceWithValidations/eq.golden @@ -0,0 +1,5 @@ +export const EqSchema = z.object({ + Slice: z.string().array().length(1), +}) +export type Eq = z.infer + diff --git a/testdata/TestConvertSliceWithValidations/gt.golden b/testdata/TestConvertSliceWithValidations/gt.golden new file mode 100644 index 0000000..32ee0e5 --- /dev/null +++ b/testdata/TestConvertSliceWithValidations/gt.golden @@ -0,0 +1,5 @@ +export const GtSchema = z.object({ + Slice: z.string().array().min(2), +}) +export type Gt = z.infer + diff --git a/testdata/TestConvertSliceWithValidations/gte.golden b/testdata/TestConvertSliceWithValidations/gte.golden new file mode 100644 index 0000000..787660f --- /dev/null +++ b/testdata/TestConvertSliceWithValidations/gte.golden @@ -0,0 +1,5 @@ +export const GteSchema = z.object({ + Slice: z.string().array().min(1), +}) +export type Gte = z.infer + diff --git a/testdata/TestConvertSliceWithValidations/len.golden b/testdata/TestConvertSliceWithValidations/len.golden new file mode 100644 index 0000000..34b5a5a --- /dev/null +++ b/testdata/TestConvertSliceWithValidations/len.golden @@ -0,0 +1,5 @@ +export const LenSchema = z.object({ + Slice: z.string().array().length(1), +}) +export type Len = z.infer + diff --git a/testdata/TestConvertSliceWithValidations/lt.golden b/testdata/TestConvertSliceWithValidations/lt.golden new file mode 100644 index 0000000..ea59a03 --- /dev/null +++ b/testdata/TestConvertSliceWithValidations/lt.golden @@ -0,0 +1,5 @@ +export const LtSchema = z.object({ + Slice: z.string().array().max(0), +}) +export type Lt = z.infer + diff --git a/testdata/TestConvertSliceWithValidations/lte.golden b/testdata/TestConvertSliceWithValidations/lte.golden new file mode 100644 index 0000000..93f447b --- /dev/null +++ b/testdata/TestConvertSliceWithValidations/lte.golden @@ -0,0 +1,5 @@ +export const LteSchema = z.object({ + Slice: z.string().array().max(1), +}) +export type Lte = z.infer + diff --git a/testdata/TestConvertSliceWithValidations/max.golden b/testdata/TestConvertSliceWithValidations/max.golden new file mode 100644 index 0000000..bda8947 --- /dev/null +++ b/testdata/TestConvertSliceWithValidations/max.golden @@ -0,0 +1,5 @@ +export const MaxSchema = z.object({ + Slice: z.string().array().max(1), +}) +export type Max = z.infer + diff --git a/testdata/TestConvertSliceWithValidations/min.golden b/testdata/TestConvertSliceWithValidations/min.golden new file mode 100644 index 0000000..1453c63 --- /dev/null +++ b/testdata/TestConvertSliceWithValidations/min.golden @@ -0,0 +1,5 @@ +export const MinSchema = z.object({ + Slice: z.string().array().min(1), +}) +export type Min = z.infer + diff --git a/testdata/TestConvertSliceWithValidations/ne.golden b/testdata/TestConvertSliceWithValidations/ne.golden new file mode 100644 index 0000000..dfd7144 --- /dev/null +++ b/testdata/TestConvertSliceWithValidations/ne.golden @@ -0,0 +1,5 @@ +export const NeSchema = z.object({ + Slice: z.string().array().refine((val) => val.length !== 0), +}) +export type Ne = z.infer + diff --git a/testdata/TestConvertSliceWithValidations/required.golden b/testdata/TestConvertSliceWithValidations/required.golden new file mode 100644 index 0000000..677595f --- /dev/null +++ b/testdata/TestConvertSliceWithValidations/required.golden @@ -0,0 +1,5 @@ +export const RequiredSchema = z.object({ + Slice: z.string().array(), +}) +export type Required = z.infer + diff --git a/testdata/TestCustom.golden b/testdata/TestCustom.golden new file mode 100644 index 0000000..515dd52 --- /dev/null +++ b/testdata/TestCustom.golden @@ -0,0 +1,6 @@ +export const UserSchema = z.object({ + Name: z.string(), + Money: z.string(), +}) +export type User = z.infer + diff --git a/testdata/TestCustomTag/v3.golden b/testdata/TestCustomTag/v3.golden new file mode 100644 index 0000000..b345d45 --- /dev/null +++ b/testdata/TestCustomTag/v3.golden @@ -0,0 +1,15 @@ +export const SortParamsSchema = z.object({ + order: z.enum(["asc", "desc"] as const).optional(), + field: z.string().optional(), +}) +export type SortParams = z.infer + +export const RequestSchema = z.object({ + PaginationParams: z.object({ + start: z.number().gt(0).optional(), + end: z.number().gt(0).optional(), + }).refine((val) => !val.start || !val.end || val.start < val.end, 'Start should be less than end'), + search: z.string().refine((val) => !val || /^[a-z0-9_]*$/.test(val), 'Invalid search identifier').optional(), +}).merge(SortParamsSchema.extend({field: z.enum(['title', 'address', 'age', 'dob'])})) +export type Request = z.infer + diff --git a/testdata/TestCustomTag/v4.golden b/testdata/TestCustomTag/v4.golden new file mode 100644 index 0000000..3a03607 --- /dev/null +++ b/testdata/TestCustomTag/v4.golden @@ -0,0 +1,16 @@ +export const SortParamsSchema = z.object({ + order: z.enum(["asc", "desc"] as const).optional(), + field: z.string().optional(), +}) +export type SortParams = z.infer + +export const RequestSchema = z.object({ + PaginationParams: z.object({ + start: z.number().gt(0).optional(), + end: z.number().gt(0).optional(), + }).refine((val) => !val.start || !val.end || val.start < val.end, 'Start should be less than end'), + search: z.string().refine((val) => !val || /^[a-z0-9_]*$/.test(val), 'Invalid search identifier').optional(), + ...SortParamsSchema.extend({field: z.enum(['title', 'address', 'age', 'dob'])}).shape, +}) +export type Request = z.infer + diff --git a/testdata/TestDuration.golden b/testdata/TestDuration.golden new file mode 100644 index 0000000..7504872 --- /dev/null +++ b/testdata/TestDuration.golden @@ -0,0 +1,5 @@ +export const UserSchema = z.object({ + HowLong: z.number(), +}) +export type User = z.infer + diff --git a/testdata/TestEverything.golden b/testdata/TestEverything.golden new file mode 100644 index 0000000..c64ee31 --- /dev/null +++ b/testdata/TestEverything.golden @@ -0,0 +1,41 @@ +export const PostSchema = z.object({ + Title: z.string(), +}) +export type Post = z.infer + +export const PostWithMetaDataSchema = z.object({ + Title: z.string(), + Post: PostSchema, +}) +export type PostWithMetaData = z.infer + +export const UserSchema = z.object({ + Name: z.string(), + Nickname: z.string().nullable(), + Age: z.number(), + Height: z.number(), + OldPostWithMetaData: PostWithMetaDataSchema, + Tags: z.string().array().nullable(), + TagsOptional: z.string().array().optional(), + TagsOptionalNullable: z.string().array().optional().nullable(), + Favourites: z.object({ + Name: z.string(), + }).array().nullable(), + Posts: PostSchema.array().nullable(), + Post: PostSchema, + PostOptional: PostSchema.optional(), + PostOptionalNullable: PostSchema.optional().nullable(), + Metadata: z.record(z.string(), z.string()).nullable(), + MetadataOptional: z.record(z.string(), z.string()).optional(), + MetadataOptionalNullable: z.record(z.string(), z.string()).optional().nullable(), + ExtendedProps: z.any(), + ExtendedPropsOptional: z.any(), + ExtendedPropsNullable: z.any(), + ExtendedPropsOptionalNullable: z.any(), + ExtendedPropsVeryIndirect: z.any(), + NewPostWithMetaData: PostWithMetaDataSchema, + VeryNewPost: PostSchema, + MapWithStruct: z.record(z.string(), PostWithMetaDataSchema).nullable(), +}) +export type User = z.infer + diff --git a/testdata/TestEverythingWithValidations.golden b/testdata/TestEverythingWithValidations.golden new file mode 100644 index 0000000..c7fc1c5 --- /dev/null +++ b/testdata/TestEverythingWithValidations.golden @@ -0,0 +1,42 @@ +export const PostSchema = z.object({ + Title: z.string().min(1), +}) +export type Post = z.infer + +export const PostWithMetaDataSchema = z.object({ + Title: z.string().min(1), + Post: PostSchema, +}) +export type PostWithMetaData = z.infer + +export const UserSchema = z.object({ + Name: z.string().min(1), + Nickname: z.string().nullable(), + Age: z.number().gte(18).refine((val) => val !== 0), + Height: z.number().gte(1.5).refine((val) => val !== 0), + OldPostWithMetaData: PostWithMetaDataSchema, + Tags: z.string().array().min(1), + TagsOptional: z.string().array().optional(), + TagsOptionalNullable: z.string().array().optional().nullable(), + Favourites: z.object({ + Name: z.string().min(1), + }).array().nullable(), + Posts: PostSchema.array(), + Post: PostSchema, + PostOptional: PostSchema.optional(), + PostOptionalNullable: PostSchema.optional().nullable(), + Metadata: z.record(z.string(), z.string()).nullable(), + MetadataLength: z.record(z.string(), z.string()).refine((val) => Object.keys(val).length >= 1, 'Map too small').refine((val) => Object.keys(val).length <= 10, 'Map too large'), + MetadataOptional: z.record(z.string(), z.string()).optional(), + MetadataOptionalNullable: z.record(z.string(), z.string()).optional().nullable(), + ExtendedProps: z.any(), + ExtendedPropsOptional: z.any(), + ExtendedPropsNullable: z.any(), + ExtendedPropsOptionalNullable: z.any(), + ExtendedPropsVeryIndirect: z.any(), + NewPostWithMetaData: PostWithMetaDataSchema, + VeryNewPost: PostSchema, + MapWithStruct: z.record(z.string(), PostWithMetaDataSchema).nullable(), +}) +export type User = z.infer + diff --git a/testdata/TestGenerics.golden b/testdata/TestGenerics.golden new file mode 100644 index 0000000..df79dfd --- /dev/null +++ b/testdata/TestGenerics.golden @@ -0,0 +1,17 @@ +export const StringIntPairSchema = z.object({ + First: z.string(), + Second: z.number(), +}) +export type StringIntPair = z.infer + +export const GenericPairIntBoolSchema = z.object({ + First: z.number(), + Second: z.boolean(), +}) +export type GenericPairIntBool = z.infer + +export const PairMapStringIntBoolSchema = z.object({ + items: z.record(z.string(), GenericPairIntBoolSchema).nullable(), +}) +export type PairMapStringIntBool = z.infer + diff --git a/testdata/TestInterfaceAny.golden b/testdata/TestInterfaceAny.golden new file mode 100644 index 0000000..3f83cc0 --- /dev/null +++ b/testdata/TestInterfaceAny.golden @@ -0,0 +1,6 @@ +export const UserSchema = z.object({ + Name: z.string(), + Metadata: z.any(), +}) +export type User = z.infer + diff --git a/testdata/TestInterfaceEmptyAny.golden b/testdata/TestInterfaceEmptyAny.golden new file mode 100644 index 0000000..3f83cc0 --- /dev/null +++ b/testdata/TestInterfaceEmptyAny.golden @@ -0,0 +1,6 @@ +export const UserSchema = z.object({ + Name: z.string(), + Metadata: z.any(), +}) +export type User = z.infer + diff --git a/testdata/TestInterfacePointerAny.golden b/testdata/TestInterfacePointerAny.golden new file mode 100644 index 0000000..3f83cc0 --- /dev/null +++ b/testdata/TestInterfacePointerAny.golden @@ -0,0 +1,6 @@ +export const UserSchema = z.object({ + Name: z.string(), + Metadata: z.any(), +}) +export type User = z.infer + diff --git a/testdata/TestInterfacePointerEmptyAny.golden b/testdata/TestInterfacePointerEmptyAny.golden new file mode 100644 index 0000000..3f83cc0 --- /dev/null +++ b/testdata/TestInterfacePointerEmptyAny.golden @@ -0,0 +1,6 @@ +export const UserSchema = z.object({ + Name: z.string(), + Metadata: z.any(), +}) +export type User = z.infer + diff --git a/testdata/TestMapStringToInterface.golden b/testdata/TestMapStringToInterface.golden new file mode 100644 index 0000000..36c00f8 --- /dev/null +++ b/testdata/TestMapStringToInterface.golden @@ -0,0 +1,6 @@ +export const UserSchema = z.object({ + Name: z.string(), + Metadata: z.record(z.string(), z.any()).nullable(), +}) +export type User = z.infer + diff --git a/testdata/TestMapStringToString.golden b/testdata/TestMapStringToString.golden new file mode 100644 index 0000000..43d1a57 --- /dev/null +++ b/testdata/TestMapStringToString.golden @@ -0,0 +1,6 @@ +export const UserSchema = z.object({ + Name: z.string(), + Metadata: z.record(z.string(), z.string()).nullable(), +}) +export type User = z.infer + diff --git a/testdata/TestMapWithNonStringKey/float_key.golden b/testdata/TestMapWithNonStringKey/float_key.golden new file mode 100644 index 0000000..0c0143c --- /dev/null +++ b/testdata/TestMapWithNonStringKey/float_key.golden @@ -0,0 +1,6 @@ +export const Map3Schema = z.object({ + Name: z.string(), + Metadata: z.record(z.coerce.number(), z.string()).nullable(), +}) +export type Map3 = z.infer + diff --git a/testdata/TestMapWithNonStringKey/int_key.golden b/testdata/TestMapWithNonStringKey/int_key.golden new file mode 100644 index 0000000..94d603e --- /dev/null +++ b/testdata/TestMapWithNonStringKey/int_key.golden @@ -0,0 +1,6 @@ +export const Map1Schema = z.object({ + Name: z.string(), + Metadata: z.record(z.coerce.number(), z.string()).nullable(), +}) +export type Map1 = z.infer + diff --git a/testdata/TestMapWithNonStringKey/time_key.golden b/testdata/TestMapWithNonStringKey/time_key.golden new file mode 100644 index 0000000..efad40a --- /dev/null +++ b/testdata/TestMapWithNonStringKey/time_key.golden @@ -0,0 +1,6 @@ +export const Map2Schema = z.object({ + Name: z.string(), + Metadata: z.record(z.coerce.date(), z.string()).nullable(), +}) +export type Map2 = z.infer + diff --git a/testdata/TestMapWithStruct.golden b/testdata/TestMapWithStruct.golden new file mode 100644 index 0000000..1f49715 --- /dev/null +++ b/testdata/TestMapWithStruct.golden @@ -0,0 +1,10 @@ +export const PostWithMetaDataSchema = z.object({ + Title: z.string(), +}) +export type PostWithMetaData = z.infer + +export const UserSchema = z.object({ + MapWithStruct: z.record(z.string(), PostWithMetaDataSchema).nullable(), +}) +export type User = z.infer + diff --git a/testdata/TestMapWithValidations/dive1.golden b/testdata/TestMapWithValidations/dive1.golden new file mode 100644 index 0000000..67e5c10 --- /dev/null +++ b/testdata/TestMapWithValidations/dive1.golden @@ -0,0 +1,5 @@ +export const Dive1Schema = z.object({ + 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 + diff --git a/testdata/TestMapWithValidations/dive2.golden b/testdata/TestMapWithValidations/dive2.golden new file mode 100644 index 0000000..00143a5 --- /dev/null +++ b/testdata/TestMapWithValidations/dive2.golden @@ -0,0 +1,5 @@ +export const Dive2Schema = z.object({ + 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 + diff --git a/testdata/TestMapWithValidations/dive3.golden b/testdata/TestMapWithValidations/dive3.golden new file mode 100644 index 0000000..c10a8aa --- /dev/null +++ b/testdata/TestMapWithValidations/dive3.golden @@ -0,0 +1,5 @@ +export const Dive3Schema = z.object({ + 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 + diff --git a/testdata/TestMapWithValidations/eq.golden b/testdata/TestMapWithValidations/eq.golden new file mode 100644 index 0000000..c6b20a4 --- /dev/null +++ b/testdata/TestMapWithValidations/eq.golden @@ -0,0 +1,5 @@ +export const EqSchema = z.object({ + Map: z.record(z.string(), z.string()).refine((val) => Object.keys(val).length === 1, 'Map wrong size'), +}) +export type Eq = z.infer + diff --git a/testdata/TestMapWithValidations/gt.golden b/testdata/TestMapWithValidations/gt.golden new file mode 100644 index 0000000..d68285e --- /dev/null +++ b/testdata/TestMapWithValidations/gt.golden @@ -0,0 +1,5 @@ +export const GtSchema = z.object({ + Map: z.record(z.string(), z.string()).refine((val) => Object.keys(val).length > 1, 'Map too small'), +}) +export type Gt = z.infer + diff --git a/testdata/TestMapWithValidations/gte.golden b/testdata/TestMapWithValidations/gte.golden new file mode 100644 index 0000000..eacbbd3 --- /dev/null +++ b/testdata/TestMapWithValidations/gte.golden @@ -0,0 +1,5 @@ +export const GteSchema = z.object({ + Map: z.record(z.string(), z.string()).refine((val) => Object.keys(val).length >= 1, 'Map too small'), +}) +export type Gte = z.infer + diff --git a/testdata/TestMapWithValidations/len.golden b/testdata/TestMapWithValidations/len.golden new file mode 100644 index 0000000..7640ab2 --- /dev/null +++ b/testdata/TestMapWithValidations/len.golden @@ -0,0 +1,5 @@ +export const LenSchema = z.object({ + Map: z.record(z.string(), z.string()).refine((val) => Object.keys(val).length === 1, 'Map wrong size'), +}) +export type Len = z.infer + diff --git a/testdata/TestMapWithValidations/lt.golden b/testdata/TestMapWithValidations/lt.golden new file mode 100644 index 0000000..b6b534b --- /dev/null +++ b/testdata/TestMapWithValidations/lt.golden @@ -0,0 +1,5 @@ +export const LtSchema = z.object({ + Map: z.record(z.string(), z.string()).refine((val) => Object.keys(val).length < 1, 'Map too large'), +}) +export type Lt = z.infer + diff --git a/testdata/TestMapWithValidations/lte.golden b/testdata/TestMapWithValidations/lte.golden new file mode 100644 index 0000000..de55553 --- /dev/null +++ b/testdata/TestMapWithValidations/lte.golden @@ -0,0 +1,5 @@ +export const LteSchema = z.object({ + Map: z.record(z.string(), z.string()).refine((val) => Object.keys(val).length <= 1, 'Map too large'), +}) +export type Lte = z.infer + diff --git a/testdata/TestMapWithValidations/max.golden b/testdata/TestMapWithValidations/max.golden new file mode 100644 index 0000000..ef80246 --- /dev/null +++ b/testdata/TestMapWithValidations/max.golden @@ -0,0 +1,5 @@ +export const MaxSchema = z.object({ + Map: z.record(z.string(), z.string()).refine((val) => Object.keys(val).length <= 1, 'Map too large'), +}) +export type Max = z.infer + diff --git a/testdata/TestMapWithValidations/min.golden b/testdata/TestMapWithValidations/min.golden new file mode 100644 index 0000000..c7e8a5e --- /dev/null +++ b/testdata/TestMapWithValidations/min.golden @@ -0,0 +1,5 @@ +export const MinSchema = z.object({ + Map: z.record(z.string(), z.string()).refine((val) => Object.keys(val).length >= 1, 'Map too small'), +}) +export type Min = z.infer + diff --git a/testdata/TestMapWithValidations/minmax.golden b/testdata/TestMapWithValidations/minmax.golden new file mode 100644 index 0000000..2d53c0a --- /dev/null +++ b/testdata/TestMapWithValidations/minmax.golden @@ -0,0 +1,5 @@ +export const MinMaxSchema = z.object({ + Map: z.record(z.string(), z.string()).refine((val) => Object.keys(val).length >= 1, 'Map too small').refine((val) => Object.keys(val).length <= 2, 'Map too large'), +}) +export type MinMax = z.infer + diff --git a/testdata/TestMapWithValidations/ne.golden b/testdata/TestMapWithValidations/ne.golden new file mode 100644 index 0000000..53102a3 --- /dev/null +++ b/testdata/TestMapWithValidations/ne.golden @@ -0,0 +1,5 @@ +export const NeSchema = z.object({ + Map: z.record(z.string(), z.string()).refine((val) => Object.keys(val).length !== 1, 'Map wrong size'), +}) +export type Ne = z.infer + diff --git a/testdata/TestMapWithValidations/required.golden b/testdata/TestMapWithValidations/required.golden new file mode 100644 index 0000000..36baebd --- /dev/null +++ b/testdata/TestMapWithValidations/required.golden @@ -0,0 +1,5 @@ +export const RequiredSchema = z.object({ + Map: z.record(z.string(), z.string()), +}) +export type Required = z.infer + diff --git a/testdata/TestNestedStruct/v3.golden b/testdata/TestNestedStruct/v3.golden new file mode 100644 index 0000000..160c5a9 --- /dev/null +++ b/testdata/TestNestedStruct/v3.golden @@ -0,0 +1,15 @@ +export const HasIDSchema = z.object({ + ID: z.string(), +}) +export type HasID = z.infer + +export const HasNameSchema = z.object({ + name: z.string(), +}) +export type HasName = z.infer + +export const UserSchema = z.object({ + Tags: z.string().array().nullable(), +}).merge(HasIDSchema).merge(HasNameSchema) +export type User = z.infer + diff --git a/testdata/TestNestedStruct/v4.golden b/testdata/TestNestedStruct/v4.golden new file mode 100644 index 0000000..30a7b0e --- /dev/null +++ b/testdata/TestNestedStruct/v4.golden @@ -0,0 +1,17 @@ +export const HasIDSchema = z.object({ + ID: z.string(), +}) +export type HasID = z.infer + +export const HasNameSchema = z.object({ + name: z.string(), +}) +export type HasName = z.infer + +export const UserSchema = z.object({ + Tags: z.string().array().nullable(), + ...HasIDSchema.shape, + ...HasNameSchema.shape, +}) +export type User = z.infer + diff --git a/testdata/TestNullableWithValidations.golden b/testdata/TestNullableWithValidations.golden new file mode 100644 index 0000000..56f107a --- /dev/null +++ b/testdata/TestNullableWithValidations.golden @@ -0,0 +1,35 @@ +export const UserSchema = z.object({ + Name: z.string().min(1), + PtrMapOptionalNullable1: z.record(z.string(), z.any()).optional().nullable(), + PtrMapOptionalNullable2: z.record(z.string(), z.any()).refine((val) => Object.keys(val).length >= 2, 'Map too small').refine((val) => Object.keys(val).length <= 5, 'Map too large').optional().nullable(), + PtrMap1: z.record(z.string(), z.any()).refine((val) => Object.keys(val).length >= 2, 'Map too small').refine((val) => Object.keys(val).length <= 5, 'Map too large'), + PtrMap2: z.record(z.string(), z.any()).refine((val) => Object.keys(val).length >= 2, 'Map too small').refine((val) => Object.keys(val).length <= 5, 'Map too large'), + PtrMapNullable: z.record(z.string(), z.any()).refine((val) => Object.keys(val).length >= 2, 'Map too small').refine((val) => Object.keys(val).length <= 5, 'Map too large').nullable(), + MapOptional1: z.record(z.string(), z.any()).optional(), + MapOptional2: z.record(z.string(), z.any()).refine((val) => Object.keys(val).length >= 2, 'Map too small').refine((val) => Object.keys(val).length <= 5, 'Map too large').optional(), + Map1: z.record(z.string(), z.any()).refine((val) => Object.keys(val).length >= 2, 'Map too small').refine((val) => Object.keys(val).length <= 5, 'Map too large'), + Map2: z.record(z.string(), z.any()).refine((val) => Object.keys(val).length >= 2, 'Map too small').refine((val) => Object.keys(val).length <= 5, 'Map too large'), + MapNullable: z.record(z.string(), z.any()).refine((val) => Object.keys(val).length >= 2, 'Map too small').refine((val) => Object.keys(val).length <= 5, 'Map too large').nullable(), + PtrSliceOptionalNullable1: z.string().array().optional().nullable(), + PtrSliceOptionalNullable2: z.string().array().min(2).max(5).optional().nullable(), + PtrSlice1: z.string().array().min(2).max(5), + PtrSlice2: z.string().array().min(2).max(5), + PtrSliceNullable: z.string().array().min(2).max(5).nullable(), + SliceOptional1: z.string().array().optional(), + SliceOptional2: z.string().array().min(2).max(5).optional(), + Slice1: z.string().array().min(2).max(5), + Slice2: z.string().array().min(2).max(5), + SliceNullable: z.string().array().min(2).max(5).nullable(), + PtrIntOptional1: z.number().optional(), + PtrIntOptional2: z.number().gte(2).lte(5).optional(), + PtrInt1: z.number().gte(2).lte(5), + PtrInt2: z.number().gte(2).lte(5), + PtrIntNullable: z.number().gte(2).lte(5).nullable(), + PtrStringOptional1: z.string().optional(), + 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 + diff --git a/testdata/TestNumberValidations/eq.golden b/testdata/TestNumberValidations/eq.golden new file mode 100644 index 0000000..261bcf4 --- /dev/null +++ b/testdata/TestNumberValidations/eq.golden @@ -0,0 +1,5 @@ +export const User3Schema = z.object({ + Age: z.number().refine((val) => val === 18), +}) +export type User3 = z.infer + diff --git a/testdata/TestNumberValidations/gt_lt.golden b/testdata/TestNumberValidations/gt_lt.golden new file mode 100644 index 0000000..fbebdc6 --- /dev/null +++ b/testdata/TestNumberValidations/gt_lt.golden @@ -0,0 +1,5 @@ +export const User2Schema = z.object({ + Age: z.number().gt(18).lt(60), +}) +export type User2 = z.infer + diff --git a/testdata/TestNumberValidations/gte_lte.golden b/testdata/TestNumberValidations/gte_lte.golden new file mode 100644 index 0000000..1b41ef9 --- /dev/null +++ b/testdata/TestNumberValidations/gte_lte.golden @@ -0,0 +1,5 @@ +export const User1Schema = z.object({ + Age: z.number().gte(18).lte(60), +}) +export type User1 = z.infer + diff --git a/testdata/TestNumberValidations/len.golden b/testdata/TestNumberValidations/len.golden new file mode 100644 index 0000000..d22e72e --- /dev/null +++ b/testdata/TestNumberValidations/len.golden @@ -0,0 +1,5 @@ +export const User7Schema = z.object({ + Age: z.number().refine((val) => val === 18), +}) +export type User7 = z.infer + diff --git a/testdata/TestNumberValidations/min_max.golden b/testdata/TestNumberValidations/min_max.golden new file mode 100644 index 0000000..aac74da --- /dev/null +++ b/testdata/TestNumberValidations/min_max.golden @@ -0,0 +1,5 @@ +export const User6Schema = z.object({ + Age: z.number().gte(18).lte(60), +}) +export type User6 = z.infer + diff --git a/testdata/TestNumberValidations/ne.golden b/testdata/TestNumberValidations/ne.golden new file mode 100644 index 0000000..85cf5fb --- /dev/null +++ b/testdata/TestNumberValidations/ne.golden @@ -0,0 +1,5 @@ +export const User4Schema = z.object({ + Age: z.number().refine((val) => val !== 18), +}) +export type User4 = z.infer + diff --git a/testdata/TestNumberValidations/oneof.golden b/testdata/TestNumberValidations/oneof.golden new file mode 100644 index 0000000..f2b4d4e --- /dev/null +++ b/testdata/TestNumberValidations/oneof.golden @@ -0,0 +1,5 @@ +export const User5Schema = z.object({ + Age: z.number().refine((val) => [18, 19, 20].includes(val)), +}) +export type User5 = z.infer + diff --git a/testdata/TestRecursive1/v3.golden b/testdata/TestRecursive1/v3.golden new file mode 100644 index 0000000..0c6c645 --- /dev/null +++ b/testdata/TestRecursive1/v3.golden @@ -0,0 +1,18 @@ +export type NestedItem = { + id: number, + title: string, + pos: number, + parent_id: number, + project_id: number, + children: NestedItem[] | null, +} +const NestedItemSchemaShape = { + id: z.number(), + title: z.string(), + pos: z.number(), + parent_id: z.number(), + project_id: z.number(), + children: z.lazy(() => NestedItemSchema).array().nullable(), +} +export const NestedItemSchema: z.ZodType = z.object(NestedItemSchemaShape) + diff --git a/testdata/TestRecursive1/v4.golden b/testdata/TestRecursive1/v4.golden new file mode 100644 index 0000000..89dcd93 --- /dev/null +++ b/testdata/TestRecursive1/v4.golden @@ -0,0 +1,18 @@ +export type NestedItem = { + id: number, + title: string, + pos: number, + parent_id: number, + project_id: number, + children: NestedItem[] | null, +} +const NestedItemSchemaShape = { + id: z.number(), + title: z.string(), + pos: z.number(), + parent_id: z.number(), + project_id: z.number(), + get children() { return NestedItemSchema.array().nullable(); }, +} +export const NestedItemSchema: z.ZodType = z.object(NestedItemSchemaShape) + diff --git a/testdata/TestRecursive2/v3.golden b/testdata/TestRecursive2/v3.golden new file mode 100644 index 0000000..86aa76a --- /dev/null +++ b/testdata/TestRecursive2/v3.golden @@ -0,0 +1,15 @@ +export type Node = { + value: number, + next: Node | null, +} +const NodeSchemaShape = { + value: z.number(), + next: z.lazy(() => NodeSchema).nullable(), +} +export const NodeSchema: z.ZodType = z.object(NodeSchemaShape) + +export const ParentSchema = z.object({ + child: NodeSchema.nullable(), +}) +export type Parent = z.infer + diff --git a/testdata/TestRecursive2/v4.golden b/testdata/TestRecursive2/v4.golden new file mode 100644 index 0000000..813ad9f --- /dev/null +++ b/testdata/TestRecursive2/v4.golden @@ -0,0 +1,15 @@ +export type Node = { + value: number, + next: Node | null, +} +const NodeSchemaShape = { + value: z.number(), + get next() { return NodeSchema.nullable(); }, +} +export const NodeSchema: z.ZodType = z.object(NodeSchemaShape) + +export const ParentSchema = z.object({ + child: NodeSchema.nullable(), +}) +export type Parent = z.infer + diff --git a/testdata/TestRecursiveEmbeddedStruct/v3.golden b/testdata/TestRecursiveEmbeddedStruct/v3.golden new file mode 100644 index 0000000..7f23bde --- /dev/null +++ b/testdata/TestRecursiveEmbeddedStruct/v3.golden @@ -0,0 +1,39 @@ +export type ItemA = { + Name: string, + Children: ItemA[] | null, +} +const ItemASchemaShape = { + Name: z.string(), + Children: z.lazy(() => ItemASchema).array().nullable(), +} +export const ItemASchema: z.ZodType = z.object(ItemASchemaShape) + +export const ItemBSchema = z.object({ + ...ItemASchemaShape, +}) +export type ItemB = z.infer + +export const ItemCSchema = z.object({ +}).merge(ItemBSchema) +export type ItemC = z.infer + +export const ItemDSchema = z.object({ + ItemA: ItemASchema, +}) +export type ItemD = z.infer + +export type ItemE = ItemA & ItemD & { + Children: ItemE[] | null, +} +const ItemESchemaShape = { + ...ItemASchemaShape, + ...ItemDSchema.shape, + Children: z.lazy(() => ItemESchema).array().nullable(), +} +export const ItemESchema: z.ZodType = z.object(ItemESchemaShape) + +export const ItemFSchema = z.object({ + ...ItemESchemaShape, +}) +export type ItemF = z.infer + diff --git a/testdata/TestRecursiveEmbeddedStruct/v4.golden b/testdata/TestRecursiveEmbeddedStruct/v4.golden new file mode 100644 index 0000000..dba6903 --- /dev/null +++ b/testdata/TestRecursiveEmbeddedStruct/v4.golden @@ -0,0 +1,40 @@ +export type ItemA = { + Name: string, + Children: ItemA[] | null, +} +const ItemASchemaShape = { + Name: z.string(), + get Children() { return ItemASchema.array().nullable(); }, +} +export const ItemASchema: z.ZodType = z.object(ItemASchemaShape) + +export const ItemBSchema = z.object({ + ...ItemASchemaShape, +}) +export type ItemB = z.infer + +export const ItemCSchema = z.object({ + ...ItemBSchema.shape, +}) +export type ItemC = z.infer + +export const ItemDSchema = z.object({ + ItemA: ItemASchema, +}) +export type ItemD = z.infer + +export type ItemE = ItemA & ItemD & { + Children: ItemE[] | null, +} +const ItemESchemaShape = { + ...ItemASchemaShape, + ...ItemDSchema.shape, + get Children() { return ItemESchema.array().nullable(); }, +} +export const ItemESchema: z.ZodType = z.object(ItemESchemaShape) + +export const ItemFSchema = z.object({ + ...ItemESchemaShape, +}) +export type ItemF = z.infer + diff --git a/testdata/TestRecursiveEmbeddedWithPointersAndDates/embedded_struct_with_pointer_to_self_and_date/v3.golden b/testdata/TestRecursiveEmbeddedWithPointersAndDates/embedded_struct_with_pointer_to_self_and_date/v3.golden new file mode 100644 index 0000000..17fd80f --- /dev/null +++ b/testdata/TestRecursiveEmbeddedWithPointersAndDates/embedded_struct_with_pointer_to_self_and_date/v3.golden @@ -0,0 +1,18 @@ +export type Comment = { + Text: string, + Timestamp: Date, + Reply: Comment | null, +} +const CommentSchemaShape = { + Text: z.string(), + Timestamp: z.coerce.date(), + Reply: z.lazy(() => CommentSchema).nullable(), +} +export const CommentSchema: z.ZodType = z.object(CommentSchemaShape) + +export const ArticleSchema = z.object({ + ...CommentSchemaShape, + Title: z.string(), +}) +export type Article = z.infer + diff --git a/testdata/TestRecursiveEmbeddedWithPointersAndDates/embedded_struct_with_pointer_to_self_and_date/v4.golden b/testdata/TestRecursiveEmbeddedWithPointersAndDates/embedded_struct_with_pointer_to_self_and_date/v4.golden new file mode 100644 index 0000000..ed3cb84 --- /dev/null +++ b/testdata/TestRecursiveEmbeddedWithPointersAndDates/embedded_struct_with_pointer_to_self_and_date/v4.golden @@ -0,0 +1,18 @@ +export type Comment = { + Text: string, + Timestamp: Date, + Reply: Comment | null, +} +const CommentSchemaShape = { + Text: z.string(), + Timestamp: z.coerce.date(), + get Reply() { return CommentSchema.nullable(); }, +} +export const CommentSchema: z.ZodType = z.object(CommentSchemaShape) + +export const ArticleSchema = z.object({ + Title: z.string(), + ...CommentSchemaShape, +}) +export type Article = z.infer + diff --git a/testdata/TestRecursiveEmbeddedWithPointersAndDates/recursive_struct_with_pointer_field_and_date/v3.golden b/testdata/TestRecursiveEmbeddedWithPointersAndDates/recursive_struct_with_pointer_field_and_date/v3.golden new file mode 100644 index 0000000..8c7a1f3 --- /dev/null +++ b/testdata/TestRecursiveEmbeddedWithPointersAndDates/recursive_struct_with_pointer_field_and_date/v3.golden @@ -0,0 +1,18 @@ +export type TreeNode = { + Value: string, + CreatedAt: Date, + Children: TreeNode[] | null, +} +const TreeNodeSchemaShape = { + Value: z.string(), + CreatedAt: z.coerce.date(), + Children: z.lazy(() => TreeNodeSchema).array().nullable(), +} +export const TreeNodeSchema: z.ZodType = z.object(TreeNodeSchemaShape) + +export const TreeSchema = z.object({ + ...TreeNodeSchemaShape, + UpdatedAt: z.coerce.date(), +}) +export type Tree = z.infer + diff --git a/testdata/TestRecursiveEmbeddedWithPointersAndDates/recursive_struct_with_pointer_field_and_date/v4.golden b/testdata/TestRecursiveEmbeddedWithPointersAndDates/recursive_struct_with_pointer_field_and_date/v4.golden new file mode 100644 index 0000000..afead2e --- /dev/null +++ b/testdata/TestRecursiveEmbeddedWithPointersAndDates/recursive_struct_with_pointer_field_and_date/v4.golden @@ -0,0 +1,18 @@ +export type TreeNode = { + Value: string, + CreatedAt: Date, + Children: TreeNode[] | null, +} +const TreeNodeSchemaShape = { + Value: z.string(), + CreatedAt: z.coerce.date(), + get Children() { return TreeNodeSchema.array().nullable(); }, +} +export const TreeNodeSchema: z.ZodType = z.object(TreeNodeSchemaShape) + +export const TreeSchema = z.object({ + UpdatedAt: z.coerce.date(), + ...TreeNodeSchemaShape, +}) +export type Tree = z.infer + diff --git a/testdata/TestSliceFields.golden b/testdata/TestSliceFields.golden new file mode 100644 index 0000000..223d6ea --- /dev/null +++ b/testdata/TestSliceFields.golden @@ -0,0 +1,11 @@ +export const TestSliceFieldsStructSchema = z.object({ + NoValidate: z.number().array().nullable(), + Required: z.number().array(), + Min: z.number().array().min(1), + OmitEmpty: z.number().array().nullable(), + JSONOmitEmpty: z.number().array().optional(), + MinOmitEmpty: z.number().array().min(1).nullable(), + JSONMinOmitEmpty: z.number().array().min(1).optional(), +}) +export type TestSliceFieldsStruct = z.infer + diff --git a/testdata/TestStringArray.golden b/testdata/TestStringArray.golden new file mode 100644 index 0000000..e1de802 --- /dev/null +++ b/testdata/TestStringArray.golden @@ -0,0 +1,5 @@ +export const UserSchema = z.object({ + Tags: z.string().array().nullable(), +}) +export type User = z.infer + diff --git a/testdata/TestStringArrayNullable.golden b/testdata/TestStringArrayNullable.golden new file mode 100644 index 0000000..26d2624 --- /dev/null +++ b/testdata/TestStringArrayNullable.golden @@ -0,0 +1,6 @@ +export const UserSchema = z.object({ + Name: z.string(), + Tags: z.string().array().nullable(), +}) +export type User = z.infer + diff --git a/testdata/TestStringNestedArray.golden b/testdata/TestStringNestedArray.golden new file mode 100644 index 0000000..e8b001d --- /dev/null +++ b/testdata/TestStringNestedArray.golden @@ -0,0 +1,5 @@ +export const UserSchema = z.object({ + TagPairs: z.string().array().length(2).array().nullable(), +}) +export type User = z.infer + diff --git a/testdata/TestStringNullable.golden b/testdata/TestStringNullable.golden new file mode 100644 index 0000000..1d4282a --- /dev/null +++ b/testdata/TestStringNullable.golden @@ -0,0 +1,6 @@ +export const UserSchema = z.object({ + Name: z.string(), + Nickname: z.string().nullable(), +}) +export type User = z.infer + diff --git a/testdata/TestStringOptional.golden b/testdata/TestStringOptional.golden new file mode 100644 index 0000000..e629a2a --- /dev/null +++ b/testdata/TestStringOptional.golden @@ -0,0 +1,6 @@ +export const UserSchema = z.object({ + Name: z.string(), + Nickname: z.string().optional(), +}) +export type User = z.infer + diff --git a/testdata/TestStringOptionalNotNullable.golden b/testdata/TestStringOptionalNotNullable.golden new file mode 100644 index 0000000..e629a2a --- /dev/null +++ b/testdata/TestStringOptionalNotNullable.golden @@ -0,0 +1,6 @@ +export const UserSchema = z.object({ + Name: z.string(), + Nickname: z.string().optional(), +}) +export type User = z.infer + diff --git a/testdata/TestStringOptionalNullable.golden b/testdata/TestStringOptionalNullable.golden new file mode 100644 index 0000000..621f79b --- /dev/null +++ b/testdata/TestStringOptionalNullable.golden @@ -0,0 +1,6 @@ +export const UserSchema = z.object({ + Name: z.string(), + Nickname: z.string().optional().nullable(), +}) +export type User = z.infer + diff --git a/testdata/TestStringValidations/alpha.golden b/testdata/TestStringValidations/alpha.golden new file mode 100644 index 0000000..504c717 --- /dev/null +++ b/testdata/TestStringValidations/alpha.golden @@ -0,0 +1,5 @@ +export const AlphaSchema = z.object({ + Name: z.string().regex(/^[a-zA-Z]+$/), +}) +export type Alpha = z.infer + diff --git a/testdata/TestStringValidations/alphanum.golden b/testdata/TestStringValidations/alphanum.golden new file mode 100644 index 0000000..767be25 --- /dev/null +++ b/testdata/TestStringValidations/alphanum.golden @@ -0,0 +1,5 @@ +export const AlphaNumSchema = z.object({ + Name: z.string().regex(/^[a-zA-Z0-9]+$/), +}) +export type AlphaNum = z.infer + diff --git a/testdata/TestStringValidations/alphanumunicode.golden b/testdata/TestStringValidations/alphanumunicode.golden new file mode 100644 index 0000000..56f9eb0 --- /dev/null +++ b/testdata/TestStringValidations/alphanumunicode.golden @@ -0,0 +1,5 @@ +export const AlphaNumUnicodeSchema = z.object({ + Name: z.string().regex(/^[\p{L}\p{N}]+$/), +}) +export type AlphaNumUnicode = z.infer + diff --git a/testdata/TestStringValidations/alphaunicode.golden b/testdata/TestStringValidations/alphaunicode.golden new file mode 100644 index 0000000..99ec880 --- /dev/null +++ b/testdata/TestStringValidations/alphaunicode.golden @@ -0,0 +1,5 @@ +export const AlphaUnicodeSchema = z.object({ + Name: z.string().regex(/^[\p{L}]+$/), +}) +export type AlphaUnicode = z.infer + diff --git a/testdata/TestStringValidations/ascii.golden b/testdata/TestStringValidations/ascii.golden new file mode 100644 index 0000000000000000000000000000000000000000..868771d397c7620558342b99abfa047946c82de9 GIT binary patch literal 128 zcmYeTD9A4=QAp0uD=txR40iVP3{K8S%}rFWRjAU-Ps&P7F43swQc&i?%51 literal 0 HcmV?d00001 diff --git a/testdata/TestStringValidations/base64/v3.golden b/testdata/TestStringValidations/base64/v3.golden new file mode 100644 index 0000000..8b3063d --- /dev/null +++ b/testdata/TestStringValidations/base64/v3.golden @@ -0,0 +1,5 @@ +export const Base64Schema = z.object({ + Name: z.string().regex(/^(?:[A-Za-z0-9+\/]{4})*(?:[A-Za-z0-9+\/]{2}==|[A-Za-z0-9+\/]{3}=|[A-Za-z0-9+\/]{4})$/), +}) +export type Base64 = z.infer + diff --git a/testdata/TestStringValidations/base64/v4.golden b/testdata/TestStringValidations/base64/v4.golden new file mode 100644 index 0000000..cc582c9 --- /dev/null +++ b/testdata/TestStringValidations/base64/v4.golden @@ -0,0 +1,5 @@ +export const Base64Schema = z.object({ + Name: z.base64(), +}) +export type Base64 = z.infer + diff --git a/testdata/TestStringValidations/boolean.golden b/testdata/TestStringValidations/boolean.golden new file mode 100644 index 0000000..846d48e --- /dev/null +++ b/testdata/TestStringValidations/boolean.golden @@ -0,0 +1,5 @@ +export const BooleanSchema = z.object({ + Name: z.enum(['true', 'false']), +}) +export type Boolean = z.infer + diff --git a/testdata/TestStringValidations/contains.golden b/testdata/TestStringValidations/contains.golden new file mode 100644 index 0000000..4c4828e --- /dev/null +++ b/testdata/TestStringValidations/contains.golden @@ -0,0 +1,5 @@ +export const ContainsSchema = z.object({ + Name: z.string().includes("hello"), +}) +export type Contains = z.infer + diff --git a/testdata/TestStringValidations/datetime/v3.golden b/testdata/TestStringValidations/datetime/v3.golden new file mode 100644 index 0000000..60cfb98 --- /dev/null +++ b/testdata/TestStringValidations/datetime/v3.golden @@ -0,0 +1,5 @@ +export const datetimeSchema = z.object({ + Name: z.string().datetime(), +}) +export type datetime = z.infer + diff --git a/testdata/TestStringValidations/datetime/v4.golden b/testdata/TestStringValidations/datetime/v4.golden new file mode 100644 index 0000000..7301ca3 --- /dev/null +++ b/testdata/TestStringValidations/datetime/v4.golden @@ -0,0 +1,5 @@ +export const datetimeSchema = z.object({ + Name: z.iso.datetime(), +}) +export type datetime = z.infer + diff --git a/testdata/TestStringValidations/email/v3.golden b/testdata/TestStringValidations/email/v3.golden new file mode 100644 index 0000000..1505022 --- /dev/null +++ b/testdata/TestStringValidations/email/v3.golden @@ -0,0 +1,5 @@ +export const EmailSchema = z.object({ + Name: z.string().email(), +}) +export type Email = z.infer + diff --git a/testdata/TestStringValidations/email/v4.golden b/testdata/TestStringValidations/email/v4.golden new file mode 100644 index 0000000..79310be --- /dev/null +++ b/testdata/TestStringValidations/email/v4.golden @@ -0,0 +1,5 @@ +export const EmailSchema = z.object({ + Name: z.email(), +}) +export type Email = z.infer + diff --git a/testdata/TestStringValidations/endswith.golden b/testdata/TestStringValidations/endswith.golden new file mode 100644 index 0000000..3b9face --- /dev/null +++ b/testdata/TestStringValidations/endswith.golden @@ -0,0 +1,5 @@ +export const EndsWithSchema = z.object({ + Name: z.string().endsWith("hello"), +}) +export type EndsWith = z.infer + diff --git a/testdata/TestStringValidations/eq.golden b/testdata/TestStringValidations/eq.golden new file mode 100644 index 0000000..b0aa17b --- /dev/null +++ b/testdata/TestStringValidations/eq.golden @@ -0,0 +1,5 @@ +export const EqSchema = z.object({ + Name: z.string().refine((val) => val === "hello"), +}) +export type Eq = z.infer + diff --git a/testdata/TestStringValidations/gt.golden b/testdata/TestStringValidations/gt.golden new file mode 100644 index 0000000..3f6646c --- /dev/null +++ b/testdata/TestStringValidations/gt.golden @@ -0,0 +1,5 @@ +export const GtSchema = z.object({ + Name: z.string().refine((val) => [...val].length > 5, 'String must contain at least 6 character(s)'), +}) +export type Gt = z.infer + diff --git a/testdata/TestStringValidations/gte.golden b/testdata/TestStringValidations/gte.golden new file mode 100644 index 0000000..dfb471d --- /dev/null +++ b/testdata/TestStringValidations/gte.golden @@ -0,0 +1,5 @@ +export const GteSchema = z.object({ + Name: z.string().refine((val) => [...val].length >= 5, 'String must contain at least 5 character(s)'), +}) +export type Gte = z.infer + diff --git a/testdata/TestStringValidations/hexadecimal/v3.golden b/testdata/TestStringValidations/hexadecimal/v3.golden new file mode 100644 index 0000000..a618d1a --- /dev/null +++ b/testdata/TestStringValidations/hexadecimal/v3.golden @@ -0,0 +1,5 @@ +export const HexadecimalSchema = z.object({ + Name: z.string().regex(/^(0[xX])?[0-9a-fA-F]+$/), +}) +export type Hexadecimal = z.infer + diff --git a/testdata/TestStringValidations/hexadecimal/v4.golden b/testdata/TestStringValidations/hexadecimal/v4.golden new file mode 100644 index 0000000..bc70aef --- /dev/null +++ b/testdata/TestStringValidations/hexadecimal/v4.golden @@ -0,0 +1,5 @@ +export const HexadecimalSchema = z.object({ + Name: z.hex(), +}) +export type Hexadecimal = z.infer + diff --git a/testdata/TestStringValidations/http_url/v3.golden b/testdata/TestStringValidations/http_url/v3.golden new file mode 100644 index 0000000..eb5cdd7 --- /dev/null +++ b/testdata/TestStringValidations/http_url/v3.golden @@ -0,0 +1,5 @@ +export const HttpURLSchema = z.object({ + Name: z.string().url(), +}) +export type HttpURL = z.infer + diff --git a/testdata/TestStringValidations/http_url/v4.golden b/testdata/TestStringValidations/http_url/v4.golden new file mode 100644 index 0000000..5f8210d --- /dev/null +++ b/testdata/TestStringValidations/http_url/v4.golden @@ -0,0 +1,5 @@ +export const HttpURLSchema = z.object({ + Name: z.httpUrl(), +}) +export type HttpURL = z.infer + diff --git a/testdata/TestStringValidations/ip/v3.golden b/testdata/TestStringValidations/ip/v3.golden new file mode 100644 index 0000000..77781f0 --- /dev/null +++ b/testdata/TestStringValidations/ip/v3.golden @@ -0,0 +1,5 @@ +export const IPSchema = z.object({ + Name: z.string().ip(), +}) +export type IP = z.infer + diff --git a/testdata/TestStringValidations/ip/v4.golden b/testdata/TestStringValidations/ip/v4.golden new file mode 100644 index 0000000..b9051ca --- /dev/null +++ b/testdata/TestStringValidations/ip/v4.golden @@ -0,0 +1,5 @@ +export const IPSchema = z.object({ + Name: z.union([z.ipv4(), z.ipv6()]), +}) +export type IP = z.infer + diff --git a/testdata/TestStringValidations/ip4_addr/v3.golden b/testdata/TestStringValidations/ip4_addr/v3.golden new file mode 100644 index 0000000..ef185b0 --- /dev/null +++ b/testdata/TestStringValidations/ip4_addr/v3.golden @@ -0,0 +1,5 @@ +export const IP4AddrSchema = z.object({ + Name: z.string().ip({ version: "v4" }), +}) +export type IP4Addr = z.infer + diff --git a/testdata/TestStringValidations/ip4_addr/v4.golden b/testdata/TestStringValidations/ip4_addr/v4.golden new file mode 100644 index 0000000..673b49f --- /dev/null +++ b/testdata/TestStringValidations/ip4_addr/v4.golden @@ -0,0 +1,5 @@ +export const IP4AddrSchema = z.object({ + Name: z.ipv4(), +}) +export type IP4Addr = z.infer + diff --git a/testdata/TestStringValidations/ip6_addr/v3.golden b/testdata/TestStringValidations/ip6_addr/v3.golden new file mode 100644 index 0000000..45c104b --- /dev/null +++ b/testdata/TestStringValidations/ip6_addr/v3.golden @@ -0,0 +1,5 @@ +export const IP6AddrSchema = z.object({ + Name: z.string().ip({ version: "v6" }), +}) +export type IP6Addr = z.infer + diff --git a/testdata/TestStringValidations/ip6_addr/v4.golden b/testdata/TestStringValidations/ip6_addr/v4.golden new file mode 100644 index 0000000..d028867 --- /dev/null +++ b/testdata/TestStringValidations/ip6_addr/v4.golden @@ -0,0 +1,5 @@ +export const IP6AddrSchema = z.object({ + Name: z.ipv6(), +}) +export type IP6Addr = z.infer + diff --git a/testdata/TestStringValidations/ip_addr/v3.golden b/testdata/TestStringValidations/ip_addr/v3.golden new file mode 100644 index 0000000..d9b309f --- /dev/null +++ b/testdata/TestStringValidations/ip_addr/v3.golden @@ -0,0 +1,5 @@ +export const IPAddrSchema = z.object({ + Name: z.string().ip(), +}) +export type IPAddr = z.infer + diff --git a/testdata/TestStringValidations/ip_addr/v4.golden b/testdata/TestStringValidations/ip_addr/v4.golden new file mode 100644 index 0000000..059527b --- /dev/null +++ b/testdata/TestStringValidations/ip_addr/v4.golden @@ -0,0 +1,5 @@ +export const IPAddrSchema = z.object({ + Name: z.union([z.ipv4(), z.ipv6()]), +}) +export type IPAddr = z.infer + diff --git a/testdata/TestStringValidations/ipv4/v3.golden b/testdata/TestStringValidations/ipv4/v3.golden new file mode 100644 index 0000000..d320aa5 --- /dev/null +++ b/testdata/TestStringValidations/ipv4/v3.golden @@ -0,0 +1,5 @@ +export const IPv4Schema = z.object({ + Name: z.string().ip({ version: "v4" }), +}) +export type IPv4 = z.infer + diff --git a/testdata/TestStringValidations/ipv4/v4.golden b/testdata/TestStringValidations/ipv4/v4.golden new file mode 100644 index 0000000..aa196c6 --- /dev/null +++ b/testdata/TestStringValidations/ipv4/v4.golden @@ -0,0 +1,5 @@ +export const IPv4Schema = z.object({ + Name: z.ipv4(), +}) +export type IPv4 = z.infer + diff --git a/testdata/TestStringValidations/ipv6/v3.golden b/testdata/TestStringValidations/ipv6/v3.golden new file mode 100644 index 0000000..a6fa053 --- /dev/null +++ b/testdata/TestStringValidations/ipv6/v3.golden @@ -0,0 +1,5 @@ +export const IPv6Schema = z.object({ + Name: z.string().ip({ version: "v6" }), +}) +export type IPv6 = z.infer + diff --git a/testdata/TestStringValidations/ipv6/v4.golden b/testdata/TestStringValidations/ipv6/v4.golden new file mode 100644 index 0000000..777754e --- /dev/null +++ b/testdata/TestStringValidations/ipv6/v4.golden @@ -0,0 +1,5 @@ +export const IPv6Schema = z.object({ + Name: z.ipv6(), +}) +export type IPv6 = z.infer + diff --git a/testdata/TestStringValidations/json.golden b/testdata/TestStringValidations/json.golden new file mode 100644 index 0000000..fabddff --- /dev/null +++ b/testdata/TestStringValidations/json.golden @@ -0,0 +1,5 @@ +export const jsonSchema = z.object({ + Name: z.string().refine((val) => { try { JSON.parse(val); return true } catch { return false } }), +}) +export type json = z.infer + diff --git a/testdata/TestStringValidations/latitude.golden b/testdata/TestStringValidations/latitude.golden new file mode 100644 index 0000000..c8ac3cb --- /dev/null +++ b/testdata/TestStringValidations/latitude.golden @@ -0,0 +1,5 @@ +export const LatitudeSchema = z.object({ + Name: z.string().regex(/^[-+]?([1-8]?\d(\.\d+)?|90(\.0+)?)$/), +}) +export type Latitude = z.infer + diff --git a/testdata/TestStringValidations/len.golden b/testdata/TestStringValidations/len.golden new file mode 100644 index 0000000..d2809dd --- /dev/null +++ b/testdata/TestStringValidations/len.golden @@ -0,0 +1,5 @@ +export const LenSchema = z.object({ + Name: z.string().refine((val) => [...val].length === 5, 'String must contain 5 character(s)'), +}) +export type Len = z.infer + diff --git a/testdata/TestStringValidations/longitude.golden b/testdata/TestStringValidations/longitude.golden new file mode 100644 index 0000000..7378179 --- /dev/null +++ b/testdata/TestStringValidations/longitude.golden @@ -0,0 +1,5 @@ +export const LongitudeSchema = z.object({ + Name: z.string().regex(/^[-+]?(180(\.0+)?|((1[0-7]\d)|([1-9]?\d))(\.\d+)?)$/), +}) +export type Longitude = z.infer + diff --git a/testdata/TestStringValidations/lowercase.golden b/testdata/TestStringValidations/lowercase.golden new file mode 100644 index 0000000..35afacb --- /dev/null +++ b/testdata/TestStringValidations/lowercase.golden @@ -0,0 +1,5 @@ +export const LowercaseSchema = z.object({ + Name: z.string().refine((val) => val === val.toLowerCase()), +}) +export type Lowercase = z.infer + diff --git a/testdata/TestStringValidations/lt.golden b/testdata/TestStringValidations/lt.golden new file mode 100644 index 0000000..9962f97 --- /dev/null +++ b/testdata/TestStringValidations/lt.golden @@ -0,0 +1,5 @@ +export const LtSchema = z.object({ + Name: z.string().refine((val) => [...val].length < 5, 'String must contain at most 4 character(s)'), +}) +export type Lt = z.infer + diff --git a/testdata/TestStringValidations/lte.golden b/testdata/TestStringValidations/lte.golden new file mode 100644 index 0000000..3bb2591 --- /dev/null +++ b/testdata/TestStringValidations/lte.golden @@ -0,0 +1,5 @@ +export const LteSchema = z.object({ + Name: z.string().refine((val) => [...val].length <= 5, 'String must contain at most 5 character(s)'), +}) +export type Lte = z.infer + diff --git a/testdata/TestStringValidations/max.golden b/testdata/TestStringValidations/max.golden new file mode 100644 index 0000000..531aa94 --- /dev/null +++ b/testdata/TestStringValidations/max.golden @@ -0,0 +1,5 @@ +export const MaxSchema = z.object({ + Name: z.string().refine((val) => [...val].length <= 5, 'String must contain at most 5 character(s)'), +}) +export type Max = z.infer + diff --git a/testdata/TestStringValidations/md4.golden b/testdata/TestStringValidations/md4.golden new file mode 100644 index 0000000..02218c5 --- /dev/null +++ b/testdata/TestStringValidations/md4.golden @@ -0,0 +1,5 @@ +export const MD4Schema = z.object({ + Name: z.string().regex(/^[0-9a-f]{32}$/), +}) +export type MD4 = z.infer + diff --git a/testdata/TestStringValidations/md5/v3.golden b/testdata/TestStringValidations/md5/v3.golden new file mode 100644 index 0000000..b222b8a --- /dev/null +++ b/testdata/TestStringValidations/md5/v3.golden @@ -0,0 +1,5 @@ +export const MD5Schema = z.object({ + Name: z.string().regex(/^[0-9a-f]{32}$/), +}) +export type MD5 = z.infer + diff --git a/testdata/TestStringValidations/md5/v4.golden b/testdata/TestStringValidations/md5/v4.golden new file mode 100644 index 0000000..916e966 --- /dev/null +++ b/testdata/TestStringValidations/md5/v4.golden @@ -0,0 +1,5 @@ +export const MD5Schema = z.object({ + Name: z.hash("md5"), +}) +export type MD5 = z.infer + diff --git a/testdata/TestStringValidations/min.golden b/testdata/TestStringValidations/min.golden new file mode 100644 index 0000000..97d4b2a --- /dev/null +++ b/testdata/TestStringValidations/min.golden @@ -0,0 +1,5 @@ +export const MinSchema = z.object({ + Name: z.string().refine((val) => [...val].length >= 5, 'String must contain at least 5 character(s)'), +}) +export type Min = z.infer + diff --git a/testdata/TestStringValidations/minmax.golden b/testdata/TestStringValidations/minmax.golden new file mode 100644 index 0000000..5997d5b --- /dev/null +++ b/testdata/TestStringValidations/minmax.golden @@ -0,0 +1,5 @@ +export const MinMaxSchema = z.object({ + 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 + diff --git a/testdata/TestStringValidations/mongodb.golden b/testdata/TestStringValidations/mongodb.golden new file mode 100644 index 0000000..4af106f --- /dev/null +++ b/testdata/TestStringValidations/mongodb.golden @@ -0,0 +1,5 @@ +export const mongodbSchema = z.object({ + Name: z.string().regex(/^[a-f\d]{24}$/), +}) +export type mongodb = z.infer + diff --git a/testdata/TestStringValidations/ne.golden b/testdata/TestStringValidations/ne.golden new file mode 100644 index 0000000..abf32f1 --- /dev/null +++ b/testdata/TestStringValidations/ne.golden @@ -0,0 +1,5 @@ +export const NeSchema = z.object({ + Name: z.string().refine((val) => val !== "hello"), +}) +export type Ne = z.infer + diff --git a/testdata/TestStringValidations/number.golden b/testdata/TestStringValidations/number.golden new file mode 100644 index 0000000..3a95f74 --- /dev/null +++ b/testdata/TestStringValidations/number.golden @@ -0,0 +1,5 @@ +export const NumberSchema = z.object({ + Name: z.string().regex(/^[0-9]+$/), +}) +export type Number = z.infer + diff --git a/testdata/TestStringValidations/numeric.golden b/testdata/TestStringValidations/numeric.golden new file mode 100644 index 0000000..afc9ae7 --- /dev/null +++ b/testdata/TestStringValidations/numeric.golden @@ -0,0 +1,5 @@ +export const NumericSchema = z.object({ + Name: z.string().regex(/^[-+]?[0-9]+(?:\.[0-9]+)?$/), +}) +export type Numeric = z.infer + diff --git a/testdata/TestStringValidations/oneof.golden b/testdata/TestStringValidations/oneof.golden new file mode 100644 index 0000000..41afee2 --- /dev/null +++ b/testdata/TestStringValidations/oneof.golden @@ -0,0 +1,5 @@ +export const OneOfSchema = z.object({ + Name: z.enum(["hello", "world"] as const), +}) +export type OneOf = z.infer + diff --git a/testdata/TestStringValidations/oneof_separated.golden b/testdata/TestStringValidations/oneof_separated.golden new file mode 100644 index 0000000..3af90a9 --- /dev/null +++ b/testdata/TestStringValidations/oneof_separated.golden @@ -0,0 +1,5 @@ +export const OneOfSeparatedSchema = z.object({ + Name: z.enum(["a b c", "d e f"] as const), +}) +export type OneOfSeparated = z.infer + diff --git a/testdata/TestStringValidations/required.golden b/testdata/TestStringValidations/required.golden new file mode 100644 index 0000000..fdd8771 --- /dev/null +++ b/testdata/TestStringValidations/required.golden @@ -0,0 +1,5 @@ +export const RequiredSchema = z.object({ + Name: z.string().min(1), +}) +export type Required = z.infer + diff --git a/testdata/TestStringValidations/sha256/v3.golden b/testdata/TestStringValidations/sha256/v3.golden new file mode 100644 index 0000000..aa129fc --- /dev/null +++ b/testdata/TestStringValidations/sha256/v3.golden @@ -0,0 +1,5 @@ +export const SHA256Schema = z.object({ + Name: z.string().regex(/^[0-9a-f]{64}$/), +}) +export type SHA256 = z.infer + diff --git a/testdata/TestStringValidations/sha256/v4.golden b/testdata/TestStringValidations/sha256/v4.golden new file mode 100644 index 0000000..f3f70d7 --- /dev/null +++ b/testdata/TestStringValidations/sha256/v4.golden @@ -0,0 +1,5 @@ +export const SHA256Schema = z.object({ + Name: z.hash("sha256"), +}) +export type SHA256 = z.infer + diff --git a/testdata/TestStringValidations/sha384/v3.golden b/testdata/TestStringValidations/sha384/v3.golden new file mode 100644 index 0000000..91385b9 --- /dev/null +++ b/testdata/TestStringValidations/sha384/v3.golden @@ -0,0 +1,5 @@ +export const SHA384Schema = z.object({ + Name: z.string().regex(/^[0-9a-f]{96}$/), +}) +export type SHA384 = z.infer + diff --git a/testdata/TestStringValidations/sha384/v4.golden b/testdata/TestStringValidations/sha384/v4.golden new file mode 100644 index 0000000..02639a3 --- /dev/null +++ b/testdata/TestStringValidations/sha384/v4.golden @@ -0,0 +1,5 @@ +export const SHA384Schema = z.object({ + Name: z.hash("sha384"), +}) +export type SHA384 = z.infer + diff --git a/testdata/TestStringValidations/sha512/v3.golden b/testdata/TestStringValidations/sha512/v3.golden new file mode 100644 index 0000000..f7ac7fd --- /dev/null +++ b/testdata/TestStringValidations/sha512/v3.golden @@ -0,0 +1,5 @@ +export const SHA512Schema = z.object({ + Name: z.string().regex(/^[0-9a-f]{128}$/), +}) +export type SHA512 = z.infer + diff --git a/testdata/TestStringValidations/sha512/v4.golden b/testdata/TestStringValidations/sha512/v4.golden new file mode 100644 index 0000000..77bc7c8 --- /dev/null +++ b/testdata/TestStringValidations/sha512/v4.golden @@ -0,0 +1,5 @@ +export const SHA512Schema = z.object({ + Name: z.hash("sha512"), +}) +export type SHA512 = z.infer + diff --git a/testdata/TestStringValidations/startswith.golden b/testdata/TestStringValidations/startswith.golden new file mode 100644 index 0000000..f00ffef --- /dev/null +++ b/testdata/TestStringValidations/startswith.golden @@ -0,0 +1,5 @@ +export const StartsWithSchema = z.object({ + Name: z.string().startsWith("hello"), +}) +export type StartsWith = z.infer + diff --git a/testdata/TestStringValidations/uppercase.golden b/testdata/TestStringValidations/uppercase.golden new file mode 100644 index 0000000..fceefcc --- /dev/null +++ b/testdata/TestStringValidations/uppercase.golden @@ -0,0 +1,5 @@ +export const UppercaseSchema = z.object({ + Name: z.string().refine((val) => val === val.toUpperCase()), +}) +export type Uppercase = z.infer + diff --git a/testdata/TestStringValidations/url/v3.golden b/testdata/TestStringValidations/url/v3.golden new file mode 100644 index 0000000..51c00fe --- /dev/null +++ b/testdata/TestStringValidations/url/v3.golden @@ -0,0 +1,5 @@ +export const URLSchema = z.object({ + Name: z.string().url(), +}) +export type URL = z.infer + diff --git a/testdata/TestStringValidations/url/v4.golden b/testdata/TestStringValidations/url/v4.golden new file mode 100644 index 0000000..00988f0 --- /dev/null +++ b/testdata/TestStringValidations/url/v4.golden @@ -0,0 +1,5 @@ +export const URLSchema = z.object({ + Name: z.url(), +}) +export type URL = z.infer + diff --git a/testdata/TestStringValidations/url_encoded.golden b/testdata/TestStringValidations/url_encoded.golden new file mode 100644 index 0000000..180542f --- /dev/null +++ b/testdata/TestStringValidations/url_encoded.golden @@ -0,0 +1,5 @@ +export const URLEncodedSchema = z.object({ + Name: z.string().regex(/^(?:[^%]|%[0-9A-Fa-f]{2})*$/), +}) +export type URLEncoded = z.infer + diff --git a/testdata/TestStringValidations/uuid/v3.golden b/testdata/TestStringValidations/uuid/v3.golden new file mode 100644 index 0000000..1999396 --- /dev/null +++ b/testdata/TestStringValidations/uuid/v3.golden @@ -0,0 +1,5 @@ +export const UUIDSchema = z.object({ + Name: z.string().regex(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/), +}) +export type UUID = z.infer + diff --git a/testdata/TestStringValidations/uuid/v4.golden b/testdata/TestStringValidations/uuid/v4.golden new file mode 100644 index 0000000..8f1f3a6 --- /dev/null +++ b/testdata/TestStringValidations/uuid/v4.golden @@ -0,0 +1,5 @@ +export const UUIDSchema = z.object({ + Name: z.uuid(), +}) +export type UUID = z.infer + diff --git a/testdata/TestStringValidations/uuid3/v3.golden b/testdata/TestStringValidations/uuid3/v3.golden new file mode 100644 index 0000000..00931f3 --- /dev/null +++ b/testdata/TestStringValidations/uuid3/v3.golden @@ -0,0 +1,5 @@ +export const UUID3Schema = z.object({ + Name: z.string().regex(/^[0-9a-f]{8}-[0-9a-f]{4}-3[0-9a-f]{3}-[0-9a-f]{4}-[0-9a-f]{12}$/), +}) +export type UUID3 = z.infer + diff --git a/testdata/TestStringValidations/uuid3/v4.golden b/testdata/TestStringValidations/uuid3/v4.golden new file mode 100644 index 0000000..9711673 --- /dev/null +++ b/testdata/TestStringValidations/uuid3/v4.golden @@ -0,0 +1,5 @@ +export const UUID3Schema = z.object({ + Name: z.uuid({ version: "v3" }), +}) +export type UUID3 = z.infer + diff --git a/testdata/TestStringValidations/uuid3_rfc4122/v3.golden b/testdata/TestStringValidations/uuid3_rfc4122/v3.golden new file mode 100644 index 0000000..49f6d8c --- /dev/null +++ b/testdata/TestStringValidations/uuid3_rfc4122/v3.golden @@ -0,0 +1,5 @@ +export const UUID3RFC4122Schema = z.object({ + Name: z.string().regex(/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-3[0-9a-fA-F]{3}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/), +}) +export type UUID3RFC4122 = z.infer + diff --git a/testdata/TestStringValidations/uuid3_rfc4122/v4.golden b/testdata/TestStringValidations/uuid3_rfc4122/v4.golden new file mode 100644 index 0000000..b48d645 --- /dev/null +++ b/testdata/TestStringValidations/uuid3_rfc4122/v4.golden @@ -0,0 +1,5 @@ +export const UUID3RFC4122Schema = z.object({ + Name: z.uuid({ version: "v3" }), +}) +export type UUID3RFC4122 = z.infer + diff --git a/testdata/TestStringValidations/uuid4/v3.golden b/testdata/TestStringValidations/uuid4/v3.golden new file mode 100644 index 0000000..5002a65 --- /dev/null +++ b/testdata/TestStringValidations/uuid4/v3.golden @@ -0,0 +1,5 @@ +export const UUID4Schema = z.object({ + Name: z.string().regex(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/), +}) +export type UUID4 = z.infer + diff --git a/testdata/TestStringValidations/uuid4/v4.golden b/testdata/TestStringValidations/uuid4/v4.golden new file mode 100644 index 0000000..4e56638 --- /dev/null +++ b/testdata/TestStringValidations/uuid4/v4.golden @@ -0,0 +1,5 @@ +export const UUID4Schema = z.object({ + Name: z.uuid({ version: "v4" }), +}) +export type UUID4 = z.infer + diff --git a/testdata/TestStringValidations/uuid4_rfc4122/v3.golden b/testdata/TestStringValidations/uuid4_rfc4122/v3.golden new file mode 100644 index 0000000..d025b7b --- /dev/null +++ b/testdata/TestStringValidations/uuid4_rfc4122/v3.golden @@ -0,0 +1,5 @@ +export const UUID4RFC4122Schema = z.object({ + Name: z.string().regex(/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$/), +}) +export type UUID4RFC4122 = z.infer + diff --git a/testdata/TestStringValidations/uuid4_rfc4122/v4.golden b/testdata/TestStringValidations/uuid4_rfc4122/v4.golden new file mode 100644 index 0000000..24f7f24 --- /dev/null +++ b/testdata/TestStringValidations/uuid4_rfc4122/v4.golden @@ -0,0 +1,5 @@ +export const UUID4RFC4122Schema = z.object({ + Name: z.uuid({ version: "v4" }), +}) +export type UUID4RFC4122 = z.infer + diff --git a/testdata/TestStringValidations/uuid5/v3.golden b/testdata/TestStringValidations/uuid5/v3.golden new file mode 100644 index 0000000..669b090 --- /dev/null +++ b/testdata/TestStringValidations/uuid5/v3.golden @@ -0,0 +1,5 @@ +export const UUID5Schema = z.object({ + Name: z.string().regex(/^[0-9a-f]{8}-[0-9a-f]{4}-5[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/), +}) +export type UUID5 = z.infer + diff --git a/testdata/TestStringValidations/uuid5/v4.golden b/testdata/TestStringValidations/uuid5/v4.golden new file mode 100644 index 0000000..eb4ca18 --- /dev/null +++ b/testdata/TestStringValidations/uuid5/v4.golden @@ -0,0 +1,5 @@ +export const UUID5Schema = z.object({ + Name: z.uuid({ version: "v5" }), +}) +export type UUID5 = z.infer + diff --git a/testdata/TestStringValidations/uuid5_rfc4122/v3.golden b/testdata/TestStringValidations/uuid5_rfc4122/v3.golden new file mode 100644 index 0000000..b35a6aa --- /dev/null +++ b/testdata/TestStringValidations/uuid5_rfc4122/v3.golden @@ -0,0 +1,5 @@ +export const UUID5RFC4122Schema = z.object({ + Name: z.string().regex(/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-5[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$/), +}) +export type UUID5RFC4122 = z.infer + diff --git a/testdata/TestStringValidations/uuid5_rfc4122/v4.golden b/testdata/TestStringValidations/uuid5_rfc4122/v4.golden new file mode 100644 index 0000000..32d8b44 --- /dev/null +++ b/testdata/TestStringValidations/uuid5_rfc4122/v4.golden @@ -0,0 +1,5 @@ +export const UUID5RFC4122Schema = z.object({ + Name: z.uuid({ version: "v5" }), +}) +export type UUID5RFC4122 = z.infer + diff --git a/testdata/TestStringValidations/uuid_rfc4122/v3.golden b/testdata/TestStringValidations/uuid_rfc4122/v3.golden new file mode 100644 index 0000000..9c98a2f --- /dev/null +++ b/testdata/TestStringValidations/uuid_rfc4122/v3.golden @@ -0,0 +1,5 @@ +export const UUIDRFC4122Schema = z.object({ + Name: z.string().regex(/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/), +}) +export type UUIDRFC4122 = z.infer + diff --git a/testdata/TestStringValidations/uuid_rfc4122/v4.golden b/testdata/TestStringValidations/uuid_rfc4122/v4.golden new file mode 100644 index 0000000..c392b05 --- /dev/null +++ b/testdata/TestStringValidations/uuid_rfc4122/v4.golden @@ -0,0 +1,5 @@ +export const UUIDRFC4122Schema = z.object({ + Name: z.uuid(), +}) +export type UUIDRFC4122 = z.infer + diff --git a/testdata/TestStructSimple.golden b/testdata/TestStructSimple.golden new file mode 100644 index 0000000..729e518 --- /dev/null +++ b/testdata/TestStructSimple.golden @@ -0,0 +1,7 @@ +export const UserSchema = z.object({ + Name: z.string(), + Age: z.number(), + Height: z.number(), +}) +export type User = z.infer + diff --git a/testdata/TestStructSimplePrefix.golden b/testdata/TestStructSimplePrefix.golden new file mode 100644 index 0000000..917ff7d --- /dev/null +++ b/testdata/TestStructSimplePrefix.golden @@ -0,0 +1,7 @@ +export const BotUserSchema = z.object({ + Name: z.string(), + Age: z.number(), + Height: z.number(), +}) +export type BotUser = z.infer + diff --git a/testdata/TestStructSimpleWithOmittedField.golden b/testdata/TestStructSimpleWithOmittedField.golden new file mode 100644 index 0000000..729e518 --- /dev/null +++ b/testdata/TestStructSimpleWithOmittedField.golden @@ -0,0 +1,7 @@ +export const UserSchema = z.object({ + Name: z.string(), + Age: z.number(), + Height: z.number(), +}) +export type User = z.infer + diff --git a/testdata/TestStructSlice.golden b/testdata/TestStructSlice.golden new file mode 100644 index 0000000..7907668 --- /dev/null +++ b/testdata/TestStructSlice.golden @@ -0,0 +1,7 @@ +export const UserSchema = z.object({ + Favourites: z.object({ + Name: z.string(), + }).array().nullable(), +}) +export type User = z.infer + diff --git a/testdata/TestStructSliceOptional.golden b/testdata/TestStructSliceOptional.golden new file mode 100644 index 0000000..822690b --- /dev/null +++ b/testdata/TestStructSliceOptional.golden @@ -0,0 +1,7 @@ +export const UserSchema = z.object({ + Favourites: z.object({ + Name: z.string(), + }).array().optional(), +}) +export type User = z.infer + diff --git a/testdata/TestStructSliceOptionalNullable.golden b/testdata/TestStructSliceOptionalNullable.golden new file mode 100644 index 0000000..87cb553 --- /dev/null +++ b/testdata/TestStructSliceOptionalNullable.golden @@ -0,0 +1,7 @@ +export const UserSchema = z.object({ + Favourites: z.object({ + Name: z.string(), + }).array().optional().nullable(), +}) +export type User = z.infer + diff --git a/testdata/TestStructTime.golden b/testdata/TestStructTime.golden new file mode 100644 index 0000000..5a5940e --- /dev/null +++ b/testdata/TestStructTime.golden @@ -0,0 +1,6 @@ +export const UserSchema = z.object({ + Name: z.string(), + When: z.coerce.date(), +}) +export type User = z.infer + diff --git a/testdata/TestTimeWithRequired.golden b/testdata/TestTimeWithRequired.golden new file mode 100644 index 0000000..ef99490 --- /dev/null +++ b/testdata/TestTimeWithRequired.golden @@ -0,0 +1,5 @@ +export const UserSchema = z.object({ + When: z.coerce.date().refine((val) => val.getTime() !== new Date('0001-01-01T00:00:00Z').getTime() && val.getTime() !== new Date(0).getTime(), 'Invalid date'), +}) +export type User = z.infer + diff --git a/testdata/TestZodV4Defaults/embedded_structs_use_shape_spreads.golden b/testdata/TestZodV4Defaults/embedded_structs_use_shape_spreads.golden new file mode 100644 index 0000000..30a7b0e --- /dev/null +++ b/testdata/TestZodV4Defaults/embedded_structs_use_shape_spreads.golden @@ -0,0 +1,17 @@ +export const HasIDSchema = z.object({ + ID: z.string(), +}) +export type HasID = z.infer + +export const HasNameSchema = z.object({ + name: z.string(), +}) +export type HasName = z.infer + +export const UserSchema = z.object({ + Tags: z.string().array().nullable(), + ...HasIDSchema.shape, + ...HasNameSchema.shape, +}) +export type User = z.infer + diff --git a/testdata/TestZodV4Defaults/enum_keyed_maps_become_partial_records.golden b/testdata/TestZodV4Defaults/enum_keyed_maps_become_partial_records.golden new file mode 100644 index 0000000..b4e0ff8 --- /dev/null +++ b/testdata/TestZodV4Defaults/enum_keyed_maps_become_partial_records.golden @@ -0,0 +1,5 @@ +export const PayloadSchema = z.object({ + Metadata: z.partialRecord(z.enum(["draft", "published"] as const), z.string()).nullable(), +}) +export type Payload = z.infer + diff --git a/testdata/TestZodV4Defaults/ip_mixed_with_another_format_falls_back_to_legacy_chain_semantics.golden b/testdata/TestZodV4Defaults/ip_mixed_with_another_format_falls_back_to_legacy_chain_semantics.golden new file mode 100644 index 0000000..cd33cb8 --- /dev/null +++ b/testdata/TestZodV4Defaults/ip_mixed_with_another_format_falls_back_to_legacy_chain_semantics.golden @@ -0,0 +1,5 @@ +export const PayloadSchema = z.object({ + Address: z.string().email().ip(), +}) +export type Payload = z.infer + diff --git a/testdata/TestZodV4Defaults/ip_unions_inherit_generic_string_constraints.golden b/testdata/TestZodV4Defaults/ip_unions_inherit_generic_string_constraints.golden new file mode 100644 index 0000000..542f6a9 --- /dev/null +++ b/testdata/TestZodV4Defaults/ip_unions_inherit_generic_string_constraints.golden @@ -0,0 +1,5 @@ +export const PayloadSchema = z.object({ + Address: z.union([z.ipv4().min(1).refine((val) => [...val].length <= 45, 'String must contain at most 45 character(s)'), z.ipv6().min(1).refine((val) => [...val].length <= 45, 'String must contain at most 45 character(s)')]), +}) +export type Payload = z.infer + diff --git a/testdata/TestZodV4Defaults/oneof_takes_precedence_over_ip_specialization.golden b/testdata/TestZodV4Defaults/oneof_takes_precedence_over_ip_specialization.golden new file mode 100644 index 0000000..98f2ccd --- /dev/null +++ b/testdata/TestZodV4Defaults/oneof_takes_precedence_over_ip_specialization.golden @@ -0,0 +1,5 @@ +export const PayloadSchema = z.object({ + Address: z.enum(["127.0.0.1", "::1"] as const), +}) +export type Payload = z.infer + diff --git a/testdata/TestZodV4Defaults/recursive_embedded_shapes_keep_named_fields_before_spreads.golden b/testdata/TestZodV4Defaults/recursive_embedded_shapes_keep_named_fields_before_spreads.golden new file mode 100644 index 0000000..afead2e --- /dev/null +++ b/testdata/TestZodV4Defaults/recursive_embedded_shapes_keep_named_fields_before_spreads.golden @@ -0,0 +1,18 @@ +export type TreeNode = { + Value: string, + CreatedAt: Date, + Children: TreeNode[] | null, +} +const TreeNodeSchemaShape = { + Value: z.string(), + CreatedAt: z.coerce.date(), + get Children() { return TreeNodeSchema.array().nullable(); }, +} +export const TreeNodeSchema: z.ZodType = z.object(TreeNodeSchemaShape) + +export const TreeSchema = z.object({ + UpdatedAt: z.coerce.date(), + ...TreeNodeSchemaShape, +}) +export type Tree = z.infer + diff --git a/testdata/TestZodV4Defaults/recursive_embedded_shapes_preserve_encounter_order_for_duplicate_keys.golden b/testdata/TestZodV4Defaults/recursive_embedded_shapes_preserve_encounter_order_for_duplicate_keys.golden new file mode 100644 index 0000000..f4095fd --- /dev/null +++ b/testdata/TestZodV4Defaults/recursive_embedded_shapes_preserve_encounter_order_for_duplicate_keys.golden @@ -0,0 +1,16 @@ +export const BaseSchema = z.object({ + id: z.string(), +}) +export type Base = z.infer + +export type Node = Base & { + id: number, + next: Node | null, +} +const NodeSchemaShape = { + ...BaseSchema.shape, + id: z.number(), + get next() { return NodeSchema.nullable(); }, +} +export const NodeSchema: z.ZodType = z.object(NodeSchemaShape) + diff --git a/testdata/TestZodV4Defaults/string_formats_use_zod_v4_builders.golden b/testdata/TestZodV4Defaults/string_formats_use_zod_v4_builders.golden new file mode 100644 index 0000000..d1c7c53 --- /dev/null +++ b/testdata/TestZodV4Defaults/string_formats_use_zod_v4_builders.golden @@ -0,0 +1,9 @@ +export const PayloadSchema = z.object({ + Email: z.email(), + Link: z.httpUrl(), + Base64: z.base64(), + ID: z.uuid({ version: "v4" }), + Checksum: z.hash("md5"), +}) +export type Payload = z.infer + diff --git a/testdata/TestZodV4Defaults/string_tag_order_is_preserved_around_v4_format_helpers.golden b/testdata/TestZodV4Defaults/string_tag_order_is_preserved_around_v4_format_helpers.golden new file mode 100644 index 0000000..cd576a8 --- /dev/null +++ b/testdata/TestZodV4Defaults/string_tag_order_is_preserved_around_v4_format_helpers.golden @@ -0,0 +1,6 @@ +export const PayloadSchema = z.object({ + TrimmedThenEmail: z.string().trim().email(), + EmailThenTrimmed: z.email().trim(), +}) +export type Payload = z.infer + diff --git a/zod.go b/zod.go index a0c7aae..cf239c7 100644 --- a/zod.go +++ b/zod.go @@ -185,10 +185,11 @@ type Converter struct { customTypes map[string]CustomFn customTags map[string]CustomFn ignoreTags []string - zodV3 bool - structs int - outputs map[string]entry - stack []meta + zodV3 bool + lastFieldSelfRef bool + structs int + outputs map[string]entry + stack []meta } func (c *Converter) addSchema(name string, data string, selfRef bool) { @@ -480,7 +481,12 @@ func (c *Converter) ConvertType(t reflect.Type, validate string, indent int) str } else { if c.stack[len(c.stack)-1].name == name { c.stack[len(c.stack)-1].selfRef = true - validateStr.WriteString(fmt.Sprintf("z.lazy(() => %s)", schemaName(c.prefix, name))) + if c.zodV3 { + validateStr.WriteString(fmt.Sprintf("z.lazy(() => %s)", schemaName(c.prefix, name))) + } else { + c.lastFieldSelfRef = true + validateStr.WriteString(schemaName(c.prefix, name)) + } } else { // throws panic if there is a cycle detectCycle(name, c.stack) @@ -602,6 +608,19 @@ func (c *Converter) convertNamedField(f reflect.StructField, indent int, optiona } t := c.ConvertType(f.Type, f.Tag.Get("validate"), indent) + isSelfRef := c.lastFieldSelfRef + c.lastFieldSelfRef = false + + if isSelfRef && !c.zodV3 { + return fmt.Sprintf( + "%sget %s() { return %s%s%s; },\n", + indentation(indent), + name, + t, + optionalCall, + nullableCall) + } + return fmt.Sprintf( "%s%s: %s%s%s,\n", indentation(indent), diff --git a/zod_test.go b/zod_test.go index cdc85ba..ad91eb3 100644 --- a/zod_test.go +++ b/zod_test.go @@ -8,8 +8,46 @@ import ( "time" "github.com/stretchr/testify/assert" + "github.com/xorcare/golden" ) +// assertSchema is a golden file test helper for Zod schema output. +// +// When no versions are specified, it asserts that v3 and v4 produce identical +// output and golden-tests that output once. +// +// When one version is specified ("v3" or "v4"), it golden-tests that version's +// output directly without a subtest. +// +// When multiple versions are specified, it creates a subtest per version and +// golden-tests each independently. +func assertSchema(t *testing.T, schema any, versions ...string) { + t.Helper() + + optsFor := func(ver string) []Opt { + if ver == "v3" { + return []Opt{WithZodV3()} + } + return nil + } + + switch len(versions) { + case 0: + v3out := StructToZodSchema(schema, WithZodV3()) + v4out := StructToZodSchema(schema) + assert.Equal(t, v3out, v4out) + golden.Assert(t, []byte(v4out)) + case 1: + golden.Assert(t, []byte(StructToZodSchema(schema, optsFor(versions[0])...))) + default: + for _, ver := range versions { + t.Run(ver, func(t *testing.T) { + golden.Assert(t, []byte(StructToZodSchema(schema, optsFor(ver)...))) + }) + } + } +} + func TestFieldName(t *testing.T) { assert.Equal(t, fieldName(reflect.StructField{Name: "RCONPassword"}), @@ -66,16 +104,7 @@ func TestStructSimple(t *testing.T) { Age int Height float64 } - assert.Equal(t, - `export const UserSchema = z.object({ - Name: z.string(), - Age: z.number(), - Height: z.number(), -}) -export type User = z.infer - -`, - StructToZodSchema(User{})) + assertSchema(t, User{}) } func TestStructSimpleWithOmittedField(t *testing.T) { @@ -85,16 +114,7 @@ func TestStructSimpleWithOmittedField(t *testing.T) { Height float64 NotExported string `json:"-"` } - assert.Equal(t, - `export const UserSchema = z.object({ - Name: z.string(), - Age: z.number(), - Height: z.number(), -}) -export type User = z.infer - -`, - StructToZodSchema(User{}, WithZodV3())) + assertSchema(t, User{}) } func TestStructSimplePrefix(t *testing.T) { @@ -103,16 +123,10 @@ func TestStructSimplePrefix(t *testing.T) { Age int Height float64 } - assert.Equal(t, - `export const BotUserSchema = z.object({ - Name: z.string(), - Age: z.number(), - Height: z.number(), -}) -export type BotUser = z.infer - -`, - StructToZodSchema(User{}, WithPrefix("Bot"))) + v3out := StructToZodSchema(User{}, WithPrefix("Bot"), WithZodV3()) + v4out := StructToZodSchema(User{}, WithPrefix("Bot")) + assert.Equal(t, v3out, v4out) + golden.Assert(t, []byte(v4out)) } func TestNestedStruct(t *testing.T) { @@ -127,38 +141,14 @@ func TestNestedStruct(t *testing.T) { HasName Tags []string } - assert.Equal(t, - `export const HasIDSchema = z.object({ - ID: z.string(), -}) -export type HasID = z.infer - -export const HasNameSchema = z.object({ - name: z.string(), -}) -export type HasName = z.infer - -export const UserSchema = z.object({ - Tags: z.string().array().nullable(), -}).merge(HasIDSchema).merge(HasNameSchema) -export type User = z.infer - -`, - StructToZodSchema(User{}, WithZodV3())) + assertSchema(t, User{}, "v3", "v4") } func TestStringArray(t *testing.T) { type User struct { Tags []string } - assert.Equal(t, - `export const UserSchema = z.object({ - Tags: z.string().array().nullable(), -}) -export type User = z.infer - -`, - StructToZodSchema(User{})) + assertSchema(t, User{}) } func TestStringNestedArray(t *testing.T) { @@ -166,14 +156,7 @@ func TestStringNestedArray(t *testing.T) { type User struct { TagPairs []TagPair } - assert.Equal(t, - `export const UserSchema = z.object({ - TagPairs: z.string().array().length(2).array().nullable(), -}) -export type User = z.infer - -`, - StructToZodSchema(User{})) + assertSchema(t, User{}) } func TestStructSlice(t *testing.T) { @@ -182,16 +165,7 @@ func TestStructSlice(t *testing.T) { Name string } } - assert.Equal(t, - `export const UserSchema = z.object({ - Favourites: z.object({ - Name: z.string(), - }).array().nullable(), -}) -export type User = z.infer - -`, - StructToZodSchema(User{})) + assertSchema(t, User{}) } func TestStructSliceOptional(t *testing.T) { @@ -200,16 +174,7 @@ func TestStructSliceOptional(t *testing.T) { Name string } `json:",omitempty"` } - assert.Equal(t, - `export const UserSchema = z.object({ - Favourites: z.object({ - Name: z.string(), - }).array().optional(), -}) -export type User = z.infer - -`, - StructToZodSchema(User{})) + assertSchema(t, User{}) } func TestStructSliceOptionalNullable(t *testing.T) { @@ -218,16 +183,7 @@ func TestStructSliceOptionalNullable(t *testing.T) { Name string } `json:",omitempty"` } - assert.Equal(t, - `export const UserSchema = z.object({ - Favourites: z.object({ - Name: z.string(), - }).array().optional().nullable(), -}) -export type User = z.infer - -`, - StructToZodSchema(User{})) + assertSchema(t, User{}) } func TestStringOptional(t *testing.T) { @@ -235,15 +191,7 @@ func TestStringOptional(t *testing.T) { Name string Nickname string `json:",omitempty"` } - assert.Equal(t, - `export const UserSchema = z.object({ - Name: z.string(), - Nickname: z.string().optional(), -}) -export type User = z.infer - -`, - StructToZodSchema(User{})) + assertSchema(t, User{}) } func TestStringNullable(t *testing.T) { @@ -251,15 +199,7 @@ func TestStringNullable(t *testing.T) { Name string Nickname *string } - assert.Equal(t, - `export const UserSchema = z.object({ - Name: z.string(), - Nickname: z.string().nullable(), -}) -export type User = z.infer - -`, - StructToZodSchema(User{})) + assertSchema(t, User{}) } func TestStringOptionalNotNullable(t *testing.T) { @@ -267,15 +207,7 @@ func TestStringOptionalNotNullable(t *testing.T) { Name string Nickname *string `json:",omitempty"` // nil values are omitted } - assert.Equal(t, - `export const UserSchema = z.object({ - Name: z.string(), - Nickname: z.string().optional(), -}) -export type User = z.infer - -`, - StructToZodSchema(User{})) + assertSchema(t, User{}) } func TestStringOptionalNullable(t *testing.T) { @@ -283,15 +215,7 @@ func TestStringOptionalNullable(t *testing.T) { Name string Nickname **string `json:",omitempty"` // nil values are omitted } - assert.Equal(t, - `export const UserSchema = z.object({ - Name: z.string(), - Nickname: z.string().optional().nullable(), -}) -export type User = z.infer - -`, - StructToZodSchema(User{})) + assertSchema(t, User{}) } func TestStringArrayNullable(t *testing.T) { @@ -299,15 +223,7 @@ func TestStringArrayNullable(t *testing.T) { Name string Tags []*string } - assert.Equal(t, - `export const UserSchema = z.object({ - Name: z.string(), - Tags: z.string().array().nullable(), -}) -export type User = z.infer - -`, - StructToZodSchema(User{})) + assertSchema(t, User{}) } func TestNullableWithValidations(t *testing.T) { @@ -365,759 +281,418 @@ func TestNullableWithValidations(t *testing.T) { // StringNullable2 string `json:",omitempty" validate:"omitempty,min=2,max=5"` } - assert.Equal(t, - `export const UserSchema = z.object({ - Name: z.string().min(1), - PtrMapOptionalNullable1: z.record(z.string(), z.any()).optional().nullable(), - PtrMapOptionalNullable2: z.record(z.string(), z.any()).refine((val) => Object.keys(val).length >= 2, 'Map too small').refine((val) => Object.keys(val).length <= 5, 'Map too large').optional().nullable(), - PtrMap1: z.record(z.string(), z.any()).refine((val) => Object.keys(val).length >= 2, 'Map too small').refine((val) => Object.keys(val).length <= 5, 'Map too large'), - PtrMap2: z.record(z.string(), z.any()).refine((val) => Object.keys(val).length >= 2, 'Map too small').refine((val) => Object.keys(val).length <= 5, 'Map too large'), - PtrMapNullable: z.record(z.string(), z.any()).refine((val) => Object.keys(val).length >= 2, 'Map too small').refine((val) => Object.keys(val).length <= 5, 'Map too large').nullable(), - MapOptional1: z.record(z.string(), z.any()).optional(), - MapOptional2: z.record(z.string(), z.any()).refine((val) => Object.keys(val).length >= 2, 'Map too small').refine((val) => Object.keys(val).length <= 5, 'Map too large').optional(), - Map1: z.record(z.string(), z.any()).refine((val) => Object.keys(val).length >= 2, 'Map too small').refine((val) => Object.keys(val).length <= 5, 'Map too large'), - Map2: z.record(z.string(), z.any()).refine((val) => Object.keys(val).length >= 2, 'Map too small').refine((val) => Object.keys(val).length <= 5, 'Map too large'), - MapNullable: z.record(z.string(), z.any()).refine((val) => Object.keys(val).length >= 2, 'Map too small').refine((val) => Object.keys(val).length <= 5, 'Map too large').nullable(), - PtrSliceOptionalNullable1: z.string().array().optional().nullable(), - PtrSliceOptionalNullable2: z.string().array().min(2).max(5).optional().nullable(), - PtrSlice1: z.string().array().min(2).max(5), - PtrSlice2: z.string().array().min(2).max(5), - PtrSliceNullable: z.string().array().min(2).max(5).nullable(), - SliceOptional1: z.string().array().optional(), - SliceOptional2: z.string().array().min(2).max(5).optional(), - Slice1: z.string().array().min(2).max(5), - Slice2: z.string().array().min(2).max(5), - SliceNullable: z.string().array().min(2).max(5).nullable(), - PtrIntOptional1: z.number().optional(), - PtrIntOptional2: z.number().gte(2).lte(5).optional(), - PtrInt1: z.number().gte(2).lte(5), - PtrInt2: z.number().gte(2).lte(5), - PtrIntNullable: z.number().gte(2).lte(5).nullable(), - PtrStringOptional1: z.string().optional(), - 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 - -`, - StructToZodSchema(User{})) + assertSchema(t, User{}) } func TestStringValidations(t *testing.T) { - type Eq struct { - Name string `validate:"eq=hello"` - } - assert.Equal(t, - `export const EqSchema = z.object({ - Name: z.string().refine((val) => val === "hello"), -}) -export type Eq = z.infer - -`, - StructToZodSchema(Eq{})) - - type Ne struct { - Name string `validate:"ne=hello"` - } - assert.Equal(t, - `export const NeSchema = z.object({ - Name: z.string().refine((val) => val !== "hello"), -}) -export type Ne = z.infer - -`, - StructToZodSchema(Ne{})) - - type OneOf struct { - Name string `validate:"oneof=hello world"` - } - assert.Equal(t, - `export const OneOfSchema = z.object({ - Name: z.enum(["hello", "world"] as const), -}) -export type OneOf = z.infer - -`, - StructToZodSchema(OneOf{})) - - type OneOfSeparated struct { - Name string `validate:"oneof='a b c' 'd e f'"` - } - assert.Equal(t, - `export const OneOfSeparatedSchema = z.object({ - Name: z.enum(["a b c", "d e f"] as const), -}) -export type OneOfSeparated = z.infer - -`, - StructToZodSchema(OneOfSeparated{})) - - // TODO: This test case is not supported yet even for the go-validator package whose logic - // I stole to parse the value after oneof=. - // - // type OneOfEscaped struct { - // Name string `validate:"oneof='a b c' 'd e f' 'g\\' h'"` - // } - // assert.Equal(t, - // `export const OneOfEscapedSchema = z.object({ - // Name: z.string().enum(["a b c", "d e f", "g' h"]), - //}) - //export type OneOfEscaped = z.infer - // - //`, - // StructToZodSchema(OneOfEscaped{})) - - // Same story as above. - // type OneOfEscaped2 struct { - // Name string `validate:"oneof='a b c' 'd e f' 'g\x27 h'"` - // } - // assert.Equal(t, - // `export const OneOfEscapedSchema = z.object({ - // Name: z.string().enum(["a b c", "d e f", "g' h"]), - //}) - //export type OneOfEscaped = z.infer - // - //`, - // StructToZodSchema(OneOfEscaped2{})) - - type Len struct { - Name string `validate:"len=5"` - } - assert.Equal(t, - `export const LenSchema = z.object({ - Name: z.string().refine((val) => [...val].length === 5, 'String must contain 5 character(s)'), -}) -export type Len = z.infer - -`, - StructToZodSchema(Len{})) - - type Min struct { - Name string `validate:"min=5"` - } - assert.Equal(t, - `export const MinSchema = z.object({ - Name: z.string().refine((val) => [...val].length >= 5, 'String must contain at least 5 character(s)'), -}) -export type Min = z.infer - -`, - StructToZodSchema(Min{})) - - type Max struct { - Name string `validate:"max=5"` - } - assert.Equal(t, - `export const MaxSchema = z.object({ - Name: z.string().refine((val) => [...val].length <= 5, 'String must contain at most 5 character(s)'), -}) -export type Max = z.infer - -`, - StructToZodSchema(Max{})) - - type MinMax struct { - Name string `validate:"min=3,max=7"` - } - assert.Equal(t, - `export const MinMaxSchema = z.object({ - 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 - -`, - StructToZodSchema(MinMax{})) - - type Gt struct { - Name string `validate:"gt=5"` - } - assert.Equal(t, - `export const GtSchema = z.object({ - Name: z.string().refine((val) => [...val].length > 5, 'String must contain at least 6 character(s)'), -}) -export type Gt = z.infer - -`, - StructToZodSchema(Gt{})) - - type Gte struct { - Name string `validate:"gte=5"` - } - assert.Equal(t, - `export const GteSchema = z.object({ - Name: z.string().refine((val) => [...val].length >= 5, 'String must contain at least 5 character(s)'), -}) -export type Gte = z.infer - -`, - StructToZodSchema(Gte{})) - - type Lt struct { - Name string `validate:"lt=5"` - } - assert.Equal(t, - `export const LtSchema = z.object({ - Name: z.string().refine((val) => [...val].length < 5, 'String must contain at most 4 character(s)'), -}) -export type Lt = z.infer - -`, - StructToZodSchema(Lt{})) - - type Lte struct { - Name string `validate:"lte=5"` - } - assert.Equal(t, - `export const LteSchema = z.object({ - Name: z.string().refine((val) => [...val].length <= 5, 'String must contain at most 5 character(s)'), -}) -export type Lte = z.infer - -`, - StructToZodSchema(Lte{})) - - type Contains struct { - Name string `validate:"contains=hello"` - } - assert.Equal(t, - `export const ContainsSchema = z.object({ - Name: z.string().includes("hello"), -}) -export type Contains = z.infer - -`, - StructToZodSchema(Contains{})) - - type EndsWith struct { - Name string `validate:"endswith=hello"` - } - assert.Equal(t, - `export const EndsWithSchema = z.object({ - Name: z.string().endsWith("hello"), -}) -export type EndsWith = z.infer - -`, - StructToZodSchema(EndsWith{})) - - type StartsWith struct { - Name string `validate:"startswith=hello"` - } - assert.Equal(t, - `export const StartsWithSchema = z.object({ - Name: z.string().startsWith("hello"), -}) -export type StartsWith = z.infer - -`, - StructToZodSchema(StartsWith{})) - - type Bad struct { - Name string `validate:"bad=hello"` - } - assert.Panics(t, func() { - StructToZodSchema(Bad{}) + t.Run("eq", func(t *testing.T) { + type Eq struct { + Name string `validate:"eq=hello"` + } + assertSchema(t, Eq{}) }) - type Required struct { - Name string `validate:"required"` - } - assert.Equal(t, - `export const RequiredSchema = z.object({ - Name: z.string().min(1), -}) -export type Required = z.infer - -`, - StructToZodSchema(Required{})) - - type Email struct { - Name string `validate:"email"` - } - assert.Equal(t, - `export const EmailSchema = z.object({ - Name: z.string().email(), -}) -export type Email = z.infer - -`, - StructToZodSchema(Email{}, WithZodV3())) - - type URL struct { - Name string `validate:"url"` - } - assert.Equal(t, - `export const URLSchema = z.object({ - Name: z.string().url(), -}) -export type URL = z.infer - -`, - StructToZodSchema(URL{}, WithZodV3())) - - type IPv4 struct { - Name string `validate:"ipv4"` - } - assert.Equal(t, - `export const IPv4Schema = z.object({ - Name: z.string().ip({ version: "v4" }), -}) -export type IPv4 = z.infer - -`, - StructToZodSchema(IPv4{}, WithZodV3())) - - type IPv6 struct { - Name string `validate:"ipv6"` - } - assert.Equal(t, - `export const IPv6Schema = z.object({ - Name: z.string().ip({ version: "v6" }), -}) -export type IPv6 = z.infer - -`, - StructToZodSchema(IPv6{}, WithZodV3())) - - type IP4Addr struct { - Name string `validate:"ip4_addr"` - } - assert.Equal(t, - `export const IP4AddrSchema = z.object({ - Name: z.string().ip({ version: "v4" }), -}) -export type IP4Addr = z.infer - -`, - StructToZodSchema(IP4Addr{}, WithZodV3())) - - type IP6Addr struct { - Name string `validate:"ip6_addr"` - } - assert.Equal(t, - `export const IP6AddrSchema = z.object({ - Name: z.string().ip({ version: "v6" }), -}) -export type IP6Addr = z.infer - -`, - StructToZodSchema(IP6Addr{}, WithZodV3())) - - type IP struct { - Name string `validate:"ip"` - } - assert.Equal(t, - `export const IPSchema = z.object({ - Name: z.string().ip(), -}) -export type IP = z.infer - -`, - StructToZodSchema(IP{}, WithZodV3())) - - type IPAddr struct { - Name string `validate:"ip_addr"` - } - assert.Equal(t, - `export const IPAddrSchema = z.object({ - Name: z.string().ip(), -}) -export type IPAddr = z.infer - -`, - StructToZodSchema(IPAddr{}, WithZodV3())) - - type HttpURL struct { - Name string `validate:"http_url"` - } - assert.Equal(t, - `export const HttpURLSchema = z.object({ - Name: z.string().url(), -}) -export type HttpURL = z.infer - -`, - StructToZodSchema(HttpURL{}, WithZodV3())) - - type URLEncoded struct { - Name string `validate:"url_encoded"` - } - assert.Equal(t, - fmt.Sprintf(`export const URLEncodedSchema = z.object({ - Name: z.string().regex(/%s/), -}) -export type URLEncoded = z.infer - -`, uRLEncodedRegexString), - StructToZodSchema(URLEncoded{})) - - type Alpha struct { - Name string `validate:"alpha"` - } - assert.Equal(t, - fmt.Sprintf(`export const AlphaSchema = z.object({ - Name: z.string().regex(/%s/), -}) -export type Alpha = z.infer - -`, alphaRegexString), - StructToZodSchema(Alpha{})) - - type AlphaNum struct { - Name string `validate:"alphanum"` - } - assert.Equal(t, - fmt.Sprintf(`export const AlphaNumSchema = z.object({ - Name: z.string().regex(/%s/), -}) -export type AlphaNum = z.infer - -`, alphaNumericRegexString), - StructToZodSchema(AlphaNum{})) - - type AlphaNumUnicode struct { - Name string `validate:"alphanumunicode"` - } - assert.Equal(t, - fmt.Sprintf(`export const AlphaNumUnicodeSchema = z.object({ - Name: z.string().regex(/%s/), -}) -export type AlphaNumUnicode = z.infer + t.Run("ne", func(t *testing.T) { + type Ne struct { + Name string `validate:"ne=hello"` + } + assertSchema(t, Ne{}) + }) -`, alphaUnicodeNumericRegexString), - StructToZodSchema(AlphaNumUnicode{})) + t.Run("oneof", func(t *testing.T) { + type OneOf struct { + Name string `validate:"oneof=hello world"` + } + assertSchema(t, OneOf{}) + }) - type AlphaUnicode struct { - Name string `validate:"alphaunicode"` - } - assert.Equal(t, - fmt.Sprintf(`export const AlphaUnicodeSchema = z.object({ - Name: z.string().regex(/%s/), -}) -export type AlphaUnicode = z.infer + t.Run("oneof_separated", func(t *testing.T) { + type OneOfSeparated struct { + Name string `validate:"oneof='a b c' 'd e f'"` + } + assertSchema(t, OneOfSeparated{}) + }) -`, alphaUnicodeRegexString), - StructToZodSchema(AlphaUnicode{})) + t.Run("len", func(t *testing.T) { + type Len struct { + Name string `validate:"len=5"` + } + assertSchema(t, Len{}) + }) - type ASCII struct { - Name string `validate:"ascii"` - } - assert.Equal(t, - fmt.Sprintf(`export const ASCIISchema = z.object({ - Name: z.string().regex(/%s/), -}) -export type ASCII = z.infer + t.Run("min", func(t *testing.T) { + type Min struct { + Name string `validate:"min=5"` + } + assertSchema(t, Min{}) + }) -`, aSCIIRegexString), - StructToZodSchema(ASCII{})) + t.Run("max", func(t *testing.T) { + type Max struct { + Name string `validate:"max=5"` + } + assertSchema(t, Max{}) + }) - type Boolean struct { - Name string `validate:"boolean"` - } - assert.Equal(t, - `export const BooleanSchema = z.object({ - Name: z.enum(['true', 'false']), -}) -export type Boolean = z.infer + t.Run("minmax", func(t *testing.T) { + type MinMax struct { + Name string `validate:"min=3,max=7"` + } + assertSchema(t, MinMax{}) + }) -`, - StructToZodSchema(Boolean{})) + t.Run("gt", func(t *testing.T) { + type Gt struct { + Name string `validate:"gt=5"` + } + assertSchema(t, Gt{}) + }) - type Lowercase struct { - Name string `validate:"lowercase"` - } - assert.Equal(t, - `export const LowercaseSchema = z.object({ - Name: z.string().refine((val) => val === val.toLowerCase()), -}) -export type Lowercase = z.infer + t.Run("gte", func(t *testing.T) { + type Gte struct { + Name string `validate:"gte=5"` + } + assertSchema(t, Gte{}) + }) -`, - StructToZodSchema(Lowercase{})) + t.Run("lt", func(t *testing.T) { + type Lt struct { + Name string `validate:"lt=5"` + } + assertSchema(t, Lt{}) + }) - type Number struct { - Name string `validate:"number"` - } - assert.Equal(t, - fmt.Sprintf(`export const NumberSchema = z.object({ - Name: z.string().regex(/%s/), -}) -export type Number = z.infer + t.Run("lte", func(t *testing.T) { + type Lte struct { + Name string `validate:"lte=5"` + } + assertSchema(t, Lte{}) + }) -`, numberRegexString), - StructToZodSchema(Number{})) + t.Run("contains", func(t *testing.T) { + type Contains struct { + Name string `validate:"contains=hello"` + } + assertSchema(t, Contains{}) + }) - type Numeric struct { - Name string `validate:"numeric"` - } - assert.Equal(t, - fmt.Sprintf(`export const NumericSchema = z.object({ - Name: z.string().regex(/%s/), -}) -export type Numeric = z.infer + t.Run("endswith", func(t *testing.T) { + type EndsWith struct { + Name string `validate:"endswith=hello"` + } + assertSchema(t, EndsWith{}) + }) -`, numericRegexString), - StructToZodSchema(Numeric{})) + t.Run("startswith", func(t *testing.T) { + type StartsWith struct { + Name string `validate:"startswith=hello"` + } + assertSchema(t, StartsWith{}) + }) - type Uppercase struct { - Name string `validate:"uppercase"` - } - assert.Equal(t, - `export const UppercaseSchema = z.object({ - Name: z.string().refine((val) => val === val.toUpperCase()), -}) -export type Uppercase = z.infer + t.Run("bad", func(t *testing.T) { + type Bad struct { + Name string `validate:"bad=hello"` + } + assert.Panics(t, func() { + StructToZodSchema(Bad{}) + }) + }) -`, - StructToZodSchema(Uppercase{})) + t.Run("required", func(t *testing.T) { + type Required struct { + Name string `validate:"required"` + } + assertSchema(t, Required{}) + }) - type Base64 struct { - Name string `validate:"base64"` - } - assert.Equal(t, - fmt.Sprintf(`export const Base64Schema = z.object({ - Name: z.string().regex(/%s/), -}) -export type Base64 = z.infer + t.Run("email", func(t *testing.T) { + type Email struct { + Name string `validate:"email"` + } + assertSchema(t, Email{}, "v3", "v4") + }) -`, base64RegexString), - StructToZodSchema(Base64{}, WithZodV3())) + t.Run("url", func(t *testing.T) { + type URL struct { + Name string `validate:"url"` + } + assertSchema(t, URL{}, "v3", "v4") + }) - type mongodb struct { - Name string `validate:"mongodb"` - } - assert.Equal(t, - fmt.Sprintf(`export const mongodbSchema = z.object({ - Name: z.string().regex(/%s/), -}) -export type mongodb = z.infer + t.Run("ipv4", func(t *testing.T) { + type IPv4 struct { + Name string `validate:"ipv4"` + } + assertSchema(t, IPv4{}, "v3", "v4") + }) -`, mongodbRegexString), - StructToZodSchema(mongodb{})) + t.Run("ipv6", func(t *testing.T) { + type IPv6 struct { + Name string `validate:"ipv6"` + } + assertSchema(t, IPv6{}, "v3", "v4") + }) - type datetime struct { - Name string `validate:"datetime"` - } - assert.Equal(t, - `export const datetimeSchema = z.object({ - Name: z.string().datetime(), -}) -export type datetime = z.infer + t.Run("ip4_addr", func(t *testing.T) { + type IP4Addr struct { + Name string `validate:"ip4_addr"` + } + assertSchema(t, IP4Addr{}, "v3", "v4") + }) -`, - StructToZodSchema(datetime{}, WithZodV3())) + t.Run("ip6_addr", func(t *testing.T) { + type IP6Addr struct { + Name string `validate:"ip6_addr"` + } + assertSchema(t, IP6Addr{}, "v3", "v4") + }) - type Hexadecimal struct { - Name string `validate:"hexadecimal"` - } - assert.Equal(t, - fmt.Sprintf(`export const HexadecimalSchema = z.object({ - Name: z.string().regex(/%s/), -}) -export type Hexadecimal = z.infer + t.Run("ip", func(t *testing.T) { + type IP struct { + Name string `validate:"ip"` + } + assertSchema(t, IP{}, "v3", "v4") + }) -`, hexadecimalRegexString), - StructToZodSchema(Hexadecimal{}, WithZodV3())) + t.Run("ip_addr", func(t *testing.T) { + type IPAddr struct { + Name string `validate:"ip_addr"` + } + assertSchema(t, IPAddr{}, "v3", "v4") + }) - type json struct { - Name string `validate:"json"` - } - assert.Equal(t, - `export const jsonSchema = z.object({ - Name: z.string().refine((val) => { try { JSON.parse(val); return true } catch { return false } }), -}) -export type json = z.infer + t.Run("http_url", func(t *testing.T) { + type HttpURL struct { + Name string `validate:"http_url"` + } + assertSchema(t, HttpURL{}, "v3", "v4") + }) -`, - StructToZodSchema(json{})) + t.Run("url_encoded", func(t *testing.T) { + type URLEncoded struct { + Name string `validate:"url_encoded"` + } + assertSchema(t, URLEncoded{}) + }) - type Latitude struct { - Name string `validate:"latitude"` - } - assert.Equal(t, - fmt.Sprintf(`export const LatitudeSchema = z.object({ - Name: z.string().regex(/%s/), -}) -export type Latitude = z.infer + t.Run("alpha", func(t *testing.T) { + type Alpha struct { + Name string `validate:"alpha"` + } + assertSchema(t, Alpha{}) + }) -`, latitudeRegexString), - StructToZodSchema(Latitude{})) + t.Run("alphanum", func(t *testing.T) { + type AlphaNum struct { + Name string `validate:"alphanum"` + } + assertSchema(t, AlphaNum{}) + }) - type Longitude struct { - Name string `validate:"longitude"` - } - assert.Equal(t, - fmt.Sprintf(`export const LongitudeSchema = z.object({ - Name: z.string().regex(/%s/), -}) -export type Longitude = z.infer + t.Run("alphanumunicode", func(t *testing.T) { + type AlphaNumUnicode struct { + Name string `validate:"alphanumunicode"` + } + assertSchema(t, AlphaNumUnicode{}) + }) -`, longitudeRegexString), - StructToZodSchema(Longitude{})) + t.Run("alphaunicode", func(t *testing.T) { + type AlphaUnicode struct { + Name string `validate:"alphaunicode"` + } + assertSchema(t, AlphaUnicode{}) + }) - type UUID struct { - Name string `validate:"uuid"` - } - assert.Equal(t, - fmt.Sprintf(`export const UUIDSchema = z.object({ - Name: z.string().regex(/%s/), -}) -export type UUID = z.infer + t.Run("ascii", func(t *testing.T) { + type ASCII struct { + Name string `validate:"ascii"` + } + assertSchema(t, ASCII{}) + }) -`, uUIDRegexString), - StructToZodSchema(UUID{}, WithZodV3())) + t.Run("boolean", func(t *testing.T) { + type Boolean struct { + Name string `validate:"boolean"` + } + assertSchema(t, Boolean{}) + }) - type UUID3 struct { - Name string `validate:"uuid3"` - } - assert.Equal(t, - fmt.Sprintf(`export const UUID3Schema = z.object({ - Name: z.string().regex(/%s/), -}) -export type UUID3 = z.infer + t.Run("lowercase", func(t *testing.T) { + type Lowercase struct { + Name string `validate:"lowercase"` + } + assertSchema(t, Lowercase{}) + }) -`, uUID3RegexString), - StructToZodSchema(UUID3{}, WithZodV3())) + t.Run("number", func(t *testing.T) { + type Number struct { + Name string `validate:"number"` + } + assertSchema(t, Number{}) + }) - type UUID3RFC4122 struct { - Name string `validate:"uuid3_rfc4122"` - } - assert.Equal(t, - fmt.Sprintf(`export const UUID3RFC4122Schema = z.object({ - Name: z.string().regex(/%s/), -}) -export type UUID3RFC4122 = z.infer + t.Run("numeric", func(t *testing.T) { + type Numeric struct { + Name string `validate:"numeric"` + } + assertSchema(t, Numeric{}) + }) -`, uUID3RFC4122RegexString), - StructToZodSchema(UUID3RFC4122{}, WithZodV3())) + t.Run("uppercase", func(t *testing.T) { + type Uppercase struct { + Name string `validate:"uppercase"` + } + assertSchema(t, Uppercase{}) + }) - type UUID4 struct { - Name string `validate:"uuid4"` - } - assert.Equal(t, - fmt.Sprintf(`export const UUID4Schema = z.object({ - Name: z.string().regex(/%s/), -}) -export type UUID4 = z.infer + t.Run("base64", func(t *testing.T) { + type Base64 struct { + Name string `validate:"base64"` + } + assertSchema(t, Base64{}, "v3", "v4") + }) -`, uUID4RegexString), - StructToZodSchema(UUID4{}, WithZodV3())) + t.Run("mongodb", func(t *testing.T) { + type mongodb struct { + Name string `validate:"mongodb"` + } + assertSchema(t, mongodb{}) + }) - type UUID4RFC4122 struct { - Name string `validate:"uuid4_rfc4122"` - } - assert.Equal(t, - fmt.Sprintf(`export const UUID4RFC4122Schema = z.object({ - Name: z.string().regex(/%s/), -}) -export type UUID4RFC4122 = z.infer + t.Run("datetime", func(t *testing.T) { + type datetime struct { + Name string `validate:"datetime"` + } + assertSchema(t, datetime{}, "v3", "v4") + }) -`, uUID4RFC4122RegexString), - StructToZodSchema(UUID4RFC4122{}, WithZodV3())) + t.Run("hexadecimal", func(t *testing.T) { + type Hexadecimal struct { + Name string `validate:"hexadecimal"` + } + assertSchema(t, Hexadecimal{}, "v3", "v4") + }) - type UUID5 struct { - Name string `validate:"uuid5"` - } - assert.Equal(t, - fmt.Sprintf(`export const UUID5Schema = z.object({ - Name: z.string().regex(/%s/), -}) -export type UUID5 = z.infer + t.Run("json", func(t *testing.T) { + type json struct { + Name string `validate:"json"` + } + assertSchema(t, json{}) + }) -`, uUID5RegexString), - StructToZodSchema(UUID5{}, WithZodV3())) + t.Run("latitude", func(t *testing.T) { + type Latitude struct { + Name string `validate:"latitude"` + } + assertSchema(t, Latitude{}) + }) - type UUID5RFC4122 struct { - Name string `validate:"uuid5_rfc4122"` - } - assert.Equal(t, - fmt.Sprintf(`export const UUID5RFC4122Schema = z.object({ - Name: z.string().regex(/%s/), -}) -export type UUID5RFC4122 = z.infer + t.Run("longitude", func(t *testing.T) { + type Longitude struct { + Name string `validate:"longitude"` + } + assertSchema(t, Longitude{}) + }) -`, uUID5RFC4122RegexString), - StructToZodSchema(UUID5RFC4122{}, WithZodV3())) + t.Run("uuid", func(t *testing.T) { + type UUID struct { + Name string `validate:"uuid"` + } + assertSchema(t, UUID{}, "v3", "v4") + }) - type UUIDRFC4122 struct { - Name string `validate:"uuid_rfc4122"` - } - assert.Equal(t, - fmt.Sprintf(`export const UUIDRFC4122Schema = z.object({ - Name: z.string().regex(/%s/), -}) -export type UUIDRFC4122 = z.infer + t.Run("uuid3", func(t *testing.T) { + type UUID3 struct { + Name string `validate:"uuid3"` + } + assertSchema(t, UUID3{}, "v3", "v4") + }) -`, uUIDRFC4122RegexString), - StructToZodSchema(UUIDRFC4122{}, WithZodV3())) + t.Run("uuid3_rfc4122", func(t *testing.T) { + type UUID3RFC4122 struct { + Name string `validate:"uuid3_rfc4122"` + } + assertSchema(t, UUID3RFC4122{}, "v3", "v4") + }) - type MD4 struct { - Name string `validate:"md4"` - } - assert.Equal(t, - fmt.Sprintf(`export const MD4Schema = z.object({ - Name: z.string().regex(/%s/), -}) -export type MD4 = z.infer + t.Run("uuid4", func(t *testing.T) { + type UUID4 struct { + Name string `validate:"uuid4"` + } + assertSchema(t, UUID4{}, "v3", "v4") + }) -`, md4RegexString), - StructToZodSchema(MD4{})) + t.Run("uuid4_rfc4122", func(t *testing.T) { + type UUID4RFC4122 struct { + Name string `validate:"uuid4_rfc4122"` + } + assertSchema(t, UUID4RFC4122{}, "v3", "v4") + }) - type MD5 struct { - Name string `validate:"md5"` - } - assert.Equal(t, - fmt.Sprintf(`export const MD5Schema = z.object({ - Name: z.string().regex(/%s/), -}) -export type MD5 = z.infer + t.Run("uuid5", func(t *testing.T) { + type UUID5 struct { + Name string `validate:"uuid5"` + } + assertSchema(t, UUID5{}, "v3", "v4") + }) -`, md5RegexString), - StructToZodSchema(MD5{}, WithZodV3())) + t.Run("uuid5_rfc4122", func(t *testing.T) { + type UUID5RFC4122 struct { + Name string `validate:"uuid5_rfc4122"` + } + assertSchema(t, UUID5RFC4122{}, "v3", "v4") + }) - type SHA256 struct { - Name string `validate:"sha256"` - } - assert.Equal(t, - fmt.Sprintf(`export const SHA256Schema = z.object({ - Name: z.string().regex(/%s/), -}) -export type SHA256 = z.infer + t.Run("uuid_rfc4122", func(t *testing.T) { + type UUIDRFC4122 struct { + Name string `validate:"uuid_rfc4122"` + } + assertSchema(t, UUIDRFC4122{}, "v3", "v4") + }) -`, sha256RegexString), - StructToZodSchema(SHA256{}, WithZodV3())) + t.Run("md4", func(t *testing.T) { + type MD4 struct { + Name string `validate:"md4"` + } + assertSchema(t, MD4{}) + }) - type SHA384 struct { - Name string `validate:"sha384"` - } - assert.Equal(t, - fmt.Sprintf(`export const SHA384Schema = z.object({ - Name: z.string().regex(/%s/), -}) -export type SHA384 = z.infer + t.Run("md5", func(t *testing.T) { + type MD5 struct { + Name string `validate:"md5"` + } + assertSchema(t, MD5{}, "v3", "v4") + }) -`, sha384RegexString), - StructToZodSchema(SHA384{}, WithZodV3())) + t.Run("sha256", func(t *testing.T) { + type SHA256 struct { + Name string `validate:"sha256"` + } + assertSchema(t, SHA256{}, "v3", "v4") + }) - type SHA512 struct { - Name string `validate:"sha512"` - } - assert.Equal(t, - fmt.Sprintf(`export const SHA512Schema = z.object({ - Name: z.string().regex(/%s/), -}) -export type SHA512 = z.infer + t.Run("sha384", func(t *testing.T) { + type SHA384 struct { + Name string `validate:"sha384"` + } + assertSchema(t, SHA384{}, "v3", "v4") + }) -`, sha512RegexString), - StructToZodSchema(SHA512{}, WithZodV3())) + t.Run("sha512", func(t *testing.T) { + type SHA512 struct { + Name string `validate:"sha512"` + } + assertSchema(t, SHA512{}, "v3", "v4") + }) - type Bad2 struct { - Name string `validate:"bad2"` - } - assert.Panics(t, func() { - StructToZodSchema(Bad2{}) + t.Run("bad2", func(t *testing.T) { + type Bad2 struct { + Name string `validate:"bad2"` + } + assert.Panics(t, func() { + StructToZodSchema(Bad2{}) + }) }) } @@ -1135,24 +710,7 @@ func TestZodV4Defaults(t *testing.T) { Tags []string } - assert.Equal(t, `export const HasIDSchema = z.object({ - ID: z.string(), -}) -export type HasID = z.infer - -export const HasNameSchema = z.object({ - name: z.string(), -}) -export type HasName = z.infer - -export const UserSchema = z.object({ - Tags: z.string().array().nullable(), - ...HasIDSchema.shape, - ...HasNameSchema.shape, -}) -export type User = z.infer - -`, StructToZodSchema(User{})) + assertSchema(t, User{}, "v4") }) t.Run("string formats use zod v4 builders", func(t *testing.T) { @@ -1164,16 +722,7 @@ export type User = z.infer Checksum string `validate:"md5"` } - assert.Equal(t, `export const PayloadSchema = z.object({ - Email: z.email(), - Link: z.httpUrl(), - Base64: z.base64(), - ID: z.uuid({ version: "v4" }), - Checksum: z.hash("md5"), -}) -export type Payload = z.infer - -`, StructToZodSchema(Payload{})) + assertSchema(t, Payload{}, "v4") }) t.Run("string tag order is preserved around v4 format helpers", func(t *testing.T) { @@ -1188,13 +737,7 @@ export type Payload = z.infer }, } - assert.Equal(t, `export const PayloadSchema = z.object({ - TrimmedThenEmail: z.string().trim().email(), - EmailThenTrimmed: z.email().trim(), -}) -export type Payload = z.infer - -`, NewConverterWithOpts(WithCustomTags(customTagHandlers)).Convert(Payload{})) + golden.Assert(t, []byte(NewConverterWithOpts(WithCustomTags(customTagHandlers)).Convert(Payload{}))) }) t.Run("ip unions inherit generic string constraints", func(t *testing.T) { @@ -1202,12 +745,7 @@ export type Payload = z.infer Address string `validate:"ip,required,max=45"` } - assert.Equal(t, `export const PayloadSchema = z.object({ - Address: z.union([z.ipv4().min(1).refine((val) => [...val].length <= 45, 'String must contain at most 45 character(s)'), z.ipv6().min(1).refine((val) => [...val].length <= 45, 'String must contain at most 45 character(s)')]), -}) -export type Payload = z.infer - -`, StructToZodSchema(Payload{})) + assertSchema(t, Payload{}, "v4") }) t.Run("oneof takes precedence over ip specialization", func(t *testing.T) { @@ -1215,12 +753,7 @@ export type Payload = z.infer Address string `validate:"oneof='127.0.0.1' '::1',ip"` } - assert.Equal(t, `export const PayloadSchema = z.object({ - Address: z.enum(["127.0.0.1", "::1"] as const), -}) -export type Payload = z.infer - -`, StructToZodSchema(Payload{})) + assertSchema(t, Payload{}, "v4") }) t.Run("ip mixed with another format falls back to legacy chain semantics", func(t *testing.T) { @@ -1228,12 +761,7 @@ export type Payload = z.infer Address string `validate:"email,ip"` } - assert.Equal(t, `export const PayloadSchema = z.object({ - Address: z.string().email().ip(), -}) -export type Payload = z.infer - -`, StructToZodSchema(Payload{})) + assertSchema(t, Payload{}, "v4") }) t.Run("enum keyed maps become partial records", func(t *testing.T) { @@ -1241,12 +769,7 @@ export type Payload = z.infer Metadata map[string]string `validate:"dive,keys,oneof=draft published,endkeys"` } - assert.Equal(t, `export const PayloadSchema = z.object({ - Metadata: z.partialRecord(z.enum(["draft", "published"] as const), z.string()).nullable(), -}) -export type Payload = z.infer - -`, StructToZodSchema(Payload{})) + assertSchema(t, Payload{}, "v4") }) t.Run("recursive embedded shapes preserve encounter order for duplicate keys", func(t *testing.T) { @@ -1260,23 +783,7 @@ export type Payload = z.infer Next *Node `json:"next"` } - assert.Equal(t, `export const BaseSchema = z.object({ - id: z.string(), -}) -export type Base = z.infer - -export type Node = Base & { - id: number, - next: Node | null, -} -const NodeSchemaShape = { - ...BaseSchema.shape, - id: z.number(), - next: z.lazy(() => NodeSchema).nullable(), -} -export const NodeSchema: z.ZodType = z.object(NodeSchemaShape) - -`, StructToZodSchema(Node{})) + assertSchema(t, Node{}, "v4") }) t.Run("recursive embedded shapes keep named fields before spreads", func(t *testing.T) { @@ -1291,111 +798,67 @@ export const NodeSchema: z.ZodType = z.object(NodeSchemaShape) UpdatedAt time.Time } - assert.Equal(t, `export type TreeNode = { - Value: string, - CreatedAt: Date, - Children: TreeNode[] | null, -} -const TreeNodeSchemaShape = { - Value: z.string(), - CreatedAt: z.coerce.date(), - Children: z.lazy(() => TreeNodeSchema).array().nullable(), -} -export const TreeNodeSchema: z.ZodType = z.object(TreeNodeSchemaShape) - -export const TreeSchema = z.object({ - UpdatedAt: z.coerce.date(), - ...TreeNodeSchemaShape, -}) -export type Tree = z.infer - -`, StructToZodSchema(Tree{})) + assertSchema(t, Tree{}, "v4") }) } func TestNumberValidations(t *testing.T) { - type User1 struct { - Age int `validate:"gte=18,lte=60"` - } - assert.Equal(t, - `export const User1Schema = z.object({ - Age: z.number().gte(18).lte(60), -}) -export type User1 = z.infer - -`, StructToZodSchema(User1{})) - - type User2 struct { - Age int `validate:"gt=18,lt=60"` - } - assert.Equal(t, - `export const User2Schema = z.object({ - Age: z.number().gt(18).lt(60), -}) -export type User2 = z.infer - -`, StructToZodSchema(User2{})) - - type User3 struct { - Age int `validate:"eq=18"` - } - assert.Equal(t, - `export const User3Schema = z.object({ - Age: z.number().refine((val) => val === 18), -}) -export type User3 = z.infer - -`, StructToZodSchema(User3{})) - - type User4 struct { - Age int `validate:"ne=18"` - } - assert.Equal(t, - `export const User4Schema = z.object({ - Age: z.number().refine((val) => val !== 18), -}) -export type User4 = z.infer - -`, StructToZodSchema(User4{})) + t.Run("gte_lte", func(t *testing.T) { + type User1 struct { + Age int `validate:"gte=18,lte=60"` + } + assertSchema(t, User1{}) + }) - type User5 struct { - Age int `validate:"oneof=18 19 20"` - } - assert.Equal(t, - `export const User5Schema = z.object({ - Age: z.number().refine((val) => [18, 19, 20].includes(val)), -}) -export type User5 = z.infer + t.Run("gt_lt", func(t *testing.T) { + type User2 struct { + Age int `validate:"gt=18,lt=60"` + } + assertSchema(t, User2{}) + }) -`, StructToZodSchema(User5{})) + t.Run("eq", func(t *testing.T) { + type User3 struct { + Age int `validate:"eq=18"` + } + assertSchema(t, User3{}) + }) - type User6 struct { - Age int `validate:"min=18,max=60"` - } - assert.Equal(t, - `export const User6Schema = z.object({ - Age: z.number().gte(18).lte(60), -}) -export type User6 = z.infer + t.Run("ne", func(t *testing.T) { + type User4 struct { + Age int `validate:"ne=18"` + } + assertSchema(t, User4{}) + }) -`, StructToZodSchema(User6{})) + t.Run("oneof", func(t *testing.T) { + type User5 struct { + Age int `validate:"oneof=18 19 20"` + } + assertSchema(t, User5{}) + }) - type User7 struct { - Age int `validate:"len=18"` - } - assert.Equal(t, - `export const User7Schema = z.object({ - Age: z.number().refine((val) => val === 18), -}) -export type User7 = z.infer + t.Run("min_max", func(t *testing.T) { + type User6 struct { + Age int `validate:"min=18,max=60"` + } + assertSchema(t, User6{}) + }) -`, StructToZodSchema(User7{})) + t.Run("len", func(t *testing.T) { + type User7 struct { + Age int `validate:"len=18"` + } + assertSchema(t, User7{}) + }) - type User8 struct { - Age int `validate:"bad=18"` - } - assert.Panics(t, func() { - StructToZodSchema(User8{}) + t.Run("bad", func(t *testing.T) { + type User8 struct { + Age int `validate:"bad=18"` + } + assert.Panics(t, func() { + StructToZodSchema(User8{}) + }) }) } @@ -1404,15 +867,7 @@ func TestInterfaceAny(t *testing.T) { Name string Metadata interface{} } - assert.Equal(t, - `export const UserSchema = z.object({ - Name: z.string(), - Metadata: z.any(), -}) -export type User = z.infer - -`, - StructToZodSchema(User{})) + assertSchema(t, User{}) } func TestInterfacePointerAny(t *testing.T) { @@ -1420,15 +875,7 @@ func TestInterfacePointerAny(t *testing.T) { Name string Metadata *interface{} } - assert.Equal(t, - `export const UserSchema = z.object({ - Name: z.string(), - Metadata: z.any(), -}) -export type User = z.infer - -`, - StructToZodSchema(User{})) + assertSchema(t, User{}) } func TestInterfaceEmptyAny(t *testing.T) { @@ -1436,15 +883,7 @@ func TestInterfaceEmptyAny(t *testing.T) { Name string Metadata interface{} `json:",omitempty"` } - assert.Equal(t, - `export const UserSchema = z.object({ - Name: z.string(), - Metadata: z.any(), -}) -export type User = z.infer - -`, - StructToZodSchema(User{})) + assertSchema(t, User{}) } func TestInterfacePointerEmptyAny(t *testing.T) { @@ -1452,229 +891,140 @@ func TestInterfacePointerEmptyAny(t *testing.T) { Name string Metadata *interface{} `json:",omitempty"` } - assert.Equal(t, - `export const UserSchema = z.object({ - Name: z.string(), - Metadata: z.any(), -}) -export type User = z.infer - -`, - StructToZodSchema(User{})) -} - -func TestMapStringToString(t *testing.T) { - type User struct { - Name string - Metadata map[string]string - } - assert.Equal(t, - `export const UserSchema = z.object({ - Name: z.string(), - Metadata: z.record(z.string(), z.string()).nullable(), -}) -export type User = z.infer - -`, - StructToZodSchema(User{})) + assertSchema(t, User{}) } -func TestMapStringToInterface(t *testing.T) { - type User struct { - Name string - Metadata map[string]interface{} - } - assert.Equal(t, - `export const UserSchema = z.object({ - Name: z.string(), - Metadata: z.record(z.string(), z.any()).nullable(), -}) -export type User = z.infer - -`, - StructToZodSchema(User{})) -} - -func TestMapWithStruct(t *testing.T) { - type PostWithMetaData struct { - Title string - } - type User struct { - MapWithStruct map[string]PostWithMetaData - } - assert.Equal(t, - `export const PostWithMetaDataSchema = z.object({ - Title: z.string(), -}) -export type PostWithMetaData = z.infer - -export const UserSchema = z.object({ - MapWithStruct: z.record(z.string(), PostWithMetaDataSchema).nullable(), -}) -export type User = z.infer - -`, StructToZodSchema(User{})) -} - -func TestMapWithValidations(t *testing.T) { - type Required struct { - Map map[string]string `validate:"required"` - } - assert.Equal(t, - `export const RequiredSchema = z.object({ - Map: z.record(z.string(), z.string()), -}) -export type Required = z.infer - -`, StructToZodSchema(Required{})) - - type Min struct { - Map map[string]string `validate:"min=1"` - } - assert.Equal(t, - `export const MinSchema = z.object({ - Map: z.record(z.string(), z.string()).refine((val) => Object.keys(val).length >= 1, 'Map too small'), -}) -export type Min = z.infer - -`, StructToZodSchema(Min{})) - - type Max struct { - Map map[string]string `validate:"max=1"` - } - assert.Equal(t, - `export const MaxSchema = z.object({ - Map: z.record(z.string(), z.string()).refine((val) => Object.keys(val).length <= 1, 'Map too large'), -}) -export type Max = z.infer - -`, StructToZodSchema(Max{})) - - type Len struct { - Map map[string]string `validate:"len=1"` - } - assert.Equal(t, - `export const LenSchema = z.object({ - Map: z.record(z.string(), z.string()).refine((val) => Object.keys(val).length === 1, 'Map wrong size'), -}) -export type Len = z.infer - -`, StructToZodSchema(Len{})) - - type MinMax struct { - Map map[string]string `validate:"min=1,max=2"` +func TestMapStringToString(t *testing.T) { + type User struct { + Name string + Metadata map[string]string } - assert.Equal(t, - `export const MinMaxSchema = z.object({ - Map: z.record(z.string(), z.string()).refine((val) => Object.keys(val).length >= 1, 'Map too small').refine((val) => Object.keys(val).length <= 2, 'Map too large'), -}) -export type MinMax = z.infer - -`, StructToZodSchema(MinMax{})) + assertSchema(t, User{}) +} - type Eq struct { - Map map[string]string `validate:"eq=1"` +func TestMapStringToInterface(t *testing.T) { + type User struct { + Name string + Metadata map[string]interface{} } - assert.Equal(t, - `export const EqSchema = z.object({ - Map: z.record(z.string(), z.string()).refine((val) => Object.keys(val).length === 1, 'Map wrong size'), -}) -export type Eq = z.infer - -`, StructToZodSchema(Eq{})) + assertSchema(t, User{}) +} - type Ne struct { - Map map[string]string `validate:"ne=1"` +func TestMapWithStruct(t *testing.T) { + type PostWithMetaData struct { + Title string } - assert.Equal(t, - `export const NeSchema = z.object({ - Map: z.record(z.string(), z.string()).refine((val) => Object.keys(val).length !== 1, 'Map wrong size'), -}) -export type Ne = z.infer - -`, StructToZodSchema(Ne{})) - - type Gt struct { - Map map[string]string `validate:"gt=1"` + type User struct { + MapWithStruct map[string]PostWithMetaData } - assert.Equal(t, - `export const GtSchema = z.object({ - Map: z.record(z.string(), z.string()).refine((val) => Object.keys(val).length > 1, 'Map too small'), -}) -export type Gt = z.infer + assertSchema(t, User{}) +} -`, StructToZodSchema(Gt{})) +func TestMapWithValidations(t *testing.T) { + t.Run("required", func(t *testing.T) { + type Required struct { + Map map[string]string `validate:"required"` + } + assertSchema(t, Required{}) + }) - type Gte struct { - Map map[string]string `validate:"gte=1"` - } - assert.Equal(t, - `export const GteSchema = z.object({ - Map: z.record(z.string(), z.string()).refine((val) => Object.keys(val).length >= 1, 'Map too small'), -}) -export type Gte = z.infer + t.Run("min", func(t *testing.T) { + type Min struct { + Map map[string]string `validate:"min=1"` + } + assertSchema(t, Min{}) + }) -`, StructToZodSchema(Gte{})) + t.Run("max", func(t *testing.T) { + type Max struct { + Map map[string]string `validate:"max=1"` + } + assertSchema(t, Max{}) + }) - type Lt struct { - Map map[string]string `validate:"lt=1"` - } - assert.Equal(t, - `export const LtSchema = z.object({ - Map: z.record(z.string(), z.string()).refine((val) => Object.keys(val).length < 1, 'Map too large'), -}) -export type Lt = z.infer + t.Run("len", func(t *testing.T) { + type Len struct { + Map map[string]string `validate:"len=1"` + } + assertSchema(t, Len{}) + }) -`, StructToZodSchema(Lt{})) + t.Run("minmax", func(t *testing.T) { + type MinMax struct { + Map map[string]string `validate:"min=1,max=2"` + } + assertSchema(t, MinMax{}) + }) - type Lte struct { - Map map[string]string `validate:"lte=1"` - } - assert.Equal(t, - `export const LteSchema = z.object({ - Map: z.record(z.string(), z.string()).refine((val) => Object.keys(val).length <= 1, 'Map too large'), -}) -export type Lte = z.infer + t.Run("eq", func(t *testing.T) { + type Eq struct { + Map map[string]string `validate:"eq=1"` + } + assertSchema(t, Eq{}) + }) -`, StructToZodSchema(Lte{})) + t.Run("ne", func(t *testing.T) { + type Ne struct { + Map map[string]string `validate:"ne=1"` + } + assertSchema(t, Ne{}) + }) - type Bad struct { - Map map[string]string `validate:"bad=1"` - } - assert.Panics(t, func() { StructToZodSchema(Bad{}) }) + t.Run("gt", func(t *testing.T) { + type Gt struct { + Map map[string]string `validate:"gt=1"` + } + assertSchema(t, Gt{}) + }) - type Dive1 struct { - Map map[string]string `validate:"dive,min=2"` - } - assert.Equal(t, - `export const Dive1Schema = z.object({ - 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 + t.Run("gte", func(t *testing.T) { + type Gte struct { + Map map[string]string `validate:"gte=1"` + } + assertSchema(t, Gte{}) + }) -`, StructToZodSchema(Dive1{})) + t.Run("lt", func(t *testing.T) { + type Lt struct { + Map map[string]string `validate:"lt=1"` + } + assertSchema(t, Lt{}) + }) - type Dive2 struct { - Map []map[string]string `validate:"required,dive,min=2,dive,min=3"` - } - assert.Equal(t, - `export const Dive2Schema = z.object({ - 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 + t.Run("lte", func(t *testing.T) { + type Lte struct { + Map map[string]string `validate:"lte=1"` + } + assertSchema(t, Lte{}) + }) -`, StructToZodSchema(Dive2{})) + t.Run("bad", func(t *testing.T) { + type Bad struct { + Map map[string]string `validate:"bad=1"` + } + assert.Panics(t, func() { StructToZodSchema(Bad{}) }) + }) - type Dive3 struct { - Map []map[string]string `validate:"required,dive,min=2,dive,keys,min=3,endkeys,max=4"` - } - assert.Equal(t, - `export const Dive3Schema = z.object({ - 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 + t.Run("dive1", func(t *testing.T) { + type Dive1 struct { + Map map[string]string `validate:"dive,min=2"` + } + assertSchema(t, Dive1{}) + }) + + t.Run("dive2", func(t *testing.T) { + type Dive2 struct { + Map []map[string]string `validate:"required,dive,min=2,dive,min=3"` + } + assertSchema(t, Dive2{}) + }) -`, StructToZodSchema(Dive3{})) + t.Run("dive3", func(t *testing.T) { + type Dive3 struct { + Map []map[string]string `validate:"required,dive,min=2,dive,keys,min=3,endkeys,max=4"` + } + assertSchema(t, Dive3{}) + }) } func TestMapWithNonStringKey(t *testing.T) { @@ -1683,42 +1033,27 @@ func TestMapWithNonStringKey(t *testing.T) { Metadata map[int]string } - assert.Equal(t, - `export const Map1Schema = z.object({ - Name: z.string(), - Metadata: z.record(z.coerce.number(), z.string()).nullable(), -}) -export type Map1 = z.infer - -`, StructToZodSchema(Map1{})) - type Map2 struct { Name string Metadata map[time.Time]string } - assert.Equal(t, - `export const Map2Schema = z.object({ - Name: z.string(), - Metadata: z.record(z.coerce.date(), z.string()).nullable(), -}) -export type Map2 = z.infer - -`, StructToZodSchema(Map2{})) - type Map3 struct { Name string Metadata map[float64]string } - assert.Equal(t, - `export const Map3Schema = z.object({ - Name: z.string(), - Metadata: z.record(z.coerce.number(), z.string()).nullable(), -}) -export type Map3 = z.infer + t.Run("int_key", func(t *testing.T) { + assertSchema(t, Map1{}) + }) + + t.Run("time_key", func(t *testing.T) { + assertSchema(t, Map2{}) + }) -`, StructToZodSchema(Map3{})) + t.Run("float_key", func(t *testing.T) { + assertSchema(t, Map3{}) + }) } func TestGetValidateKeys(t *testing.T) { @@ -1760,6 +1095,53 @@ func TestGetValidateCurrent(t *testing.T) { assert.Equal(t, "min=2,max=3", getValidateCurrent("min=2,max=3,dive,min=2,dive,min=3,max=4")) } +func TestStructTime(t *testing.T) { + type User struct { + Name string + When time.Time + } + assertSchema(t, User{}) +} + +func TestTimeWithRequired(t *testing.T) { + type User struct { + When time.Time `validate:"required"` + } + assertSchema(t, User{}) +} + +func TestDuration(t *testing.T) { + type User struct { + HowLong time.Duration + } + assertSchema(t, User{}) +} + +func TestCustom(t *testing.T) { + type Decimal struct { + Value int + Exponent int + } + + type User struct { + Name string + Money Decimal + } + + customTypes := map[string]CustomFn{ + "github.com/hypersequent/zen.Decimal": func(c *Converter, t reflect.Type, validate string, i int) string { + return "z.string()" + }, + } + + v3c := NewConverterWithOpts(WithCustomTypes(customTypes), WithZodV3()) + v4c := NewConverterWithOpts(WithCustomTypes(customTypes)) + v3out := v3c.Convert(User{}) + v4out := v4c.Convert(User{}) + assert.Equal(t, v3out, v4out) + golden.Assert(t, []byte(v4out)) +} + func TestEverything(t *testing.T) { // The order matters PostWithMetaData needs to be declared after post otherwise it will raise a // `Block-scoped variable 'Post' used before its declaration.` typescript error. @@ -1799,49 +1181,7 @@ func TestEverything(t *testing.T) { MapWithStruct map[string]PostWithMetaData } - assert.Equal(t, - `export const PostSchema = z.object({ - Title: z.string(), -}) -export type Post = z.infer - -export const PostWithMetaDataSchema = z.object({ - Title: z.string(), - Post: PostSchema, -}) -export type PostWithMetaData = z.infer - -export const UserSchema = z.object({ - Name: z.string(), - Nickname: z.string().nullable(), - Age: z.number(), - Height: z.number(), - OldPostWithMetaData: PostWithMetaDataSchema, - Tags: z.string().array().nullable(), - TagsOptional: z.string().array().optional(), - TagsOptionalNullable: z.string().array().optional().nullable(), - Favourites: z.object({ - Name: z.string(), - }).array().nullable(), - Posts: PostSchema.array().nullable(), - Post: PostSchema, - PostOptional: PostSchema.optional(), - PostOptionalNullable: PostSchema.optional().nullable(), - Metadata: z.record(z.string(), z.string()).nullable(), - MetadataOptional: z.record(z.string(), z.string()).optional(), - MetadataOptionalNullable: z.record(z.string(), z.string()).optional().nullable(), - ExtendedProps: z.any(), - ExtendedPropsOptional: z.any(), - ExtendedPropsNullable: z.any(), - ExtendedPropsOptionalNullable: z.any(), - ExtendedPropsVeryIndirect: z.any(), - NewPostWithMetaData: PostWithMetaDataSchema, - VeryNewPost: PostSchema, - MapWithStruct: z.record(z.string(), PostWithMetaDataSchema).nullable(), -}) -export type User = z.infer - -`, StructToZodSchema(User{})) + assertSchema(t, User{}) } func TestEverythingWithValidations(t *testing.T) { @@ -1883,74 +1223,23 @@ func TestEverythingWithValidations(t *testing.T) { VeryNewPost Post MapWithStruct map[string]PostWithMetaData } - assert.Equal(t, - `export const PostSchema = z.object({ - Title: z.string().min(1), -}) -export type Post = z.infer - -export const PostWithMetaDataSchema = z.object({ - Title: z.string().min(1), - Post: PostSchema, -}) -export type PostWithMetaData = z.infer - -export const UserSchema = z.object({ - Name: z.string().min(1), - Nickname: z.string().nullable(), - Age: z.number().gte(18).refine((val) => val !== 0), - Height: z.number().gte(1.5).refine((val) => val !== 0), - OldPostWithMetaData: PostWithMetaDataSchema, - Tags: z.string().array().min(1), - TagsOptional: z.string().array().optional(), - TagsOptionalNullable: z.string().array().optional().nullable(), - Favourites: z.object({ - Name: z.string().min(1), - }).array().nullable(), - Posts: PostSchema.array(), - Post: PostSchema, - PostOptional: PostSchema.optional(), - PostOptionalNullable: PostSchema.optional().nullable(), - Metadata: z.record(z.string(), z.string()).nullable(), - MetadataLength: z.record(z.string(), z.string()).refine((val) => Object.keys(val).length >= 1, 'Map too small').refine((val) => Object.keys(val).length <= 10, 'Map too large'), - MetadataOptional: z.record(z.string(), z.string()).optional(), - MetadataOptionalNullable: z.record(z.string(), z.string()).optional().nullable(), - ExtendedProps: z.any(), - ExtendedPropsOptional: z.any(), - ExtendedPropsNullable: z.any(), - ExtendedPropsOptionalNullable: z.any(), - ExtendedPropsVeryIndirect: z.any(), - NewPostWithMetaData: PostWithMetaDataSchema, - VeryNewPost: PostSchema, - MapWithStruct: z.record(z.string(), PostWithMetaDataSchema).nullable(), -}) -export type User = z.infer - -`, StructToZodSchema(User{})) + assertSchema(t, User{}) } func TestConvertArray(t *testing.T) { - type Array struct { - Arr [10]string - } - assert.Equal(t, - `export const ArraySchema = z.object({ - Arr: z.string().array().length(10), -}) -export type Array = z.infer - -`, StructToZodSchema(Array{})) - - type MultiArray struct { - Arr [10][20][30]string - } - assert.Equal(t, - `export const MultiArraySchema = z.object({ - Arr: z.string().array().length(30).array().length(20).array().length(10), -}) -export type MultiArray = z.infer + t.Run("single", func(t *testing.T) { + type Array struct { + Arr [10]string + } + assertSchema(t, Array{}) + }) -`, StructToZodSchema(MultiArray{})) + t.Run("multi", func(t *testing.T) { + type MultiArray struct { + Arr [10][20][30]string + } + assertSchema(t, MultiArray{}) + }) } func TestConvertSlice(t *testing.T) { @@ -1967,231 +1256,113 @@ func TestConvertSlice(t *testing.T) { type Whim struct { Wham *Foo } - c := NewConverterWithOpts() + types := []interface{}{ Zip{}, Whim{}, } - assert.Equal(t, - `export const FooSchema = z.object({ - Bar: z.string(), - Baz: z.string(), - Quz: z.string(), -}) -export type Foo = z.infer - -export const ZipSchema = z.object({ - Zap: FooSchema.nullable(), -}) -export type Zip = z.infer - -export const WhimSchema = z.object({ - Wham: FooSchema.nullable(), -}) -export type Whim = z.infer - -`, c.ConvertSlice(types)) + + v3c := NewConverterWithOpts(WithZodV3()) + v4c := NewConverterWithOpts() + v3out := v3c.ConvertSlice(types) + v4out := v4c.ConvertSlice(types) + assert.Equal(t, v3out, v4out) + golden.Assert(t, []byte(v4out)) } func TestConvertSliceWithValidations(t *testing.T) { - type Required struct { - Slice []string `validate:"required"` - } - assert.Equal(t, - `export const RequiredSchema = z.object({ - Slice: z.string().array(), -}) -export type Required = z.infer - -`, StructToZodSchema(Required{})) - - type Min struct { - Slice []string `validate:"min=1"` - } - assert.Equal(t, `export const MinSchema = z.object({ - Slice: z.string().array().min(1), -}) -export type Min = z.infer - -`, StructToZodSchema(Min{})) - - type Max struct { - Slice []string `validate:"max=1"` - } - assert.Equal(t, `export const MaxSchema = z.object({ - Slice: z.string().array().max(1), -}) -export type Max = z.infer - -`, StructToZodSchema(Max{})) - - type Len struct { - Slice []string `validate:"len=1"` - } - assert.Equal(t, `export const LenSchema = z.object({ - Slice: z.string().array().length(1), -}) -export type Len = z.infer - -`, StructToZodSchema(Len{})) - - type Eq struct { - Slice []string `validate:"eq=1"` - } - assert.Equal(t, `export const EqSchema = z.object({ - Slice: z.string().array().length(1), -}) -export type Eq = z.infer - -`, StructToZodSchema(Eq{})) - - type Gt struct { - Slice []string `validate:"gt=1"` - } - assert.Equal(t, `export const GtSchema = z.object({ - Slice: z.string().array().min(2), -}) -export type Gt = z.infer - -`, StructToZodSchema(Gt{})) - - type Gte struct { - Slice []string `validate:"gte=1"` - } - assert.Equal(t, `export const GteSchema = z.object({ - Slice: z.string().array().min(1), -}) -export type Gte = z.infer - -`, StructToZodSchema(Gte{})) - - type Lt struct { - Slice []string `validate:"lt=1"` - } - assert.Equal(t, `export const LtSchema = z.object({ - Slice: z.string().array().max(0), -}) -export type Lt = z.infer - -`, StructToZodSchema(Lt{})) - - type Lte struct { - Slice []string `validate:"lte=1"` - } - assert.Equal(t, `export const LteSchema = z.object({ - Slice: z.string().array().max(1), -}) -export type Lte = z.infer - -`, StructToZodSchema(Lte{})) - - type Ne struct { - Slice []string `validate:"ne=0"` - } - assert.Equal(t, `export const NeSchema = z.object({ - Slice: z.string().array().refine((val) => val.length !== 0), -}) -export type Ne = z.infer - -`, StructToZodSchema(Ne{})) - - assert.Panics(t, func() { - type Bad struct { - Slice []string `validate:"oneof=a b c"` + t.Run("required", func(t *testing.T) { + type Required struct { + Slice []string `validate:"required"` } - StructToZodSchema(Bad{}) + assertSchema(t, Required{}) }) - type Dive1 struct { - Slice [][]string `validate:"dive,required"` - } - assert.Equal(t, `export const Dive1Schema = z.object({ - Slice: z.string().array().array().nullable(), -}) -export type Dive1 = z.infer + t.Run("min", func(t *testing.T) { + type Min struct { + Slice []string `validate:"min=1"` + } + assertSchema(t, Min{}) + }) -`, StructToZodSchema(Dive1{})) + t.Run("max", func(t *testing.T) { + type Max struct { + Slice []string `validate:"max=1"` + } + assertSchema(t, Max{}) + }) - type Dive2 struct { - Slice [][]string `validate:"required,dive,min=1"` - } - assert.Equal(t, `export const Dive2Schema = z.object({ - Slice: z.string().array().min(1).array(), -}) -export type Dive2 = z.infer + t.Run("len", func(t *testing.T) { + type Len struct { + Slice []string `validate:"len=1"` + } + assertSchema(t, Len{}) + }) -`, StructToZodSchema(Dive2{})) -} + t.Run("eq", func(t *testing.T) { + type Eq struct { + Slice []string `validate:"eq=1"` + } + assertSchema(t, Eq{}) + }) -func TestStructTime(t *testing.T) { - type User struct { - Name string - When time.Time - } - assert.Equal(t, - `export const UserSchema = z.object({ - Name: z.string(), - When: z.coerce.date(), -}) -export type User = z.infer - -`, - StructToZodSchema(User{})) -} + t.Run("gt", func(t *testing.T) { + type Gt struct { + Slice []string `validate:"gt=1"` + } + assertSchema(t, Gt{}) + }) -func TestTimeWithRequired(t *testing.T) { - type User struct { - When time.Time `validate:"required"` - } - assert.Equal(t, - `export const UserSchema = z.object({ - When: z.coerce.date().refine((val) => val.getTime() !== new Date('0001-01-01T00:00:00Z').getTime() && val.getTime() !== new Date(0).getTime(), 'Invalid date'), -}) -export type User = z.infer + t.Run("gte", func(t *testing.T) { + type Gte struct { + Slice []string `validate:"gte=1"` + } + assertSchema(t, Gte{}) + }) -`, - StructToZodSchema(User{})) -} + t.Run("lt", func(t *testing.T) { + type Lt struct { + Slice []string `validate:"lt=1"` + } + assertSchema(t, Lt{}) + }) -func TestDuration(t *testing.T) { - type User struct { - HowLong time.Duration - } - assert.Equal(t, - `export const UserSchema = z.object({ - HowLong: z.number(), -}) -export type User = z.infer + t.Run("lte", func(t *testing.T) { + type Lte struct { + Slice []string `validate:"lte=1"` + } + assertSchema(t, Lte{}) + }) -`, - StructToZodSchema(User{})) -} + t.Run("ne", func(t *testing.T) { + type Ne struct { + Slice []string `validate:"ne=0"` + } + assertSchema(t, Ne{}) + }) -func TestCustom(t *testing.T) { - c := NewConverter(map[string]CustomFn{ - "github.com/hypersequent/zen.Decimal": func(c *Converter, t reflect.Type, validate string, i int) string { - return "z.string()" - }, + t.Run("bad_oneof", func(t *testing.T) { + assert.Panics(t, func() { + type Bad struct { + Slice []string `validate:"oneof=a b c"` + } + StructToZodSchema(Bad{}) + }) }) - type Decimal struct { - Value int - Exponent int - } + t.Run("dive1", func(t *testing.T) { + type Dive1 struct { + Slice [][]string `validate:"dive,required"` + } + assertSchema(t, Dive1{}) + }) - type User struct { - Name string - Money Decimal - } - assert.Equal(t, - `export const UserSchema = z.object({ - Name: z.string(), - Money: z.string(), -}) -export type User = z.infer - -`, - c.Convert(User{})) + t.Run("dive2", func(t *testing.T) { + type Dive2 struct { + Slice [][]string `validate:"required,dive,min=1"` + } + assertSchema(t, Dive2{}) + }) } func TestRecursive1(t *testing.T) { @@ -2204,25 +1375,7 @@ func TestRecursive1(t *testing.T) { Children []*NestedItem `json:"children"` } - assert.Equal(t, `export type NestedItem = { - id: number, - title: string, - pos: number, - parent_id: number, - project_id: number, - children: NestedItem[] | null, -} -const NestedItemSchemaShape = { - id: z.number(), - title: z.string(), - pos: z.number(), - parent_id: z.number(), - project_id: z.number(), - children: z.lazy(() => NestedItemSchema).array().nullable(), -} -export const NestedItemSchema: z.ZodType = z.object(NestedItemSchemaShape) - -`, StructToZodSchema(NestedItem{})) + assertSchema(t, NestedItem{}, "v3", "v4") } func TestRecursive2(t *testing.T) { @@ -2235,22 +1388,7 @@ func TestRecursive2(t *testing.T) { Child *Node `json:"child"` } - assert.Equal(t, `export type Node = { - value: number, - next: Node | null, -} -const NodeSchemaShape = { - value: z.number(), - next: z.lazy(() => NodeSchema).nullable(), -} -export const NodeSchema: z.ZodType = z.object(NodeSchemaShape) - -export const ParentSchema = z.object({ - child: NodeSchema.nullable(), -}) -export type Parent = z.infer - -`, StructToZodSchema(Parent{})) + assertSchema(t, Parent{}, "v3", "v4") } type TestCyclicA struct { @@ -2283,24 +1421,16 @@ func TestGenerics(t *testing.T) { c.AddType(StringIntPair{}) c.AddType(GenericPair[int, bool]{}) c.AddType(PairMap[string, int, bool]{}) - assert.Equal(t, `export const StringIntPairSchema = z.object({ - First: z.string(), - Second: z.number(), -}) -export type StringIntPair = z.infer - -export const GenericPairIntBoolSchema = z.object({ - First: z.number(), - Second: z.boolean(), -}) -export type GenericPairIntBool = z.infer - -export const PairMapStringIntBoolSchema = z.object({ - items: z.record(z.string(), GenericPairIntBoolSchema).nullable(), -}) -export type PairMapStringIntBool = z.infer - -`, c.Export()) + + v3c := NewConverterWithOpts(WithZodV3()) + v3c.AddType(StringIntPair{}) + v3c.AddType(GenericPair[int, bool]{}) + v3c.AddType(PairMap[string, int, bool]{}) + + v3out := v3c.Export() + v4out := c.Export() + assert.Equal(t, v3out, v4out) + golden.Assert(t, []byte(v4out)) } func TestSliceFields(t *testing.T) { @@ -2314,18 +1444,7 @@ func TestSliceFields(t *testing.T) { JSONMinOmitEmpty []int `json:",omitempty" validate:"min=1,omitempty"` } - assert.Equal(t, `export const TestSliceFieldsStructSchema = z.object({ - NoValidate: z.number().array().nullable(), - Required: z.number().array(), - Min: z.number().array().min(1), - OmitEmpty: z.number().array().nullable(), - JSONOmitEmpty: z.number().array().optional(), - MinOmitEmpty: z.number().array().min(1).nullable(), - JSONMinOmitEmpty: z.number().array().min(1).optional(), -}) -export type TestSliceFieldsStruct = z.infer - -`, StructToZodSchema(TestSliceFieldsStruct{})) + assertSchema(t, TestSliceFieldsStruct{}) } func TestCustomTag(t *testing.T) { @@ -2359,22 +1478,12 @@ func TestCustomTag(t *testing.T) { }, } - assert.Equal(t, `export const SortParamsSchema = z.object({ - order: z.enum(["asc", "desc"] as const).optional(), - field: z.string().optional(), -}) -export type SortParams = z.infer - -export const RequestSchema = z.object({ - PaginationParams: z.object({ - start: z.number().gt(0).optional(), - end: z.number().gt(0).optional(), - }).refine((val) => !val.start || !val.end || val.start < val.end, 'Start should be less than end'), - search: z.string().refine((val) => !val || /^[a-z0-9_]*$/.test(val), 'Invalid search identifier').optional(), -}).merge(SortParamsSchema.extend({field: z.enum(['title', 'address', 'age', 'dob'])})) -export type Request = z.infer - -`, NewConverterWithOpts(WithCustomTags(customTagHandlers), WithZodV3()).Convert(Request{})) + t.Run("v3", func(t *testing.T) { + golden.Assert(t, []byte(NewConverterWithOpts(WithCustomTags(customTagHandlers), WithZodV3()).Convert(Request{}))) + }) + t.Run("v4", func(t *testing.T) { + golden.Assert(t, []byte(NewConverterWithOpts(WithCustomTags(customTagHandlers)).Convert(Request{}))) + }) } func TestRecursiveEmbeddedStruct(t *testing.T) { @@ -2405,54 +1514,26 @@ func TestRecursiveEmbeddedStruct(t *testing.T) { ItemE } - c := NewConverterWithOpts(WithZodV3()) - c.AddType(ItemA{}) - c.AddType(ItemB{}) - c.AddType(ItemC{}) - c.AddType(ItemD{}) - c.AddType(ItemE{}) - c.AddType(ItemF{}) - - assert.Equal(t, `export type ItemA = { - Name: string, - Children: ItemA[] | null, -} -const ItemASchemaShape = { - Name: z.string(), - Children: z.lazy(() => ItemASchema).array().nullable(), -} -export const ItemASchema: z.ZodType = z.object(ItemASchemaShape) - -export const ItemBSchema = z.object({ - ...ItemASchemaShape, -}) -export type ItemB = z.infer - -export const ItemCSchema = z.object({ -}).merge(ItemBSchema) -export type ItemC = z.infer - -export const ItemDSchema = z.object({ - ItemA: ItemASchema, -}) -export type ItemD = z.infer - -export type ItemE = ItemA & ItemD & { - Children: ItemE[] | null, -} -const ItemESchemaShape = { - ...ItemASchemaShape, - ...ItemDSchema.shape, - Children: z.lazy(() => ItemESchema).array().nullable(), -} -export const ItemESchema: z.ZodType = z.object(ItemESchemaShape) - -export const ItemFSchema = z.object({ - ...ItemESchemaShape, -}) -export type ItemF = z.infer - -`, c.Export()) + t.Run("v3", func(t *testing.T) { + c := NewConverterWithOpts(WithZodV3()) + c.AddType(ItemA{}) + c.AddType(ItemB{}) + c.AddType(ItemC{}) + c.AddType(ItemD{}) + c.AddType(ItemE{}) + c.AddType(ItemF{}) + golden.Assert(t, []byte(c.Export())) + }) + t.Run("v4", func(t *testing.T) { + c := NewConverterWithOpts() + c.AddType(ItemA{}) + c.AddType(ItemB{}) + c.AddType(ItemC{}) + c.AddType(ItemD{}) + c.AddType(ItemE{}) + c.AddType(ItemF{}) + golden.Assert(t, []byte(c.Export())) + }) } func TestRecursiveEmbeddedWithPointersAndDates(t *testing.T) { @@ -2468,25 +1549,7 @@ func TestRecursiveEmbeddedWithPointersAndDates(t *testing.T) { UpdatedAt time.Time } - assert.Equal(t, `export type TreeNode = { - Value: string, - CreatedAt: Date, - Children: TreeNode[] | null, -} -const TreeNodeSchemaShape = { - Value: z.string(), - CreatedAt: z.coerce.date(), - Children: z.lazy(() => TreeNodeSchema).array().nullable(), -} -export const TreeNodeSchema: z.ZodType = z.object(TreeNodeSchemaShape) - -export const TreeSchema = z.object({ - ...TreeNodeSchemaShape, - UpdatedAt: z.coerce.date(), -}) -export type Tree = z.infer - -`, StructToZodSchema(Tree{}, WithZodV3())) + assertSchema(t, Tree{}, "v3", "v4") }) t.Run("embedded struct with pointer to self and date", func(t *testing.T) { @@ -2501,24 +1564,6 @@ export type Tree = z.infer Title string } - assert.Equal(t, `export type Comment = { - Text: string, - Timestamp: Date, - Reply: Comment | null, -} -const CommentSchemaShape = { - Text: z.string(), - Timestamp: z.coerce.date(), - Reply: z.lazy(() => CommentSchema).nullable(), -} -export const CommentSchema: z.ZodType = z.object(CommentSchemaShape) - -export const ArticleSchema = z.object({ - ...CommentSchemaShape, - Title: z.string(), -}) -export type Article = z.infer - -`, StructToZodSchema(Article{}, WithZodV3())) + assertSchema(t, Article{}, "v3", "v4") }) } From 695a26b642a61b3ab9cf99783ad8aaaeb0c9a317 Mon Sep 17 00:00:00 2001 From: Ramil Amparo Date: Wed, 1 Apr 2026 19:36:25 +0400 Subject: [PATCH 03/35] Add docker typechecks and fix issues --- .github/workflows/ci.yml | 3 + Makefile | 5 +- docker-typecheck.sh | 127 ++++++++++++++++++ testdata/TestConvertArray/multi.golden | 1 + testdata/TestConvertArray/single.golden | 1 + testdata/TestConvertSlice.golden | 1 + .../dive1.golden | 1 + .../dive2.golden | 1 + .../TestConvertSliceWithValidations/eq.golden | 1 + .../TestConvertSliceWithValidations/gt.golden | 1 + .../gte.golden | 1 + .../len.golden | 1 + .../TestConvertSliceWithValidations/lt.golden | 1 + .../lte.golden | 1 + .../max.golden | 1 + .../min.golden | 1 + .../TestConvertSliceWithValidations/ne.golden | 1 + .../required.golden | 1 + testdata/TestCustom.golden | 1 + testdata/TestCustomTag/v3.golden | 2 + testdata/TestCustomTag/v4.golden | 2 + testdata/TestDuration.golden | 1 + testdata/TestEverything.golden | 1 + testdata/TestEverythingWithValidations.golden | 1 + testdata/TestGenerics.golden | 1 + testdata/TestInterfaceAny.golden | 1 + testdata/TestInterfaceEmptyAny.golden | 1 + testdata/TestInterfacePointerAny.golden | 1 + testdata/TestInterfacePointerEmptyAny.golden | 1 + testdata/TestMapStringToInterface.golden | 1 + testdata/TestMapStringToString.golden | 1 + .../TestMapWithNonStringKey/float_key.golden | 1 + .../TestMapWithNonStringKey/int_key.golden | 1 + .../TestMapWithNonStringKey/time_key.golden | 3 +- testdata/TestMapWithStruct.golden | 1 + testdata/TestMapWithValidations/dive1.golden | 1 + testdata/TestMapWithValidations/dive2.golden | 1 + testdata/TestMapWithValidations/dive3.golden | 1 + testdata/TestMapWithValidations/eq.golden | 1 + testdata/TestMapWithValidations/gt.golden | 1 + testdata/TestMapWithValidations/gte.golden | 1 + testdata/TestMapWithValidations/len.golden | 1 + testdata/TestMapWithValidations/lt.golden | 1 + testdata/TestMapWithValidations/lte.golden | 1 + testdata/TestMapWithValidations/max.golden | 1 + testdata/TestMapWithValidations/min.golden | 1 + testdata/TestMapWithValidations/minmax.golden | 1 + testdata/TestMapWithValidations/ne.golden | 1 + .../TestMapWithValidations/required.golden | 1 + testdata/TestNestedStruct/v3.golden | 2 + testdata/TestNestedStruct/v4.golden | 2 + testdata/TestNullableWithValidations.golden | 1 + testdata/TestNumberValidations/eq.golden | 1 + testdata/TestNumberValidations/gt_lt.golden | 1 + testdata/TestNumberValidations/gte_lte.golden | 1 + testdata/TestNumberValidations/len.golden | 1 + testdata/TestNumberValidations/min_max.golden | 1 + testdata/TestNumberValidations/ne.golden | 1 + testdata/TestNumberValidations/oneof.golden | 1 + testdata/TestRecursive1/v3.golden | 2 + testdata/TestRecursive1/v4.golden | 2 + testdata/TestRecursive2/v3.golden | 2 + testdata/TestRecursive2/v4.golden | 2 + .../TestRecursiveEmbeddedStruct/v3.golden | 4 +- .../TestRecursiveEmbeddedStruct/v4.golden | 4 +- .../v3.golden | 2 + .../v4.golden | 2 + .../v3.golden | 2 + .../v4.golden | 2 + testdata/TestSliceFields.golden | 1 + testdata/TestStringArray.golden | 1 + testdata/TestStringArrayNullable.golden | 1 + testdata/TestStringNestedArray.golden | 1 + testdata/TestStringNullable.golden | 1 + testdata/TestStringOptional.golden | 1 + testdata/TestStringOptionalNotNullable.golden | 1 + testdata/TestStringOptionalNullable.golden | 1 + testdata/TestStringValidations/alpha.golden | 1 + .../TestStringValidations/alphanum.golden | 1 + .../alphanumunicode.golden | 3 +- .../TestStringValidations/alphaunicode.golden | 3 +- testdata/TestStringValidations/ascii.golden | Bin 128 -> 142 bytes .../TestStringValidations/base64/v3.golden | 2 + .../TestStringValidations/base64/v4.golden | 2 + testdata/TestStringValidations/boolean.golden | 1 + .../TestStringValidations/contains.golden | 1 + .../TestStringValidations/datetime/v3.golden | 2 + .../TestStringValidations/datetime/v4.golden | 2 + .../TestStringValidations/email/v3.golden | 2 + .../TestStringValidations/email/v4.golden | 2 + .../TestStringValidations/endswith.golden | 1 + testdata/TestStringValidations/eq.golden | 1 + testdata/TestStringValidations/gt.golden | 1 + testdata/TestStringValidations/gte.golden | 1 + .../hexadecimal/v3.golden | 2 + .../hexadecimal/v4.golden | 2 + .../TestStringValidations/http_url/v3.golden | 2 + .../TestStringValidations/http_url/v4.golden | 2 + testdata/TestStringValidations/ip/v3.golden | 2 + testdata/TestStringValidations/ip/v4.golden | 2 + .../TestStringValidations/ip4_addr/v3.golden | 2 + .../TestStringValidations/ip4_addr/v4.golden | 2 + .../TestStringValidations/ip6_addr/v3.golden | 2 + .../TestStringValidations/ip6_addr/v4.golden | 2 + .../TestStringValidations/ip_addr/v3.golden | 2 + .../TestStringValidations/ip_addr/v4.golden | 2 + testdata/TestStringValidations/ipv4/v3.golden | 2 + testdata/TestStringValidations/ipv4/v4.golden | 2 + testdata/TestStringValidations/ipv6/v3.golden | 2 + testdata/TestStringValidations/ipv6/v4.golden | 2 + testdata/TestStringValidations/json.golden | 1 + .../TestStringValidations/latitude.golden | 1 + testdata/TestStringValidations/len.golden | 1 + .../TestStringValidations/longitude.golden | 1 + .../TestStringValidations/lowercase.golden | 1 + testdata/TestStringValidations/lt.golden | 1 + testdata/TestStringValidations/lte.golden | 1 + testdata/TestStringValidations/max.golden | 1 + testdata/TestStringValidations/md4.golden | 1 + testdata/TestStringValidations/md5/v3.golden | 2 + testdata/TestStringValidations/md5/v4.golden | 2 + testdata/TestStringValidations/min.golden | 1 + testdata/TestStringValidations/minmax.golden | 1 + testdata/TestStringValidations/mongodb.golden | 1 + testdata/TestStringValidations/ne.golden | 1 + testdata/TestStringValidations/number.golden | 1 + testdata/TestStringValidations/numeric.golden | 1 + testdata/TestStringValidations/oneof.golden | 1 + .../oneof_separated.golden | 1 + .../TestStringValidations/required.golden | 1 + .../TestStringValidations/sha256/v3.golden | 2 + .../TestStringValidations/sha256/v4.golden | 2 + .../TestStringValidations/sha384/v3.golden | 2 + .../TestStringValidations/sha384/v4.golden | 2 + .../TestStringValidations/sha512/v3.golden | 2 + .../TestStringValidations/sha512/v4.golden | 2 + .../TestStringValidations/startswith.golden | 1 + .../TestStringValidations/uppercase.golden | 1 + testdata/TestStringValidations/url/v3.golden | 2 + testdata/TestStringValidations/url/v4.golden | 2 + .../TestStringValidations/url_encoded.golden | 1 + testdata/TestStringValidations/uuid/v3.golden | 2 + testdata/TestStringValidations/uuid/v4.golden | 2 + .../TestStringValidations/uuid3/v3.golden | 2 + .../TestStringValidations/uuid3/v4.golden | 2 + .../uuid3_rfc4122/v3.golden | 2 + .../uuid3_rfc4122/v4.golden | 2 + .../TestStringValidations/uuid4/v3.golden | 2 + .../TestStringValidations/uuid4/v4.golden | 2 + .../uuid4_rfc4122/v3.golden | 2 + .../uuid4_rfc4122/v4.golden | 2 + .../TestStringValidations/uuid5/v3.golden | 2 + .../TestStringValidations/uuid5/v4.golden | 2 + .../uuid5_rfc4122/v3.golden | 2 + .../uuid5_rfc4122/v4.golden | 2 + .../uuid_rfc4122/v3.golden | 2 + .../uuid_rfc4122/v4.golden | 2 + testdata/TestStructSimple.golden | 1 + testdata/TestStructSimplePrefix.golden | 1 + .../TestStructSimpleWithOmittedField.golden | 1 + testdata/TestStructSlice.golden | 1 + testdata/TestStructSliceOptional.golden | 1 + .../TestStructSliceOptionalNullable.golden | 1 + testdata/TestStructTime.golden | 1 + testdata/TestTimeWithRequired.golden | 1 + .../embedded_structs_use_shape_spreads.golden | 2 + ...m_keyed_maps_become_partial_records.golden | 2 + ...alls_back_to_legacy_chain_semantics.golden | 4 +- ..._inherit_generic_string_constraints.golden | 2 + .../v3.golden | 7 + .../v4.golden | 7 + ...s_precedence_over_ip_specialization.golden | 2 + ...es_keep_named_fields_before_spreads.golden | 2 + ..._encounter_order_for_duplicate_keys.golden | 4 +- .../string_formats_use_zod_v4_builders.golden | 2 + ..._preserved_around_v4_format_helpers.golden | 2 + zod.go | 55 ++++++-- zod_test.go | 74 ++++++++-- 178 files changed, 503 insertions(+), 30 deletions(-) create mode 100755 docker-typecheck.sh create mode 100644 testdata/TestZodV4Defaults/ip_unions_work_when_chain_constraints_precede_ip_tag/v3.golden create mode 100644 testdata/TestZodV4Defaults/ip_unions_work_when_chain_constraints_precede_ip_tag/v4.golden diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6f85e80..8010cc7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -43,3 +43,6 @@ jobs: - name: Test run: make test + + - name: Type-check golden files + run: make typecheck diff --git a/Makefile b/Makefile index 2ddee02..ea02ca3 100644 --- a/Makefile +++ b/Makefile @@ -16,7 +16,10 @@ test-update: GOLDEN_UPDATE=true $(GOCMD) test ./... +typecheck: + ./docker-typecheck.sh + bench: $(GOCMD) test -bench=. -benchmem ./... -.PHONY: test test-update lint linters-install bench +.PHONY: test test-update lint linters-install typecheck bench diff --git a/docker-typecheck.sh b/docker-typecheck.sh new file mode 100755 index 0000000..1872fbe --- /dev/null +++ b/docker-typecheck.sh @@ -0,0 +1,127 @@ +#!/bin/bash +# +# Type-checks golden files against the correct zod version inside Docker. +# +# Golden files must contain these metadata comments to be included: +# // @typecheck — present by default; files without it are skipped +# // @zod-version: v3|v4 — (optional) restrict to one zod major; omit for both +# +# Usage: +# ./typecheck/docker-typecheck.sh + +set -euo pipefail + +PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +echo "========================================" +echo "Golden File Type-Check (Docker)" +echo "========================================" +echo "" + +docker run --rm \ + -v "${PROJECT_DIR}/testdata:/golden:ro" \ + node:22-alpine \ + sh -c ' +set -e + +mkdir -p /test/zod3 /test/zod4 + +zod3_count=0 +zod4_count=0 + +for file in $(find /golden -name "*.golden" -type f); do + # Only process files with @typecheck metadata + head -5 "$file" | grep -q "^// @typecheck" || continue + + # Extract zod version from metadata (empty = both) + version=$(sed -n "s|^// @zod-version: ||p" "$file" | head -1) + + # Build unique .ts filename from relative path + relpath="${file#/golden/}" + ts_name="$(echo "$relpath" | sed "s|/|__|g; s|\.golden$|.ts|")" + + prepare_ts() { + printf "import { z } from \"zod\";\n" > "$1" + sed "/^\/\/ @/d" "$file" >> "$1" + } + + case "${version}" in + v3) + prepare_ts "/test/zod3/${ts_name}" + zod3_count=$((zod3_count + 1)) + ;; + v4) + prepare_ts "/test/zod4/${ts_name}" + zod4_count=$((zod4_count + 1)) + ;; + *) + prepare_ts "/test/zod3/${ts_name}" + prepare_ts "/test/zod4/${ts_name}" + zod3_count=$((zod3_count + 1)) + zod4_count=$((zod4_count + 1)) + ;; + esac +done + +echo "Found ${zod3_count} files for zod@3, ${zod4_count} files for zod@4" +echo "" + +for dir in zod3 zod4; do + cat > "/test/${dir}/tsconfig.json" < /test/zod3/package.json < /test/zod4/package.json <&1 + npx tsc --noEmit + + echo "" + echo "✓ ${label}: PASSED" + echo "" +done + +echo "========================================" +echo "All type checks passed!" +echo "========================================" +' diff --git a/testdata/TestConvertArray/multi.golden b/testdata/TestConvertArray/multi.golden index fe8da34..d4d026f 100644 --- a/testdata/TestConvertArray/multi.golden +++ b/testdata/TestConvertArray/multi.golden @@ -1,3 +1,4 @@ +// @typecheck export const MultiArraySchema = z.object({ Arr: z.string().array().length(30).array().length(20).array().length(10), }) diff --git a/testdata/TestConvertArray/single.golden b/testdata/TestConvertArray/single.golden index 4864938..1136a4d 100644 --- a/testdata/TestConvertArray/single.golden +++ b/testdata/TestConvertArray/single.golden @@ -1,3 +1,4 @@ +// @typecheck export const ArraySchema = z.object({ Arr: z.string().array().length(10), }) diff --git a/testdata/TestConvertSlice.golden b/testdata/TestConvertSlice.golden index 37ce66d..56fde4e 100644 --- a/testdata/TestConvertSlice.golden +++ b/testdata/TestConvertSlice.golden @@ -1,3 +1,4 @@ +// @typecheck export const FooSchema = z.object({ Bar: z.string(), Baz: z.string(), diff --git a/testdata/TestConvertSliceWithValidations/dive1.golden b/testdata/TestConvertSliceWithValidations/dive1.golden index 562945f..ea90650 100644 --- a/testdata/TestConvertSliceWithValidations/dive1.golden +++ b/testdata/TestConvertSliceWithValidations/dive1.golden @@ -1,3 +1,4 @@ +// @typecheck export const Dive1Schema = z.object({ Slice: z.string().array().array().nullable(), }) diff --git a/testdata/TestConvertSliceWithValidations/dive2.golden b/testdata/TestConvertSliceWithValidations/dive2.golden index 71b0b45..adef972 100644 --- a/testdata/TestConvertSliceWithValidations/dive2.golden +++ b/testdata/TestConvertSliceWithValidations/dive2.golden @@ -1,3 +1,4 @@ +// @typecheck export const Dive2Schema = z.object({ Slice: z.string().array().min(1).array(), }) diff --git a/testdata/TestConvertSliceWithValidations/eq.golden b/testdata/TestConvertSliceWithValidations/eq.golden index 876d632..384b49a 100644 --- a/testdata/TestConvertSliceWithValidations/eq.golden +++ b/testdata/TestConvertSliceWithValidations/eq.golden @@ -1,3 +1,4 @@ +// @typecheck export const EqSchema = z.object({ Slice: z.string().array().length(1), }) diff --git a/testdata/TestConvertSliceWithValidations/gt.golden b/testdata/TestConvertSliceWithValidations/gt.golden index 32ee0e5..16f1329 100644 --- a/testdata/TestConvertSliceWithValidations/gt.golden +++ b/testdata/TestConvertSliceWithValidations/gt.golden @@ -1,3 +1,4 @@ +// @typecheck export const GtSchema = z.object({ Slice: z.string().array().min(2), }) diff --git a/testdata/TestConvertSliceWithValidations/gte.golden b/testdata/TestConvertSliceWithValidations/gte.golden index 787660f..263ba9c 100644 --- a/testdata/TestConvertSliceWithValidations/gte.golden +++ b/testdata/TestConvertSliceWithValidations/gte.golden @@ -1,3 +1,4 @@ +// @typecheck export const GteSchema = z.object({ Slice: z.string().array().min(1), }) diff --git a/testdata/TestConvertSliceWithValidations/len.golden b/testdata/TestConvertSliceWithValidations/len.golden index 34b5a5a..c52a3cf 100644 --- a/testdata/TestConvertSliceWithValidations/len.golden +++ b/testdata/TestConvertSliceWithValidations/len.golden @@ -1,3 +1,4 @@ +// @typecheck export const LenSchema = z.object({ Slice: z.string().array().length(1), }) diff --git a/testdata/TestConvertSliceWithValidations/lt.golden b/testdata/TestConvertSliceWithValidations/lt.golden index ea59a03..5c89d96 100644 --- a/testdata/TestConvertSliceWithValidations/lt.golden +++ b/testdata/TestConvertSliceWithValidations/lt.golden @@ -1,3 +1,4 @@ +// @typecheck export const LtSchema = z.object({ Slice: z.string().array().max(0), }) diff --git a/testdata/TestConvertSliceWithValidations/lte.golden b/testdata/TestConvertSliceWithValidations/lte.golden index 93f447b..cef88a9 100644 --- a/testdata/TestConvertSliceWithValidations/lte.golden +++ b/testdata/TestConvertSliceWithValidations/lte.golden @@ -1,3 +1,4 @@ +// @typecheck export const LteSchema = z.object({ Slice: z.string().array().max(1), }) diff --git a/testdata/TestConvertSliceWithValidations/max.golden b/testdata/TestConvertSliceWithValidations/max.golden index bda8947..dd94985 100644 --- a/testdata/TestConvertSliceWithValidations/max.golden +++ b/testdata/TestConvertSliceWithValidations/max.golden @@ -1,3 +1,4 @@ +// @typecheck export const MaxSchema = z.object({ Slice: z.string().array().max(1), }) diff --git a/testdata/TestConvertSliceWithValidations/min.golden b/testdata/TestConvertSliceWithValidations/min.golden index 1453c63..79c069f 100644 --- a/testdata/TestConvertSliceWithValidations/min.golden +++ b/testdata/TestConvertSliceWithValidations/min.golden @@ -1,3 +1,4 @@ +// @typecheck export const MinSchema = z.object({ Slice: z.string().array().min(1), }) diff --git a/testdata/TestConvertSliceWithValidations/ne.golden b/testdata/TestConvertSliceWithValidations/ne.golden index dfd7144..00208f2 100644 --- a/testdata/TestConvertSliceWithValidations/ne.golden +++ b/testdata/TestConvertSliceWithValidations/ne.golden @@ -1,3 +1,4 @@ +// @typecheck export const NeSchema = z.object({ Slice: z.string().array().refine((val) => val.length !== 0), }) diff --git a/testdata/TestConvertSliceWithValidations/required.golden b/testdata/TestConvertSliceWithValidations/required.golden index 677595f..bdbea1f 100644 --- a/testdata/TestConvertSliceWithValidations/required.golden +++ b/testdata/TestConvertSliceWithValidations/required.golden @@ -1,3 +1,4 @@ +// @typecheck export const RequiredSchema = z.object({ Slice: z.string().array(), }) diff --git a/testdata/TestCustom.golden b/testdata/TestCustom.golden index 515dd52..9e58894 100644 --- a/testdata/TestCustom.golden +++ b/testdata/TestCustom.golden @@ -1,3 +1,4 @@ +// @typecheck export const UserSchema = z.object({ Name: z.string(), Money: z.string(), diff --git a/testdata/TestCustomTag/v3.golden b/testdata/TestCustomTag/v3.golden index b345d45..1891285 100644 --- a/testdata/TestCustomTag/v3.golden +++ b/testdata/TestCustomTag/v3.golden @@ -1,3 +1,5 @@ +// @zod-version: v3 +// @typecheck export const SortParamsSchema = z.object({ order: z.enum(["asc", "desc"] as const).optional(), field: z.string().optional(), diff --git a/testdata/TestCustomTag/v4.golden b/testdata/TestCustomTag/v4.golden index 3a03607..8f82532 100644 --- a/testdata/TestCustomTag/v4.golden +++ b/testdata/TestCustomTag/v4.golden @@ -1,3 +1,5 @@ +// @zod-version: v4 +// @typecheck export const SortParamsSchema = z.object({ order: z.enum(["asc", "desc"] as const).optional(), field: z.string().optional(), diff --git a/testdata/TestDuration.golden b/testdata/TestDuration.golden index 7504872..a99c96c 100644 --- a/testdata/TestDuration.golden +++ b/testdata/TestDuration.golden @@ -1,3 +1,4 @@ +// @typecheck export const UserSchema = z.object({ HowLong: z.number(), }) diff --git a/testdata/TestEverything.golden b/testdata/TestEverything.golden index c64ee31..836c9b1 100644 --- a/testdata/TestEverything.golden +++ b/testdata/TestEverything.golden @@ -1,3 +1,4 @@ +// @typecheck export const PostSchema = z.object({ Title: z.string(), }) diff --git a/testdata/TestEverythingWithValidations.golden b/testdata/TestEverythingWithValidations.golden index c7fc1c5..7b837ec 100644 --- a/testdata/TestEverythingWithValidations.golden +++ b/testdata/TestEverythingWithValidations.golden @@ -1,3 +1,4 @@ +// @typecheck export const PostSchema = z.object({ Title: z.string().min(1), }) diff --git a/testdata/TestGenerics.golden b/testdata/TestGenerics.golden index df79dfd..1d9da4b 100644 --- a/testdata/TestGenerics.golden +++ b/testdata/TestGenerics.golden @@ -1,3 +1,4 @@ +// @typecheck export const StringIntPairSchema = z.object({ First: z.string(), Second: z.number(), diff --git a/testdata/TestInterfaceAny.golden b/testdata/TestInterfaceAny.golden index 3f83cc0..091f4dd 100644 --- a/testdata/TestInterfaceAny.golden +++ b/testdata/TestInterfaceAny.golden @@ -1,3 +1,4 @@ +// @typecheck export const UserSchema = z.object({ Name: z.string(), Metadata: z.any(), diff --git a/testdata/TestInterfaceEmptyAny.golden b/testdata/TestInterfaceEmptyAny.golden index 3f83cc0..091f4dd 100644 --- a/testdata/TestInterfaceEmptyAny.golden +++ b/testdata/TestInterfaceEmptyAny.golden @@ -1,3 +1,4 @@ +// @typecheck export const UserSchema = z.object({ Name: z.string(), Metadata: z.any(), diff --git a/testdata/TestInterfacePointerAny.golden b/testdata/TestInterfacePointerAny.golden index 3f83cc0..091f4dd 100644 --- a/testdata/TestInterfacePointerAny.golden +++ b/testdata/TestInterfacePointerAny.golden @@ -1,3 +1,4 @@ +// @typecheck export const UserSchema = z.object({ Name: z.string(), Metadata: z.any(), diff --git a/testdata/TestInterfacePointerEmptyAny.golden b/testdata/TestInterfacePointerEmptyAny.golden index 3f83cc0..091f4dd 100644 --- a/testdata/TestInterfacePointerEmptyAny.golden +++ b/testdata/TestInterfacePointerEmptyAny.golden @@ -1,3 +1,4 @@ +// @typecheck export const UserSchema = z.object({ Name: z.string(), Metadata: z.any(), diff --git a/testdata/TestMapStringToInterface.golden b/testdata/TestMapStringToInterface.golden index 36c00f8..abc7d79 100644 --- a/testdata/TestMapStringToInterface.golden +++ b/testdata/TestMapStringToInterface.golden @@ -1,3 +1,4 @@ +// @typecheck export const UserSchema = z.object({ Name: z.string(), Metadata: z.record(z.string(), z.any()).nullable(), diff --git a/testdata/TestMapStringToString.golden b/testdata/TestMapStringToString.golden index 43d1a57..31fec50 100644 --- a/testdata/TestMapStringToString.golden +++ b/testdata/TestMapStringToString.golden @@ -1,3 +1,4 @@ +// @typecheck export const UserSchema = z.object({ Name: z.string(), Metadata: z.record(z.string(), z.string()).nullable(), diff --git a/testdata/TestMapWithNonStringKey/float_key.golden b/testdata/TestMapWithNonStringKey/float_key.golden index 0c0143c..02d494a 100644 --- a/testdata/TestMapWithNonStringKey/float_key.golden +++ b/testdata/TestMapWithNonStringKey/float_key.golden @@ -1,3 +1,4 @@ +// @typecheck export const Map3Schema = z.object({ Name: z.string(), Metadata: z.record(z.coerce.number(), z.string()).nullable(), diff --git a/testdata/TestMapWithNonStringKey/int_key.golden b/testdata/TestMapWithNonStringKey/int_key.golden index 94d603e..8ee1192 100644 --- a/testdata/TestMapWithNonStringKey/int_key.golden +++ b/testdata/TestMapWithNonStringKey/int_key.golden @@ -1,3 +1,4 @@ +// @typecheck export const Map1Schema = z.object({ Name: z.string(), Metadata: z.record(z.coerce.number(), z.string()).nullable(), diff --git a/testdata/TestMapWithNonStringKey/time_key.golden b/testdata/TestMapWithNonStringKey/time_key.golden index efad40a..e74453d 100644 --- a/testdata/TestMapWithNonStringKey/time_key.golden +++ b/testdata/TestMapWithNonStringKey/time_key.golden @@ -1,6 +1,7 @@ +// @typecheck export const Map2Schema = z.object({ Name: z.string(), - Metadata: z.record(z.coerce.date(), z.string()).nullable(), + Metadata: z.record(z.string(), z.string()).nullable(), }) export type Map2 = z.infer diff --git a/testdata/TestMapWithStruct.golden b/testdata/TestMapWithStruct.golden index 1f49715..0fe0476 100644 --- a/testdata/TestMapWithStruct.golden +++ b/testdata/TestMapWithStruct.golden @@ -1,3 +1,4 @@ +// @typecheck export const PostWithMetaDataSchema = z.object({ Title: z.string(), }) diff --git a/testdata/TestMapWithValidations/dive1.golden b/testdata/TestMapWithValidations/dive1.golden index 67e5c10..5d2a240 100644 --- a/testdata/TestMapWithValidations/dive1.golden +++ b/testdata/TestMapWithValidations/dive1.golden @@ -1,3 +1,4 @@ +// @typecheck export const Dive1Schema = z.object({ Map: z.record(z.string(), z.string().refine((val) => [...val].length >= 2, 'String must contain at least 2 character(s)')).nullable(), }) diff --git a/testdata/TestMapWithValidations/dive2.golden b/testdata/TestMapWithValidations/dive2.golden index 00143a5..9b95249 100644 --- a/testdata/TestMapWithValidations/dive2.golden +++ b/testdata/TestMapWithValidations/dive2.golden @@ -1,3 +1,4 @@ +// @typecheck export const Dive2Schema = z.object({ 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(), }) diff --git a/testdata/TestMapWithValidations/dive3.golden b/testdata/TestMapWithValidations/dive3.golden index c10a8aa..ad0814b 100644 --- a/testdata/TestMapWithValidations/dive3.golden +++ b/testdata/TestMapWithValidations/dive3.golden @@ -1,3 +1,4 @@ +// @typecheck export const Dive3Schema = z.object({ 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(), }) diff --git a/testdata/TestMapWithValidations/eq.golden b/testdata/TestMapWithValidations/eq.golden index c6b20a4..f0d4847 100644 --- a/testdata/TestMapWithValidations/eq.golden +++ b/testdata/TestMapWithValidations/eq.golden @@ -1,3 +1,4 @@ +// @typecheck export const EqSchema = z.object({ Map: z.record(z.string(), z.string()).refine((val) => Object.keys(val).length === 1, 'Map wrong size'), }) diff --git a/testdata/TestMapWithValidations/gt.golden b/testdata/TestMapWithValidations/gt.golden index d68285e..af73f29 100644 --- a/testdata/TestMapWithValidations/gt.golden +++ b/testdata/TestMapWithValidations/gt.golden @@ -1,3 +1,4 @@ +// @typecheck export const GtSchema = z.object({ Map: z.record(z.string(), z.string()).refine((val) => Object.keys(val).length > 1, 'Map too small'), }) diff --git a/testdata/TestMapWithValidations/gte.golden b/testdata/TestMapWithValidations/gte.golden index eacbbd3..5f6ac4d 100644 --- a/testdata/TestMapWithValidations/gte.golden +++ b/testdata/TestMapWithValidations/gte.golden @@ -1,3 +1,4 @@ +// @typecheck export const GteSchema = z.object({ Map: z.record(z.string(), z.string()).refine((val) => Object.keys(val).length >= 1, 'Map too small'), }) diff --git a/testdata/TestMapWithValidations/len.golden b/testdata/TestMapWithValidations/len.golden index 7640ab2..06d6ba0 100644 --- a/testdata/TestMapWithValidations/len.golden +++ b/testdata/TestMapWithValidations/len.golden @@ -1,3 +1,4 @@ +// @typecheck export const LenSchema = z.object({ Map: z.record(z.string(), z.string()).refine((val) => Object.keys(val).length === 1, 'Map wrong size'), }) diff --git a/testdata/TestMapWithValidations/lt.golden b/testdata/TestMapWithValidations/lt.golden index b6b534b..7479753 100644 --- a/testdata/TestMapWithValidations/lt.golden +++ b/testdata/TestMapWithValidations/lt.golden @@ -1,3 +1,4 @@ +// @typecheck export const LtSchema = z.object({ Map: z.record(z.string(), z.string()).refine((val) => Object.keys(val).length < 1, 'Map too large'), }) diff --git a/testdata/TestMapWithValidations/lte.golden b/testdata/TestMapWithValidations/lte.golden index de55553..c91dba6 100644 --- a/testdata/TestMapWithValidations/lte.golden +++ b/testdata/TestMapWithValidations/lte.golden @@ -1,3 +1,4 @@ +// @typecheck export const LteSchema = z.object({ Map: z.record(z.string(), z.string()).refine((val) => Object.keys(val).length <= 1, 'Map too large'), }) diff --git a/testdata/TestMapWithValidations/max.golden b/testdata/TestMapWithValidations/max.golden index ef80246..864e47f 100644 --- a/testdata/TestMapWithValidations/max.golden +++ b/testdata/TestMapWithValidations/max.golden @@ -1,3 +1,4 @@ +// @typecheck export const MaxSchema = z.object({ Map: z.record(z.string(), z.string()).refine((val) => Object.keys(val).length <= 1, 'Map too large'), }) diff --git a/testdata/TestMapWithValidations/min.golden b/testdata/TestMapWithValidations/min.golden index c7e8a5e..251062b 100644 --- a/testdata/TestMapWithValidations/min.golden +++ b/testdata/TestMapWithValidations/min.golden @@ -1,3 +1,4 @@ +// @typecheck export const MinSchema = z.object({ Map: z.record(z.string(), z.string()).refine((val) => Object.keys(val).length >= 1, 'Map too small'), }) diff --git a/testdata/TestMapWithValidations/minmax.golden b/testdata/TestMapWithValidations/minmax.golden index 2d53c0a..97e9b27 100644 --- a/testdata/TestMapWithValidations/minmax.golden +++ b/testdata/TestMapWithValidations/minmax.golden @@ -1,3 +1,4 @@ +// @typecheck export const MinMaxSchema = z.object({ Map: z.record(z.string(), z.string()).refine((val) => Object.keys(val).length >= 1, 'Map too small').refine((val) => Object.keys(val).length <= 2, 'Map too large'), }) diff --git a/testdata/TestMapWithValidations/ne.golden b/testdata/TestMapWithValidations/ne.golden index 53102a3..ceceebe 100644 --- a/testdata/TestMapWithValidations/ne.golden +++ b/testdata/TestMapWithValidations/ne.golden @@ -1,3 +1,4 @@ +// @typecheck export const NeSchema = z.object({ Map: z.record(z.string(), z.string()).refine((val) => Object.keys(val).length !== 1, 'Map wrong size'), }) diff --git a/testdata/TestMapWithValidations/required.golden b/testdata/TestMapWithValidations/required.golden index 36baebd..708a0c0 100644 --- a/testdata/TestMapWithValidations/required.golden +++ b/testdata/TestMapWithValidations/required.golden @@ -1,3 +1,4 @@ +// @typecheck export const RequiredSchema = z.object({ Map: z.record(z.string(), z.string()), }) diff --git a/testdata/TestNestedStruct/v3.golden b/testdata/TestNestedStruct/v3.golden index 160c5a9..0fb2f9e 100644 --- a/testdata/TestNestedStruct/v3.golden +++ b/testdata/TestNestedStruct/v3.golden @@ -1,3 +1,5 @@ +// @zod-version: v3 +// @typecheck export const HasIDSchema = z.object({ ID: z.string(), }) diff --git a/testdata/TestNestedStruct/v4.golden b/testdata/TestNestedStruct/v4.golden index 30a7b0e..2e0fc02 100644 --- a/testdata/TestNestedStruct/v4.golden +++ b/testdata/TestNestedStruct/v4.golden @@ -1,3 +1,5 @@ +// @zod-version: v4 +// @typecheck export const HasIDSchema = z.object({ ID: z.string(), }) diff --git a/testdata/TestNullableWithValidations.golden b/testdata/TestNullableWithValidations.golden index 56f107a..f207104 100644 --- a/testdata/TestNullableWithValidations.golden +++ b/testdata/TestNullableWithValidations.golden @@ -1,3 +1,4 @@ +// @typecheck export const UserSchema = z.object({ Name: z.string().min(1), PtrMapOptionalNullable1: z.record(z.string(), z.any()).optional().nullable(), diff --git a/testdata/TestNumberValidations/eq.golden b/testdata/TestNumberValidations/eq.golden index 261bcf4..c9a72c2 100644 --- a/testdata/TestNumberValidations/eq.golden +++ b/testdata/TestNumberValidations/eq.golden @@ -1,3 +1,4 @@ +// @typecheck export const User3Schema = z.object({ Age: z.number().refine((val) => val === 18), }) diff --git a/testdata/TestNumberValidations/gt_lt.golden b/testdata/TestNumberValidations/gt_lt.golden index fbebdc6..44b5e1c 100644 --- a/testdata/TestNumberValidations/gt_lt.golden +++ b/testdata/TestNumberValidations/gt_lt.golden @@ -1,3 +1,4 @@ +// @typecheck export const User2Schema = z.object({ Age: z.number().gt(18).lt(60), }) diff --git a/testdata/TestNumberValidations/gte_lte.golden b/testdata/TestNumberValidations/gte_lte.golden index 1b41ef9..c4d7bb0 100644 --- a/testdata/TestNumberValidations/gte_lte.golden +++ b/testdata/TestNumberValidations/gte_lte.golden @@ -1,3 +1,4 @@ +// @typecheck export const User1Schema = z.object({ Age: z.number().gte(18).lte(60), }) diff --git a/testdata/TestNumberValidations/len.golden b/testdata/TestNumberValidations/len.golden index d22e72e..29ef608 100644 --- a/testdata/TestNumberValidations/len.golden +++ b/testdata/TestNumberValidations/len.golden @@ -1,3 +1,4 @@ +// @typecheck export const User7Schema = z.object({ Age: z.number().refine((val) => val === 18), }) diff --git a/testdata/TestNumberValidations/min_max.golden b/testdata/TestNumberValidations/min_max.golden index aac74da..6056270 100644 --- a/testdata/TestNumberValidations/min_max.golden +++ b/testdata/TestNumberValidations/min_max.golden @@ -1,3 +1,4 @@ +// @typecheck export const User6Schema = z.object({ Age: z.number().gte(18).lte(60), }) diff --git a/testdata/TestNumberValidations/ne.golden b/testdata/TestNumberValidations/ne.golden index 85cf5fb..33dfc93 100644 --- a/testdata/TestNumberValidations/ne.golden +++ b/testdata/TestNumberValidations/ne.golden @@ -1,3 +1,4 @@ +// @typecheck export const User4Schema = z.object({ Age: z.number().refine((val) => val !== 18), }) diff --git a/testdata/TestNumberValidations/oneof.golden b/testdata/TestNumberValidations/oneof.golden index f2b4d4e..1e2e0f3 100644 --- a/testdata/TestNumberValidations/oneof.golden +++ b/testdata/TestNumberValidations/oneof.golden @@ -1,3 +1,4 @@ +// @typecheck export const User5Schema = z.object({ Age: z.number().refine((val) => [18, 19, 20].includes(val)), }) diff --git a/testdata/TestRecursive1/v3.golden b/testdata/TestRecursive1/v3.golden index 0c6c645..e674ab4 100644 --- a/testdata/TestRecursive1/v3.golden +++ b/testdata/TestRecursive1/v3.golden @@ -1,3 +1,5 @@ +// @zod-version: v3 +// @typecheck export type NestedItem = { id: number, title: string, diff --git a/testdata/TestRecursive1/v4.golden b/testdata/TestRecursive1/v4.golden index 89dcd93..26d07cb 100644 --- a/testdata/TestRecursive1/v4.golden +++ b/testdata/TestRecursive1/v4.golden @@ -1,3 +1,5 @@ +// @zod-version: v4 +// @typecheck export type NestedItem = { id: number, title: string, diff --git a/testdata/TestRecursive2/v3.golden b/testdata/TestRecursive2/v3.golden index 86aa76a..be726d5 100644 --- a/testdata/TestRecursive2/v3.golden +++ b/testdata/TestRecursive2/v3.golden @@ -1,3 +1,5 @@ +// @zod-version: v3 +// @typecheck export type Node = { value: number, next: Node | null, diff --git a/testdata/TestRecursive2/v4.golden b/testdata/TestRecursive2/v4.golden index 813ad9f..f479004 100644 --- a/testdata/TestRecursive2/v4.golden +++ b/testdata/TestRecursive2/v4.golden @@ -1,3 +1,5 @@ +// @zod-version: v4 +// @typecheck export type Node = { value: number, next: Node | null, diff --git a/testdata/TestRecursiveEmbeddedStruct/v3.golden b/testdata/TestRecursiveEmbeddedStruct/v3.golden index 7f23bde..34cf577 100644 --- a/testdata/TestRecursiveEmbeddedStruct/v3.golden +++ b/testdata/TestRecursiveEmbeddedStruct/v3.golden @@ -1,3 +1,5 @@ +// @zod-version: v3 +// @typecheck export type ItemA = { Name: string, Children: ItemA[] | null, @@ -22,7 +24,7 @@ export const ItemDSchema = z.object({ }) export type ItemD = z.infer -export type ItemE = ItemA & ItemD & { +export type ItemE = Omit & ItemD & { Children: ItemE[] | null, } const ItemESchemaShape = { diff --git a/testdata/TestRecursiveEmbeddedStruct/v4.golden b/testdata/TestRecursiveEmbeddedStruct/v4.golden index dba6903..5617b0c 100644 --- a/testdata/TestRecursiveEmbeddedStruct/v4.golden +++ b/testdata/TestRecursiveEmbeddedStruct/v4.golden @@ -1,3 +1,5 @@ +// @zod-version: v4 +// @typecheck export type ItemA = { Name: string, Children: ItemA[] | null, @@ -23,7 +25,7 @@ export const ItemDSchema = z.object({ }) export type ItemD = z.infer -export type ItemE = ItemA & ItemD & { +export type ItemE = Omit & ItemD & { Children: ItemE[] | null, } const ItemESchemaShape = { diff --git a/testdata/TestRecursiveEmbeddedWithPointersAndDates/embedded_struct_with_pointer_to_self_and_date/v3.golden b/testdata/TestRecursiveEmbeddedWithPointersAndDates/embedded_struct_with_pointer_to_self_and_date/v3.golden index 17fd80f..5ab4f4c 100644 --- a/testdata/TestRecursiveEmbeddedWithPointersAndDates/embedded_struct_with_pointer_to_self_and_date/v3.golden +++ b/testdata/TestRecursiveEmbeddedWithPointersAndDates/embedded_struct_with_pointer_to_self_and_date/v3.golden @@ -1,3 +1,5 @@ +// @zod-version: v3 +// @typecheck export type Comment = { Text: string, Timestamp: Date, diff --git a/testdata/TestRecursiveEmbeddedWithPointersAndDates/embedded_struct_with_pointer_to_self_and_date/v4.golden b/testdata/TestRecursiveEmbeddedWithPointersAndDates/embedded_struct_with_pointer_to_self_and_date/v4.golden index ed3cb84..d9c859f 100644 --- a/testdata/TestRecursiveEmbeddedWithPointersAndDates/embedded_struct_with_pointer_to_self_and_date/v4.golden +++ b/testdata/TestRecursiveEmbeddedWithPointersAndDates/embedded_struct_with_pointer_to_self_and_date/v4.golden @@ -1,3 +1,5 @@ +// @zod-version: v4 +// @typecheck export type Comment = { Text: string, Timestamp: Date, diff --git a/testdata/TestRecursiveEmbeddedWithPointersAndDates/recursive_struct_with_pointer_field_and_date/v3.golden b/testdata/TestRecursiveEmbeddedWithPointersAndDates/recursive_struct_with_pointer_field_and_date/v3.golden index 8c7a1f3..fccf80b 100644 --- a/testdata/TestRecursiveEmbeddedWithPointersAndDates/recursive_struct_with_pointer_field_and_date/v3.golden +++ b/testdata/TestRecursiveEmbeddedWithPointersAndDates/recursive_struct_with_pointer_field_and_date/v3.golden @@ -1,3 +1,5 @@ +// @zod-version: v3 +// @typecheck export type TreeNode = { Value: string, CreatedAt: Date, diff --git a/testdata/TestRecursiveEmbeddedWithPointersAndDates/recursive_struct_with_pointer_field_and_date/v4.golden b/testdata/TestRecursiveEmbeddedWithPointersAndDates/recursive_struct_with_pointer_field_and_date/v4.golden index afead2e..3661084 100644 --- a/testdata/TestRecursiveEmbeddedWithPointersAndDates/recursive_struct_with_pointer_field_and_date/v4.golden +++ b/testdata/TestRecursiveEmbeddedWithPointersAndDates/recursive_struct_with_pointer_field_and_date/v4.golden @@ -1,3 +1,5 @@ +// @zod-version: v4 +// @typecheck export type TreeNode = { Value: string, CreatedAt: Date, diff --git a/testdata/TestSliceFields.golden b/testdata/TestSliceFields.golden index 223d6ea..2a5dfe2 100644 --- a/testdata/TestSliceFields.golden +++ b/testdata/TestSliceFields.golden @@ -1,3 +1,4 @@ +// @typecheck export const TestSliceFieldsStructSchema = z.object({ NoValidate: z.number().array().nullable(), Required: z.number().array(), diff --git a/testdata/TestStringArray.golden b/testdata/TestStringArray.golden index e1de802..6ed1cb2 100644 --- a/testdata/TestStringArray.golden +++ b/testdata/TestStringArray.golden @@ -1,3 +1,4 @@ +// @typecheck export const UserSchema = z.object({ Tags: z.string().array().nullable(), }) diff --git a/testdata/TestStringArrayNullable.golden b/testdata/TestStringArrayNullable.golden index 26d2624..a7ae5c7 100644 --- a/testdata/TestStringArrayNullable.golden +++ b/testdata/TestStringArrayNullable.golden @@ -1,3 +1,4 @@ +// @typecheck export const UserSchema = z.object({ Name: z.string(), Tags: z.string().array().nullable(), diff --git a/testdata/TestStringNestedArray.golden b/testdata/TestStringNestedArray.golden index e8b001d..2c39c32 100644 --- a/testdata/TestStringNestedArray.golden +++ b/testdata/TestStringNestedArray.golden @@ -1,3 +1,4 @@ +// @typecheck export const UserSchema = z.object({ TagPairs: z.string().array().length(2).array().nullable(), }) diff --git a/testdata/TestStringNullable.golden b/testdata/TestStringNullable.golden index 1d4282a..a9d3f5f 100644 --- a/testdata/TestStringNullable.golden +++ b/testdata/TestStringNullable.golden @@ -1,3 +1,4 @@ +// @typecheck export const UserSchema = z.object({ Name: z.string(), Nickname: z.string().nullable(), diff --git a/testdata/TestStringOptional.golden b/testdata/TestStringOptional.golden index e629a2a..d44d0aa 100644 --- a/testdata/TestStringOptional.golden +++ b/testdata/TestStringOptional.golden @@ -1,3 +1,4 @@ +// @typecheck export const UserSchema = z.object({ Name: z.string(), Nickname: z.string().optional(), diff --git a/testdata/TestStringOptionalNotNullable.golden b/testdata/TestStringOptionalNotNullable.golden index e629a2a..d44d0aa 100644 --- a/testdata/TestStringOptionalNotNullable.golden +++ b/testdata/TestStringOptionalNotNullable.golden @@ -1,3 +1,4 @@ +// @typecheck export const UserSchema = z.object({ Name: z.string(), Nickname: z.string().optional(), diff --git a/testdata/TestStringOptionalNullable.golden b/testdata/TestStringOptionalNullable.golden index 621f79b..f2833e3 100644 --- a/testdata/TestStringOptionalNullable.golden +++ b/testdata/TestStringOptionalNullable.golden @@ -1,3 +1,4 @@ +// @typecheck export const UserSchema = z.object({ Name: z.string(), Nickname: z.string().optional().nullable(), diff --git a/testdata/TestStringValidations/alpha.golden b/testdata/TestStringValidations/alpha.golden index 504c717..31abe6d 100644 --- a/testdata/TestStringValidations/alpha.golden +++ b/testdata/TestStringValidations/alpha.golden @@ -1,3 +1,4 @@ +// @typecheck export const AlphaSchema = z.object({ Name: z.string().regex(/^[a-zA-Z]+$/), }) diff --git a/testdata/TestStringValidations/alphanum.golden b/testdata/TestStringValidations/alphanum.golden index 767be25..889e7f8 100644 --- a/testdata/TestStringValidations/alphanum.golden +++ b/testdata/TestStringValidations/alphanum.golden @@ -1,3 +1,4 @@ +// @typecheck export const AlphaNumSchema = z.object({ Name: z.string().regex(/^[a-zA-Z0-9]+$/), }) diff --git a/testdata/TestStringValidations/alphanumunicode.golden b/testdata/TestStringValidations/alphanumunicode.golden index 56f9eb0..72ecf5e 100644 --- a/testdata/TestStringValidations/alphanumunicode.golden +++ b/testdata/TestStringValidations/alphanumunicode.golden @@ -1,5 +1,6 @@ +// @typecheck export const AlphaNumUnicodeSchema = z.object({ - Name: z.string().regex(/^[\p{L}\p{N}]+$/), + Name: z.string().regex(/^[\p{L}\p{N}]+$/u), }) export type AlphaNumUnicode = z.infer diff --git a/testdata/TestStringValidations/alphaunicode.golden b/testdata/TestStringValidations/alphaunicode.golden index 99ec880..01f9f0f 100644 --- a/testdata/TestStringValidations/alphaunicode.golden +++ b/testdata/TestStringValidations/alphaunicode.golden @@ -1,5 +1,6 @@ +// @typecheck export const AlphaUnicodeSchema = z.object({ - Name: z.string().regex(/^[\p{L}]+$/), + Name: z.string().regex(/^[\p{L}]+$/u), }) export type AlphaUnicode = z.infer diff --git a/testdata/TestStringValidations/ascii.golden b/testdata/TestStringValidations/ascii.golden index 868771d397c7620558342b99abfa047946c82de9..34df04e04508e42f4736892546810631ecf05f4d 100644 GIT binary patch delta 21 ccmZo*>|^B9*H>^TsVqoM&PYwp=9 val === "hello"), }) diff --git a/testdata/TestStringValidations/gt.golden b/testdata/TestStringValidations/gt.golden index 3f6646c..da8c9c4 100644 --- a/testdata/TestStringValidations/gt.golden +++ b/testdata/TestStringValidations/gt.golden @@ -1,3 +1,4 @@ +// @typecheck export const GtSchema = z.object({ Name: z.string().refine((val) => [...val].length > 5, 'String must contain at least 6 character(s)'), }) diff --git a/testdata/TestStringValidations/gte.golden b/testdata/TestStringValidations/gte.golden index dfb471d..3f3d021 100644 --- a/testdata/TestStringValidations/gte.golden +++ b/testdata/TestStringValidations/gte.golden @@ -1,3 +1,4 @@ +// @typecheck export const GteSchema = z.object({ Name: z.string().refine((val) => [...val].length >= 5, 'String must contain at least 5 character(s)'), }) diff --git a/testdata/TestStringValidations/hexadecimal/v3.golden b/testdata/TestStringValidations/hexadecimal/v3.golden index a618d1a..9683e70 100644 --- a/testdata/TestStringValidations/hexadecimal/v3.golden +++ b/testdata/TestStringValidations/hexadecimal/v3.golden @@ -1,3 +1,5 @@ +// @zod-version: v3 +// @typecheck export const HexadecimalSchema = z.object({ Name: z.string().regex(/^(0[xX])?[0-9a-fA-F]+$/), }) diff --git a/testdata/TestStringValidations/hexadecimal/v4.golden b/testdata/TestStringValidations/hexadecimal/v4.golden index bc70aef..90d5622 100644 --- a/testdata/TestStringValidations/hexadecimal/v4.golden +++ b/testdata/TestStringValidations/hexadecimal/v4.golden @@ -1,3 +1,5 @@ +// @zod-version: v4 +// @typecheck export const HexadecimalSchema = z.object({ Name: z.hex(), }) diff --git a/testdata/TestStringValidations/http_url/v3.golden b/testdata/TestStringValidations/http_url/v3.golden index eb5cdd7..ce2a6ef 100644 --- a/testdata/TestStringValidations/http_url/v3.golden +++ b/testdata/TestStringValidations/http_url/v3.golden @@ -1,3 +1,5 @@ +// @zod-version: v3 +// @typecheck export const HttpURLSchema = z.object({ Name: z.string().url(), }) diff --git a/testdata/TestStringValidations/http_url/v4.golden b/testdata/TestStringValidations/http_url/v4.golden index 5f8210d..71d1db2 100644 --- a/testdata/TestStringValidations/http_url/v4.golden +++ b/testdata/TestStringValidations/http_url/v4.golden @@ -1,3 +1,5 @@ +// @zod-version: v4 +// @typecheck export const HttpURLSchema = z.object({ Name: z.httpUrl(), }) diff --git a/testdata/TestStringValidations/ip/v3.golden b/testdata/TestStringValidations/ip/v3.golden index 77781f0..2cb4fc8 100644 --- a/testdata/TestStringValidations/ip/v3.golden +++ b/testdata/TestStringValidations/ip/v3.golden @@ -1,3 +1,5 @@ +// @zod-version: v3 +// @typecheck export const IPSchema = z.object({ Name: z.string().ip(), }) diff --git a/testdata/TestStringValidations/ip/v4.golden b/testdata/TestStringValidations/ip/v4.golden index b9051ca..c4fb347 100644 --- a/testdata/TestStringValidations/ip/v4.golden +++ b/testdata/TestStringValidations/ip/v4.golden @@ -1,3 +1,5 @@ +// @zod-version: v4 +// @typecheck export const IPSchema = z.object({ Name: z.union([z.ipv4(), z.ipv6()]), }) diff --git a/testdata/TestStringValidations/ip4_addr/v3.golden b/testdata/TestStringValidations/ip4_addr/v3.golden index ef185b0..518f229 100644 --- a/testdata/TestStringValidations/ip4_addr/v3.golden +++ b/testdata/TestStringValidations/ip4_addr/v3.golden @@ -1,3 +1,5 @@ +// @zod-version: v3 +// @typecheck export const IP4AddrSchema = z.object({ Name: z.string().ip({ version: "v4" }), }) diff --git a/testdata/TestStringValidations/ip4_addr/v4.golden b/testdata/TestStringValidations/ip4_addr/v4.golden index 673b49f..e5362a0 100644 --- a/testdata/TestStringValidations/ip4_addr/v4.golden +++ b/testdata/TestStringValidations/ip4_addr/v4.golden @@ -1,3 +1,5 @@ +// @zod-version: v4 +// @typecheck export const IP4AddrSchema = z.object({ Name: z.ipv4(), }) diff --git a/testdata/TestStringValidations/ip6_addr/v3.golden b/testdata/TestStringValidations/ip6_addr/v3.golden index 45c104b..0305357 100644 --- a/testdata/TestStringValidations/ip6_addr/v3.golden +++ b/testdata/TestStringValidations/ip6_addr/v3.golden @@ -1,3 +1,5 @@ +// @zod-version: v3 +// @typecheck export const IP6AddrSchema = z.object({ Name: z.string().ip({ version: "v6" }), }) diff --git a/testdata/TestStringValidations/ip6_addr/v4.golden b/testdata/TestStringValidations/ip6_addr/v4.golden index d028867..c00967d 100644 --- a/testdata/TestStringValidations/ip6_addr/v4.golden +++ b/testdata/TestStringValidations/ip6_addr/v4.golden @@ -1,3 +1,5 @@ +// @zod-version: v4 +// @typecheck export const IP6AddrSchema = z.object({ Name: z.ipv6(), }) diff --git a/testdata/TestStringValidations/ip_addr/v3.golden b/testdata/TestStringValidations/ip_addr/v3.golden index d9b309f..73e779f 100644 --- a/testdata/TestStringValidations/ip_addr/v3.golden +++ b/testdata/TestStringValidations/ip_addr/v3.golden @@ -1,3 +1,5 @@ +// @zod-version: v3 +// @typecheck export const IPAddrSchema = z.object({ Name: z.string().ip(), }) diff --git a/testdata/TestStringValidations/ip_addr/v4.golden b/testdata/TestStringValidations/ip_addr/v4.golden index 059527b..aeb8f2c 100644 --- a/testdata/TestStringValidations/ip_addr/v4.golden +++ b/testdata/TestStringValidations/ip_addr/v4.golden @@ -1,3 +1,5 @@ +// @zod-version: v4 +// @typecheck export const IPAddrSchema = z.object({ Name: z.union([z.ipv4(), z.ipv6()]), }) diff --git a/testdata/TestStringValidations/ipv4/v3.golden b/testdata/TestStringValidations/ipv4/v3.golden index d320aa5..90aa88d 100644 --- a/testdata/TestStringValidations/ipv4/v3.golden +++ b/testdata/TestStringValidations/ipv4/v3.golden @@ -1,3 +1,5 @@ +// @zod-version: v3 +// @typecheck export const IPv4Schema = z.object({ Name: z.string().ip({ version: "v4" }), }) diff --git a/testdata/TestStringValidations/ipv4/v4.golden b/testdata/TestStringValidations/ipv4/v4.golden index aa196c6..aa37310 100644 --- a/testdata/TestStringValidations/ipv4/v4.golden +++ b/testdata/TestStringValidations/ipv4/v4.golden @@ -1,3 +1,5 @@ +// @zod-version: v4 +// @typecheck export const IPv4Schema = z.object({ Name: z.ipv4(), }) diff --git a/testdata/TestStringValidations/ipv6/v3.golden b/testdata/TestStringValidations/ipv6/v3.golden index a6fa053..518c581 100644 --- a/testdata/TestStringValidations/ipv6/v3.golden +++ b/testdata/TestStringValidations/ipv6/v3.golden @@ -1,3 +1,5 @@ +// @zod-version: v3 +// @typecheck export const IPv6Schema = z.object({ Name: z.string().ip({ version: "v6" }), }) diff --git a/testdata/TestStringValidations/ipv6/v4.golden b/testdata/TestStringValidations/ipv6/v4.golden index 777754e..bbfd382 100644 --- a/testdata/TestStringValidations/ipv6/v4.golden +++ b/testdata/TestStringValidations/ipv6/v4.golden @@ -1,3 +1,5 @@ +// @zod-version: v4 +// @typecheck export const IPv6Schema = z.object({ Name: z.ipv6(), }) diff --git a/testdata/TestStringValidations/json.golden b/testdata/TestStringValidations/json.golden index fabddff..ef2c945 100644 --- a/testdata/TestStringValidations/json.golden +++ b/testdata/TestStringValidations/json.golden @@ -1,3 +1,4 @@ +// @typecheck export const jsonSchema = z.object({ Name: z.string().refine((val) => { try { JSON.parse(val); return true } catch { return false } }), }) diff --git a/testdata/TestStringValidations/latitude.golden b/testdata/TestStringValidations/latitude.golden index c8ac3cb..18b4df2 100644 --- a/testdata/TestStringValidations/latitude.golden +++ b/testdata/TestStringValidations/latitude.golden @@ -1,3 +1,4 @@ +// @typecheck export const LatitudeSchema = z.object({ Name: z.string().regex(/^[-+]?([1-8]?\d(\.\d+)?|90(\.0+)?)$/), }) diff --git a/testdata/TestStringValidations/len.golden b/testdata/TestStringValidations/len.golden index d2809dd..5a65da6 100644 --- a/testdata/TestStringValidations/len.golden +++ b/testdata/TestStringValidations/len.golden @@ -1,3 +1,4 @@ +// @typecheck export const LenSchema = z.object({ Name: z.string().refine((val) => [...val].length === 5, 'String must contain 5 character(s)'), }) diff --git a/testdata/TestStringValidations/longitude.golden b/testdata/TestStringValidations/longitude.golden index 7378179..f77d5fd 100644 --- a/testdata/TestStringValidations/longitude.golden +++ b/testdata/TestStringValidations/longitude.golden @@ -1,3 +1,4 @@ +// @typecheck export const LongitudeSchema = z.object({ Name: z.string().regex(/^[-+]?(180(\.0+)?|((1[0-7]\d)|([1-9]?\d))(\.\d+)?)$/), }) diff --git a/testdata/TestStringValidations/lowercase.golden b/testdata/TestStringValidations/lowercase.golden index 35afacb..750d188 100644 --- a/testdata/TestStringValidations/lowercase.golden +++ b/testdata/TestStringValidations/lowercase.golden @@ -1,3 +1,4 @@ +// @typecheck export const LowercaseSchema = z.object({ Name: z.string().refine((val) => val === val.toLowerCase()), }) diff --git a/testdata/TestStringValidations/lt.golden b/testdata/TestStringValidations/lt.golden index 9962f97..6f4f134 100644 --- a/testdata/TestStringValidations/lt.golden +++ b/testdata/TestStringValidations/lt.golden @@ -1,3 +1,4 @@ +// @typecheck export const LtSchema = z.object({ Name: z.string().refine((val) => [...val].length < 5, 'String must contain at most 4 character(s)'), }) diff --git a/testdata/TestStringValidations/lte.golden b/testdata/TestStringValidations/lte.golden index 3bb2591..454e751 100644 --- a/testdata/TestStringValidations/lte.golden +++ b/testdata/TestStringValidations/lte.golden @@ -1,3 +1,4 @@ +// @typecheck export const LteSchema = z.object({ Name: z.string().refine((val) => [...val].length <= 5, 'String must contain at most 5 character(s)'), }) diff --git a/testdata/TestStringValidations/max.golden b/testdata/TestStringValidations/max.golden index 531aa94..c49ef0c 100644 --- a/testdata/TestStringValidations/max.golden +++ b/testdata/TestStringValidations/max.golden @@ -1,3 +1,4 @@ +// @typecheck export const MaxSchema = z.object({ Name: z.string().refine((val) => [...val].length <= 5, 'String must contain at most 5 character(s)'), }) diff --git a/testdata/TestStringValidations/md4.golden b/testdata/TestStringValidations/md4.golden index 02218c5..01f2236 100644 --- a/testdata/TestStringValidations/md4.golden +++ b/testdata/TestStringValidations/md4.golden @@ -1,3 +1,4 @@ +// @typecheck export const MD4Schema = z.object({ Name: z.string().regex(/^[0-9a-f]{32}$/), }) diff --git a/testdata/TestStringValidations/md5/v3.golden b/testdata/TestStringValidations/md5/v3.golden index b222b8a..e8efa20 100644 --- a/testdata/TestStringValidations/md5/v3.golden +++ b/testdata/TestStringValidations/md5/v3.golden @@ -1,3 +1,5 @@ +// @zod-version: v3 +// @typecheck export const MD5Schema = z.object({ Name: z.string().regex(/^[0-9a-f]{32}$/), }) diff --git a/testdata/TestStringValidations/md5/v4.golden b/testdata/TestStringValidations/md5/v4.golden index 916e966..1ec4bd9 100644 --- a/testdata/TestStringValidations/md5/v4.golden +++ b/testdata/TestStringValidations/md5/v4.golden @@ -1,3 +1,5 @@ +// @zod-version: v4 +// @typecheck export const MD5Schema = z.object({ Name: z.hash("md5"), }) diff --git a/testdata/TestStringValidations/min.golden b/testdata/TestStringValidations/min.golden index 97d4b2a..656da20 100644 --- a/testdata/TestStringValidations/min.golden +++ b/testdata/TestStringValidations/min.golden @@ -1,3 +1,4 @@ +// @typecheck export const MinSchema = z.object({ Name: z.string().refine((val) => [...val].length >= 5, 'String must contain at least 5 character(s)'), }) diff --git a/testdata/TestStringValidations/minmax.golden b/testdata/TestStringValidations/minmax.golden index 5997d5b..67daa11 100644 --- a/testdata/TestStringValidations/minmax.golden +++ b/testdata/TestStringValidations/minmax.golden @@ -1,3 +1,4 @@ +// @typecheck export const MinMaxSchema = z.object({ 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)'), }) diff --git a/testdata/TestStringValidations/mongodb.golden b/testdata/TestStringValidations/mongodb.golden index 4af106f..983efff 100644 --- a/testdata/TestStringValidations/mongodb.golden +++ b/testdata/TestStringValidations/mongodb.golden @@ -1,3 +1,4 @@ +// @typecheck export const mongodbSchema = z.object({ Name: z.string().regex(/^[a-f\d]{24}$/), }) diff --git a/testdata/TestStringValidations/ne.golden b/testdata/TestStringValidations/ne.golden index abf32f1..adf5719 100644 --- a/testdata/TestStringValidations/ne.golden +++ b/testdata/TestStringValidations/ne.golden @@ -1,3 +1,4 @@ +// @typecheck export const NeSchema = z.object({ Name: z.string().refine((val) => val !== "hello"), }) diff --git a/testdata/TestStringValidations/number.golden b/testdata/TestStringValidations/number.golden index 3a95f74..a03709d 100644 --- a/testdata/TestStringValidations/number.golden +++ b/testdata/TestStringValidations/number.golden @@ -1,3 +1,4 @@ +// @typecheck export const NumberSchema = z.object({ Name: z.string().regex(/^[0-9]+$/), }) diff --git a/testdata/TestStringValidations/numeric.golden b/testdata/TestStringValidations/numeric.golden index afc9ae7..67b4d02 100644 --- a/testdata/TestStringValidations/numeric.golden +++ b/testdata/TestStringValidations/numeric.golden @@ -1,3 +1,4 @@ +// @typecheck export const NumericSchema = z.object({ Name: z.string().regex(/^[-+]?[0-9]+(?:\.[0-9]+)?$/), }) diff --git a/testdata/TestStringValidations/oneof.golden b/testdata/TestStringValidations/oneof.golden index 41afee2..29dc59f 100644 --- a/testdata/TestStringValidations/oneof.golden +++ b/testdata/TestStringValidations/oneof.golden @@ -1,3 +1,4 @@ +// @typecheck export const OneOfSchema = z.object({ Name: z.enum(["hello", "world"] as const), }) diff --git a/testdata/TestStringValidations/oneof_separated.golden b/testdata/TestStringValidations/oneof_separated.golden index 3af90a9..fd9d6ec 100644 --- a/testdata/TestStringValidations/oneof_separated.golden +++ b/testdata/TestStringValidations/oneof_separated.golden @@ -1,3 +1,4 @@ +// @typecheck export const OneOfSeparatedSchema = z.object({ Name: z.enum(["a b c", "d e f"] as const), }) diff --git a/testdata/TestStringValidations/required.golden b/testdata/TestStringValidations/required.golden index fdd8771..5c1e0a8 100644 --- a/testdata/TestStringValidations/required.golden +++ b/testdata/TestStringValidations/required.golden @@ -1,3 +1,4 @@ +// @typecheck export const RequiredSchema = z.object({ Name: z.string().min(1), }) diff --git a/testdata/TestStringValidations/sha256/v3.golden b/testdata/TestStringValidations/sha256/v3.golden index aa129fc..e588501 100644 --- a/testdata/TestStringValidations/sha256/v3.golden +++ b/testdata/TestStringValidations/sha256/v3.golden @@ -1,3 +1,5 @@ +// @zod-version: v3 +// @typecheck export const SHA256Schema = z.object({ Name: z.string().regex(/^[0-9a-f]{64}$/), }) diff --git a/testdata/TestStringValidations/sha256/v4.golden b/testdata/TestStringValidations/sha256/v4.golden index f3f70d7..672c660 100644 --- a/testdata/TestStringValidations/sha256/v4.golden +++ b/testdata/TestStringValidations/sha256/v4.golden @@ -1,3 +1,5 @@ +// @zod-version: v4 +// @typecheck export const SHA256Schema = z.object({ Name: z.hash("sha256"), }) diff --git a/testdata/TestStringValidations/sha384/v3.golden b/testdata/TestStringValidations/sha384/v3.golden index 91385b9..333832b 100644 --- a/testdata/TestStringValidations/sha384/v3.golden +++ b/testdata/TestStringValidations/sha384/v3.golden @@ -1,3 +1,5 @@ +// @zod-version: v3 +// @typecheck export const SHA384Schema = z.object({ Name: z.string().regex(/^[0-9a-f]{96}$/), }) diff --git a/testdata/TestStringValidations/sha384/v4.golden b/testdata/TestStringValidations/sha384/v4.golden index 02639a3..c14e4c7 100644 --- a/testdata/TestStringValidations/sha384/v4.golden +++ b/testdata/TestStringValidations/sha384/v4.golden @@ -1,3 +1,5 @@ +// @zod-version: v4 +// @typecheck export const SHA384Schema = z.object({ Name: z.hash("sha384"), }) diff --git a/testdata/TestStringValidations/sha512/v3.golden b/testdata/TestStringValidations/sha512/v3.golden index f7ac7fd..b9c410c 100644 --- a/testdata/TestStringValidations/sha512/v3.golden +++ b/testdata/TestStringValidations/sha512/v3.golden @@ -1,3 +1,5 @@ +// @zod-version: v3 +// @typecheck export const SHA512Schema = z.object({ Name: z.string().regex(/^[0-9a-f]{128}$/), }) diff --git a/testdata/TestStringValidations/sha512/v4.golden b/testdata/TestStringValidations/sha512/v4.golden index 77bc7c8..a62e7d3 100644 --- a/testdata/TestStringValidations/sha512/v4.golden +++ b/testdata/TestStringValidations/sha512/v4.golden @@ -1,3 +1,5 @@ +// @zod-version: v4 +// @typecheck export const SHA512Schema = z.object({ Name: z.hash("sha512"), }) diff --git a/testdata/TestStringValidations/startswith.golden b/testdata/TestStringValidations/startswith.golden index f00ffef..52f555f 100644 --- a/testdata/TestStringValidations/startswith.golden +++ b/testdata/TestStringValidations/startswith.golden @@ -1,3 +1,4 @@ +// @typecheck export const StartsWithSchema = z.object({ Name: z.string().startsWith("hello"), }) diff --git a/testdata/TestStringValidations/uppercase.golden b/testdata/TestStringValidations/uppercase.golden index fceefcc..31a86a6 100644 --- a/testdata/TestStringValidations/uppercase.golden +++ b/testdata/TestStringValidations/uppercase.golden @@ -1,3 +1,4 @@ +// @typecheck export const UppercaseSchema = z.object({ Name: z.string().refine((val) => val === val.toUpperCase()), }) diff --git a/testdata/TestStringValidations/url/v3.golden b/testdata/TestStringValidations/url/v3.golden index 51c00fe..bd27184 100644 --- a/testdata/TestStringValidations/url/v3.golden +++ b/testdata/TestStringValidations/url/v3.golden @@ -1,3 +1,5 @@ +// @zod-version: v3 +// @typecheck export const URLSchema = z.object({ Name: z.string().url(), }) diff --git a/testdata/TestStringValidations/url/v4.golden b/testdata/TestStringValidations/url/v4.golden index 00988f0..d2c3e4c 100644 --- a/testdata/TestStringValidations/url/v4.golden +++ b/testdata/TestStringValidations/url/v4.golden @@ -1,3 +1,5 @@ +// @zod-version: v4 +// @typecheck export const URLSchema = z.object({ Name: z.url(), }) diff --git a/testdata/TestStringValidations/url_encoded.golden b/testdata/TestStringValidations/url_encoded.golden index 180542f..93dc6ea 100644 --- a/testdata/TestStringValidations/url_encoded.golden +++ b/testdata/TestStringValidations/url_encoded.golden @@ -1,3 +1,4 @@ +// @typecheck export const URLEncodedSchema = z.object({ Name: z.string().regex(/^(?:[^%]|%[0-9A-Fa-f]{2})*$/), }) diff --git a/testdata/TestStringValidations/uuid/v3.golden b/testdata/TestStringValidations/uuid/v3.golden index 1999396..641eff6 100644 --- a/testdata/TestStringValidations/uuid/v3.golden +++ b/testdata/TestStringValidations/uuid/v3.golden @@ -1,3 +1,5 @@ +// @zod-version: v3 +// @typecheck export const UUIDSchema = z.object({ Name: z.string().regex(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/), }) diff --git a/testdata/TestStringValidations/uuid/v4.golden b/testdata/TestStringValidations/uuid/v4.golden index 8f1f3a6..0a92948 100644 --- a/testdata/TestStringValidations/uuid/v4.golden +++ b/testdata/TestStringValidations/uuid/v4.golden @@ -1,3 +1,5 @@ +// @zod-version: v4 +// @typecheck export const UUIDSchema = z.object({ Name: z.uuid(), }) diff --git a/testdata/TestStringValidations/uuid3/v3.golden b/testdata/TestStringValidations/uuid3/v3.golden index 00931f3..ec05fbd 100644 --- a/testdata/TestStringValidations/uuid3/v3.golden +++ b/testdata/TestStringValidations/uuid3/v3.golden @@ -1,3 +1,5 @@ +// @zod-version: v3 +// @typecheck export const UUID3Schema = z.object({ Name: z.string().regex(/^[0-9a-f]{8}-[0-9a-f]{4}-3[0-9a-f]{3}-[0-9a-f]{4}-[0-9a-f]{12}$/), }) diff --git a/testdata/TestStringValidations/uuid3/v4.golden b/testdata/TestStringValidations/uuid3/v4.golden index 9711673..67d4af5 100644 --- a/testdata/TestStringValidations/uuid3/v4.golden +++ b/testdata/TestStringValidations/uuid3/v4.golden @@ -1,3 +1,5 @@ +// @zod-version: v4 +// @typecheck export const UUID3Schema = z.object({ Name: z.uuid({ version: "v3" }), }) diff --git a/testdata/TestStringValidations/uuid3_rfc4122/v3.golden b/testdata/TestStringValidations/uuid3_rfc4122/v3.golden index 49f6d8c..9f955dc 100644 --- a/testdata/TestStringValidations/uuid3_rfc4122/v3.golden +++ b/testdata/TestStringValidations/uuid3_rfc4122/v3.golden @@ -1,3 +1,5 @@ +// @zod-version: v3 +// @typecheck export const UUID3RFC4122Schema = z.object({ Name: z.string().regex(/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-3[0-9a-fA-F]{3}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/), }) diff --git a/testdata/TestStringValidations/uuid3_rfc4122/v4.golden b/testdata/TestStringValidations/uuid3_rfc4122/v4.golden index b48d645..5e2a32d 100644 --- a/testdata/TestStringValidations/uuid3_rfc4122/v4.golden +++ b/testdata/TestStringValidations/uuid3_rfc4122/v4.golden @@ -1,3 +1,5 @@ +// @zod-version: v4 +// @typecheck export const UUID3RFC4122Schema = z.object({ Name: z.uuid({ version: "v3" }), }) diff --git a/testdata/TestStringValidations/uuid4/v3.golden b/testdata/TestStringValidations/uuid4/v3.golden index 5002a65..8d2959d 100644 --- a/testdata/TestStringValidations/uuid4/v3.golden +++ b/testdata/TestStringValidations/uuid4/v3.golden @@ -1,3 +1,5 @@ +// @zod-version: v3 +// @typecheck export const UUID4Schema = z.object({ Name: z.string().regex(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/), }) diff --git a/testdata/TestStringValidations/uuid4/v4.golden b/testdata/TestStringValidations/uuid4/v4.golden index 4e56638..5375789 100644 --- a/testdata/TestStringValidations/uuid4/v4.golden +++ b/testdata/TestStringValidations/uuid4/v4.golden @@ -1,3 +1,5 @@ +// @zod-version: v4 +// @typecheck export const UUID4Schema = z.object({ Name: z.uuid({ version: "v4" }), }) diff --git a/testdata/TestStringValidations/uuid4_rfc4122/v3.golden b/testdata/TestStringValidations/uuid4_rfc4122/v3.golden index d025b7b..557d7a9 100644 --- a/testdata/TestStringValidations/uuid4_rfc4122/v3.golden +++ b/testdata/TestStringValidations/uuid4_rfc4122/v3.golden @@ -1,3 +1,5 @@ +// @zod-version: v3 +// @typecheck export const UUID4RFC4122Schema = z.object({ Name: z.string().regex(/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$/), }) diff --git a/testdata/TestStringValidations/uuid4_rfc4122/v4.golden b/testdata/TestStringValidations/uuid4_rfc4122/v4.golden index 24f7f24..856aaf1 100644 --- a/testdata/TestStringValidations/uuid4_rfc4122/v4.golden +++ b/testdata/TestStringValidations/uuid4_rfc4122/v4.golden @@ -1,3 +1,5 @@ +// @zod-version: v4 +// @typecheck export const UUID4RFC4122Schema = z.object({ Name: z.uuid({ version: "v4" }), }) diff --git a/testdata/TestStringValidations/uuid5/v3.golden b/testdata/TestStringValidations/uuid5/v3.golden index 669b090..78f090c 100644 --- a/testdata/TestStringValidations/uuid5/v3.golden +++ b/testdata/TestStringValidations/uuid5/v3.golden @@ -1,3 +1,5 @@ +// @zod-version: v3 +// @typecheck export const UUID5Schema = z.object({ Name: z.string().regex(/^[0-9a-f]{8}-[0-9a-f]{4}-5[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/), }) diff --git a/testdata/TestStringValidations/uuid5/v4.golden b/testdata/TestStringValidations/uuid5/v4.golden index eb4ca18..c637452 100644 --- a/testdata/TestStringValidations/uuid5/v4.golden +++ b/testdata/TestStringValidations/uuid5/v4.golden @@ -1,3 +1,5 @@ +// @zod-version: v4 +// @typecheck export const UUID5Schema = z.object({ Name: z.uuid({ version: "v5" }), }) diff --git a/testdata/TestStringValidations/uuid5_rfc4122/v3.golden b/testdata/TestStringValidations/uuid5_rfc4122/v3.golden index b35a6aa..d58d435 100644 --- a/testdata/TestStringValidations/uuid5_rfc4122/v3.golden +++ b/testdata/TestStringValidations/uuid5_rfc4122/v3.golden @@ -1,3 +1,5 @@ +// @zod-version: v3 +// @typecheck export const UUID5RFC4122Schema = z.object({ Name: z.string().regex(/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-5[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$/), }) diff --git a/testdata/TestStringValidations/uuid5_rfc4122/v4.golden b/testdata/TestStringValidations/uuid5_rfc4122/v4.golden index 32d8b44..f258e67 100644 --- a/testdata/TestStringValidations/uuid5_rfc4122/v4.golden +++ b/testdata/TestStringValidations/uuid5_rfc4122/v4.golden @@ -1,3 +1,5 @@ +// @zod-version: v4 +// @typecheck export const UUID5RFC4122Schema = z.object({ Name: z.uuid({ version: "v5" }), }) diff --git a/testdata/TestStringValidations/uuid_rfc4122/v3.golden b/testdata/TestStringValidations/uuid_rfc4122/v3.golden index 9c98a2f..2dfc34f 100644 --- a/testdata/TestStringValidations/uuid_rfc4122/v3.golden +++ b/testdata/TestStringValidations/uuid_rfc4122/v3.golden @@ -1,3 +1,5 @@ +// @zod-version: v3 +// @typecheck export const UUIDRFC4122Schema = z.object({ Name: z.string().regex(/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/), }) diff --git a/testdata/TestStringValidations/uuid_rfc4122/v4.golden b/testdata/TestStringValidations/uuid_rfc4122/v4.golden index c392b05..3805ebb 100644 --- a/testdata/TestStringValidations/uuid_rfc4122/v4.golden +++ b/testdata/TestStringValidations/uuid_rfc4122/v4.golden @@ -1,3 +1,5 @@ +// @zod-version: v4 +// @typecheck export const UUIDRFC4122Schema = z.object({ Name: z.uuid(), }) diff --git a/testdata/TestStructSimple.golden b/testdata/TestStructSimple.golden index 729e518..97a833b 100644 --- a/testdata/TestStructSimple.golden +++ b/testdata/TestStructSimple.golden @@ -1,3 +1,4 @@ +// @typecheck export const UserSchema = z.object({ Name: z.string(), Age: z.number(), diff --git a/testdata/TestStructSimplePrefix.golden b/testdata/TestStructSimplePrefix.golden index 917ff7d..3b5a67c 100644 --- a/testdata/TestStructSimplePrefix.golden +++ b/testdata/TestStructSimplePrefix.golden @@ -1,3 +1,4 @@ +// @typecheck export const BotUserSchema = z.object({ Name: z.string(), Age: z.number(), diff --git a/testdata/TestStructSimpleWithOmittedField.golden b/testdata/TestStructSimpleWithOmittedField.golden index 729e518..97a833b 100644 --- a/testdata/TestStructSimpleWithOmittedField.golden +++ b/testdata/TestStructSimpleWithOmittedField.golden @@ -1,3 +1,4 @@ +// @typecheck export const UserSchema = z.object({ Name: z.string(), Age: z.number(), diff --git a/testdata/TestStructSlice.golden b/testdata/TestStructSlice.golden index 7907668..93c7153 100644 --- a/testdata/TestStructSlice.golden +++ b/testdata/TestStructSlice.golden @@ -1,3 +1,4 @@ +// @typecheck export const UserSchema = z.object({ Favourites: z.object({ Name: z.string(), diff --git a/testdata/TestStructSliceOptional.golden b/testdata/TestStructSliceOptional.golden index 822690b..5a4c6d7 100644 --- a/testdata/TestStructSliceOptional.golden +++ b/testdata/TestStructSliceOptional.golden @@ -1,3 +1,4 @@ +// @typecheck export const UserSchema = z.object({ Favourites: z.object({ Name: z.string(), diff --git a/testdata/TestStructSliceOptionalNullable.golden b/testdata/TestStructSliceOptionalNullable.golden index 87cb553..6d50465 100644 --- a/testdata/TestStructSliceOptionalNullable.golden +++ b/testdata/TestStructSliceOptionalNullable.golden @@ -1,3 +1,4 @@ +// @typecheck export const UserSchema = z.object({ Favourites: z.object({ Name: z.string(), diff --git a/testdata/TestStructTime.golden b/testdata/TestStructTime.golden index 5a5940e..cb6ca7d 100644 --- a/testdata/TestStructTime.golden +++ b/testdata/TestStructTime.golden @@ -1,3 +1,4 @@ +// @typecheck export const UserSchema = z.object({ Name: z.string(), When: z.coerce.date(), diff --git a/testdata/TestTimeWithRequired.golden b/testdata/TestTimeWithRequired.golden index ef99490..c2e6824 100644 --- a/testdata/TestTimeWithRequired.golden +++ b/testdata/TestTimeWithRequired.golden @@ -1,3 +1,4 @@ +// @typecheck export const UserSchema = z.object({ When: z.coerce.date().refine((val) => val.getTime() !== new Date('0001-01-01T00:00:00Z').getTime() && val.getTime() !== new Date(0).getTime(), 'Invalid date'), }) diff --git a/testdata/TestZodV4Defaults/embedded_structs_use_shape_spreads.golden b/testdata/TestZodV4Defaults/embedded_structs_use_shape_spreads.golden index 30a7b0e..2e0fc02 100644 --- a/testdata/TestZodV4Defaults/embedded_structs_use_shape_spreads.golden +++ b/testdata/TestZodV4Defaults/embedded_structs_use_shape_spreads.golden @@ -1,3 +1,5 @@ +// @zod-version: v4 +// @typecheck export const HasIDSchema = z.object({ ID: z.string(), }) diff --git a/testdata/TestZodV4Defaults/enum_keyed_maps_become_partial_records.golden b/testdata/TestZodV4Defaults/enum_keyed_maps_become_partial_records.golden index b4e0ff8..50fb1d7 100644 --- a/testdata/TestZodV4Defaults/enum_keyed_maps_become_partial_records.golden +++ b/testdata/TestZodV4Defaults/enum_keyed_maps_become_partial_records.golden @@ -1,3 +1,5 @@ +// @zod-version: v4 +// @typecheck export const PayloadSchema = z.object({ Metadata: z.partialRecord(z.enum(["draft", "published"] as const), z.string()).nullable(), }) diff --git a/testdata/TestZodV4Defaults/ip_mixed_with_another_format_falls_back_to_legacy_chain_semantics.golden b/testdata/TestZodV4Defaults/ip_mixed_with_another_format_falls_back_to_legacy_chain_semantics.golden index cd33cb8..3125525 100644 --- a/testdata/TestZodV4Defaults/ip_mixed_with_another_format_falls_back_to_legacy_chain_semantics.golden +++ b/testdata/TestZodV4Defaults/ip_mixed_with_another_format_falls_back_to_legacy_chain_semantics.golden @@ -1,5 +1,7 @@ +// @zod-version: v4 +// @typecheck export const PayloadSchema = z.object({ - Address: z.string().email().ip(), + Address: z.string().email(), }) export type Payload = z.infer diff --git a/testdata/TestZodV4Defaults/ip_unions_inherit_generic_string_constraints.golden b/testdata/TestZodV4Defaults/ip_unions_inherit_generic_string_constraints.golden index 542f6a9..302d5e5 100644 --- a/testdata/TestZodV4Defaults/ip_unions_inherit_generic_string_constraints.golden +++ b/testdata/TestZodV4Defaults/ip_unions_inherit_generic_string_constraints.golden @@ -1,3 +1,5 @@ +// @zod-version: v4 +// @typecheck export const PayloadSchema = z.object({ Address: z.union([z.ipv4().min(1).refine((val) => [...val].length <= 45, 'String must contain at most 45 character(s)'), z.ipv6().min(1).refine((val) => [...val].length <= 45, 'String must contain at most 45 character(s)')]), }) diff --git a/testdata/TestZodV4Defaults/ip_unions_work_when_chain_constraints_precede_ip_tag/v3.golden b/testdata/TestZodV4Defaults/ip_unions_work_when_chain_constraints_precede_ip_tag/v3.golden new file mode 100644 index 0000000..ccc58b7 --- /dev/null +++ b/testdata/TestZodV4Defaults/ip_unions_work_when_chain_constraints_precede_ip_tag/v3.golden @@ -0,0 +1,7 @@ +// @zod-version: v3 +// @typecheck +export const PayloadSchema = z.object({ + Address: z.string().min(1).ip(), +}) +export type Payload = z.infer + diff --git a/testdata/TestZodV4Defaults/ip_unions_work_when_chain_constraints_precede_ip_tag/v4.golden b/testdata/TestZodV4Defaults/ip_unions_work_when_chain_constraints_precede_ip_tag/v4.golden new file mode 100644 index 0000000..7dc027b --- /dev/null +++ b/testdata/TestZodV4Defaults/ip_unions_work_when_chain_constraints_precede_ip_tag/v4.golden @@ -0,0 +1,7 @@ +// @zod-version: v4 +// @typecheck +export const PayloadSchema = z.object({ + Address: z.union([z.ipv4().min(1), z.ipv6().min(1)]), +}) +export type Payload = z.infer + diff --git a/testdata/TestZodV4Defaults/oneof_takes_precedence_over_ip_specialization.golden b/testdata/TestZodV4Defaults/oneof_takes_precedence_over_ip_specialization.golden index 98f2ccd..9263d61 100644 --- a/testdata/TestZodV4Defaults/oneof_takes_precedence_over_ip_specialization.golden +++ b/testdata/TestZodV4Defaults/oneof_takes_precedence_over_ip_specialization.golden @@ -1,3 +1,5 @@ +// @zod-version: v4 +// @typecheck export const PayloadSchema = z.object({ Address: z.enum(["127.0.0.1", "::1"] as const), }) diff --git a/testdata/TestZodV4Defaults/recursive_embedded_shapes_keep_named_fields_before_spreads.golden b/testdata/TestZodV4Defaults/recursive_embedded_shapes_keep_named_fields_before_spreads.golden index afead2e..3661084 100644 --- a/testdata/TestZodV4Defaults/recursive_embedded_shapes_keep_named_fields_before_spreads.golden +++ b/testdata/TestZodV4Defaults/recursive_embedded_shapes_keep_named_fields_before_spreads.golden @@ -1,3 +1,5 @@ +// @zod-version: v4 +// @typecheck export type TreeNode = { Value: string, CreatedAt: Date, diff --git a/testdata/TestZodV4Defaults/recursive_embedded_shapes_preserve_encounter_order_for_duplicate_keys.golden b/testdata/TestZodV4Defaults/recursive_embedded_shapes_preserve_encounter_order_for_duplicate_keys.golden index f4095fd..27e4d4d 100644 --- a/testdata/TestZodV4Defaults/recursive_embedded_shapes_preserve_encounter_order_for_duplicate_keys.golden +++ b/testdata/TestZodV4Defaults/recursive_embedded_shapes_preserve_encounter_order_for_duplicate_keys.golden @@ -1,9 +1,11 @@ +// @zod-version: v4 +// @typecheck export const BaseSchema = z.object({ id: z.string(), }) export type Base = z.infer -export type Node = Base & { +export type Node = Omit & { id: number, next: Node | null, } diff --git a/testdata/TestZodV4Defaults/string_formats_use_zod_v4_builders.golden b/testdata/TestZodV4Defaults/string_formats_use_zod_v4_builders.golden index d1c7c53..f3f08b6 100644 --- a/testdata/TestZodV4Defaults/string_formats_use_zod_v4_builders.golden +++ b/testdata/TestZodV4Defaults/string_formats_use_zod_v4_builders.golden @@ -1,3 +1,5 @@ +// @zod-version: v4 +// @typecheck export const PayloadSchema = z.object({ Email: z.email(), Link: z.httpUrl(), diff --git a/testdata/TestZodV4Defaults/string_tag_order_is_preserved_around_v4_format_helpers.golden b/testdata/TestZodV4Defaults/string_tag_order_is_preserved_around_v4_format_helpers.golden index cd576a8..291cf68 100644 --- a/testdata/TestZodV4Defaults/string_tag_order_is_preserved_around_v4_format_helpers.golden +++ b/testdata/TestZodV4Defaults/string_tag_order_is_preserved_around_v4_format_helpers.golden @@ -1,3 +1,5 @@ +// @zod-version: v4 +// @typecheck export const PayloadSchema = z.object({ TrimmedThenEmail: z.string().trim().email(), EmailThenTrimmed: z.email().trim(), diff --git a/zod.go b/zod.go index cf239c7..5d236a0 100644 --- a/zod.go +++ b/zod.go @@ -181,10 +181,10 @@ type stringSchemaChunk struct { } type Converter struct { - prefix string - customTypes map[string]CustomFn - customTags map[string]CustomFn - ignoreTags []string + prefix string + customTypes map[string]CustomFn + customTags map[string]CustomFn + ignoreTags []string zodV3 bool lastFieldSelfRef bool structs int @@ -380,7 +380,18 @@ func (c *Converter) getTypeStruct(input reflect.Type, indent int) string { merges := []string{} + // Collect own (non-anonymous) field names to detect shadowing. fields := input.NumField() + ownFieldNames := map[string]bool{} + for i := 0; i < fields; i++ { + f := input.Field(i) + if !f.Anonymous { + if name := fieldName(f); name != "-" { + ownFieldNames[name] = true + } + } + } + for i := 0; i < fields; i++ { field := input.Field(i) optional := isOptional(field) @@ -391,6 +402,27 @@ func (c *Converter) getTypeStruct(input reflect.Type, indent int) string { if !shouldMerge { output.WriteString(line) } else { + // When own fields shadow embedded fields, wrap in Omit<> so the + // TypeScript intersection doesn't produce conflicting property types. + embeddedType := field.Type + if embeddedType.Kind() == reflect.Ptr { + embeddedType = embeddedType.Elem() + } + var shadowedKeys []string + if embeddedType.Kind() == reflect.Struct { + for j := 0; j < embeddedType.NumField(); j++ { + if name := fieldName(embeddedType.Field(j)); name != "-" && ownFieldNames[name] { + shadowedKeys = append(shadowedKeys, name) + } + } + } + if len(shadowedKeys) > 0 { + quoted := make([]string, len(shadowedKeys)) + for k, key := range shadowedKeys { + quoted[k] = fmt.Sprintf("'%s'", key) + } + line = fmt.Sprintf("Omit<%s, %s>", line, strings.Join(quoted, " | ")) + } merges = append(merges, line) } } @@ -776,7 +808,8 @@ func (c *Converter) getTypeSliceAndArray(t reflect.Type, indent int) string { func (c *Converter) convertKeyType(t reflect.Type, validate string) string { if t.Name() == "Time" { - return "z.coerce.date()" + // JSON serializes time.Time map keys as RFC3339 strings via TextMarshaler. + return "z.string()" } // boolean, number, string, any @@ -1106,9 +1139,9 @@ func (c *Converter) validateString(validate string) stringSchemaParts { case "alphanum": chunks = append(chunks, stringSchemaChunk{kind: "chain", text: fmt.Sprintf(".regex(/%s/)", alphaNumericRegexString)}) case "alphanumunicode": - chunks = append(chunks, stringSchemaChunk{kind: "chain", text: fmt.Sprintf(".regex(/%s/)", alphaUnicodeNumericRegexString)}) + chunks = append(chunks, stringSchemaChunk{kind: "chain", text: fmt.Sprintf(".regex(/%s/u)", alphaUnicodeNumericRegexString)}) case "alphaunicode": - chunks = append(chunks, stringSchemaChunk{kind: "chain", text: fmt.Sprintf(".regex(/%s/)", alphaUnicodeRegexString)}) + chunks = append(chunks, stringSchemaChunk{kind: "chain", text: fmt.Sprintf(".regex(/%s/u)", alphaUnicodeRegexString)}) case "ascii": chunks = append(chunks, stringSchemaChunk{kind: "chain", text: fmt.Sprintf(".regex(/%s/)", aSCIIRegexString)}) case "boolean": @@ -1219,8 +1252,14 @@ func (c *Converter) lowerStringSchemaChunks(chunks []stringSchemaChunk) stringSc } if firstIPIdx != -1 { - if hasNonIPFormat || hasChainBeforeStringSchemaChunk(chunks, firstIPIdx) { + if hasNonIPFormat { + // In v4, .ip() doesn't exist as a chain method. Since combining + // ip with another format (e.g. email) is semantically nonsensical, + // drop the ip chunk and keep only the other format + chain pieces. for _, chunk := range chunks { + if chunk.kind == "ip" { + continue + } schemaParts.chain += legacyStringSchemaChunk(chunk) } return schemaParts diff --git a/zod_test.go b/zod_test.go index ad91eb3..f0ed18d 100644 --- a/zod_test.go +++ b/zod_test.go @@ -11,6 +11,44 @@ import ( "github.com/xorcare/golden" ) +// goldenMeta holds metadata written as comments at the top of golden files. +type goldenMeta struct { + zodVersion string // "v3", "v4", or "" (works with all versions) + noTypecheck bool // opt out of docker type-check tests +} + +type goldenOpt func(*goldenMeta) + +func withGoldenZodVersion(v string) goldenOpt { + return func(m *goldenMeta) { m.zodVersion = v } +} + +// goldenAssert wraps golden.Assert, prepending metadata comments to the file. +// The metadata is used by the docker type-check script to determine which zod +// version to install and whether to include the file in type checking. +// +// All golden files are type-checked by default. Use withGoldenNoTypecheck() to +// opt out of type checking for files with known issues. +func goldenAssert(t *testing.T, data []byte, opts ...goldenOpt) { + t.Helper() + var meta goldenMeta + for _, o := range opts { + o(&meta) + } + var lines []string + if meta.zodVersion != "" { + lines = append(lines, "// @zod-version: "+meta.zodVersion) + } + if !meta.noTypecheck { + lines = append(lines, "// @typecheck") + } + if len(lines) > 0 { + header := strings.Join(lines, "\n") + "\n" + data = append([]byte(header), data...) + } + golden.Assert(t, data) +} + // assertSchema is a golden file test helper for Zod schema output. // // When no versions are specified, it asserts that v3 and v4 produce identical @@ -36,13 +74,13 @@ func assertSchema(t *testing.T, schema any, versions ...string) { v3out := StructToZodSchema(schema, WithZodV3()) v4out := StructToZodSchema(schema) assert.Equal(t, v3out, v4out) - golden.Assert(t, []byte(v4out)) + goldenAssert(t, []byte(v4out)) case 1: - golden.Assert(t, []byte(StructToZodSchema(schema, optsFor(versions[0])...))) + goldenAssert(t, []byte(StructToZodSchema(schema, optsFor(versions[0])...)), withGoldenZodVersion(versions[0])) default: for _, ver := range versions { t.Run(ver, func(t *testing.T) { - golden.Assert(t, []byte(StructToZodSchema(schema, optsFor(ver)...))) + goldenAssert(t, []byte(StructToZodSchema(schema, optsFor(ver)...)), withGoldenZodVersion(ver)) }) } } @@ -126,7 +164,7 @@ func TestStructSimplePrefix(t *testing.T) { v3out := StructToZodSchema(User{}, WithPrefix("Bot"), WithZodV3()) v4out := StructToZodSchema(User{}, WithPrefix("Bot")) assert.Equal(t, v3out, v4out) - golden.Assert(t, []byte(v4out)) + goldenAssert(t, []byte(v4out)) } func TestNestedStruct(t *testing.T) { @@ -737,7 +775,7 @@ func TestZodV4Defaults(t *testing.T) { }, } - golden.Assert(t, []byte(NewConverterWithOpts(WithCustomTags(customTagHandlers)).Convert(Payload{}))) + goldenAssert(t, []byte(NewConverterWithOpts(WithCustomTags(customTagHandlers)).Convert(Payload{})), withGoldenZodVersion("v4")) }) t.Run("ip unions inherit generic string constraints", func(t *testing.T) { @@ -748,6 +786,14 @@ func TestZodV4Defaults(t *testing.T) { assertSchema(t, Payload{}, "v4") }) + t.Run("ip unions work when chain constraints precede ip tag", func(t *testing.T) { + type Payload struct { + Address string `validate:"required,ip"` + } + + assertSchema(t, Payload{}, "v3", "v4") + }) + t.Run("oneof takes precedence over ip specialization", func(t *testing.T) { type Payload struct { Address string `validate:"oneof='127.0.0.1' '::1',ip"` @@ -761,7 +807,7 @@ func TestZodV4Defaults(t *testing.T) { Address string `validate:"email,ip"` } - assertSchema(t, Payload{}, "v4") + goldenAssert(t, []byte(StructToZodSchema(Payload{})), withGoldenZodVersion("v4")) }) t.Run("enum keyed maps become partial records", func(t *testing.T) { @@ -783,7 +829,7 @@ func TestZodV4Defaults(t *testing.T) { Next *Node `json:"next"` } - assertSchema(t, Node{}, "v4") + goldenAssert(t, []byte(StructToZodSchema(Node{})), withGoldenZodVersion("v4")) }) t.Run("recursive embedded shapes keep named fields before spreads", func(t *testing.T) { @@ -1139,7 +1185,7 @@ func TestCustom(t *testing.T) { v3out := v3c.Convert(User{}) v4out := v4c.Convert(User{}) assert.Equal(t, v3out, v4out) - golden.Assert(t, []byte(v4out)) + goldenAssert(t, []byte(v4out)) } func TestEverything(t *testing.T) { @@ -1267,7 +1313,7 @@ func TestConvertSlice(t *testing.T) { v3out := v3c.ConvertSlice(types) v4out := v4c.ConvertSlice(types) assert.Equal(t, v3out, v4out) - golden.Assert(t, []byte(v4out)) + goldenAssert(t, []byte(v4out)) } func TestConvertSliceWithValidations(t *testing.T) { @@ -1430,7 +1476,7 @@ func TestGenerics(t *testing.T) { v3out := v3c.Export() v4out := c.Export() assert.Equal(t, v3out, v4out) - golden.Assert(t, []byte(v4out)) + goldenAssert(t, []byte(v4out)) } func TestSliceFields(t *testing.T) { @@ -1479,10 +1525,10 @@ func TestCustomTag(t *testing.T) { } t.Run("v3", func(t *testing.T) { - golden.Assert(t, []byte(NewConverterWithOpts(WithCustomTags(customTagHandlers), WithZodV3()).Convert(Request{}))) + goldenAssert(t, []byte(NewConverterWithOpts(WithCustomTags(customTagHandlers), WithZodV3()).Convert(Request{})), withGoldenZodVersion("v3")) }) t.Run("v4", func(t *testing.T) { - golden.Assert(t, []byte(NewConverterWithOpts(WithCustomTags(customTagHandlers)).Convert(Request{}))) + goldenAssert(t, []byte(NewConverterWithOpts(WithCustomTags(customTagHandlers)).Convert(Request{})), withGoldenZodVersion("v4")) }) } @@ -1522,7 +1568,7 @@ func TestRecursiveEmbeddedStruct(t *testing.T) { c.AddType(ItemD{}) c.AddType(ItemE{}) c.AddType(ItemF{}) - golden.Assert(t, []byte(c.Export())) + goldenAssert(t, []byte(c.Export()), withGoldenZodVersion("v3")) }) t.Run("v4", func(t *testing.T) { c := NewConverterWithOpts() @@ -1532,7 +1578,7 @@ func TestRecursiveEmbeddedStruct(t *testing.T) { c.AddType(ItemD{}) c.AddType(ItemE{}) c.AddType(ItemF{}) - golden.Assert(t, []byte(c.Export())) + goldenAssert(t, []byte(c.Export()), withGoldenZodVersion("v4")) }) } From 17dff2dc003271c389ca3fba3c0ca092ebc1c06b Mon Sep 17 00:00:00 2001 From: Ramil Amparo Date: Wed, 1 Apr 2026 19:54:55 +0400 Subject: [PATCH 04/35] Fix comment --- zod_test.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/zod_test.go b/zod_test.go index f0ed18d..b368423 100644 --- a/zod_test.go +++ b/zod_test.go @@ -27,8 +27,7 @@ func withGoldenZodVersion(v string) goldenOpt { // The metadata is used by the docker type-check script to determine which zod // version to install and whether to include the file in type checking. // -// All golden files are type-checked by default. Use withGoldenNoTypecheck() to -// opt out of type checking for files with known issues. +// All golden files are type-checked by default. func goldenAssert(t *testing.T, data []byte, opts ...goldenOpt) { t.Helper() var meta goldenMeta From b6927cca234ab3f025c5a2f936b6701c8df112b2 Mon Sep 17 00:00:00 2001 From: Ramil Amparo Date: Wed, 1 Apr 2026 20:12:31 +0400 Subject: [PATCH 05/35] Fix comment --- docker-typecheck.sh | 3 --- 1 file changed, 3 deletions(-) diff --git a/docker-typecheck.sh b/docker-typecheck.sh index 1872fbe..dc062bd 100755 --- a/docker-typecheck.sh +++ b/docker-typecheck.sh @@ -5,9 +5,6 @@ # Golden files must contain these metadata comments to be included: # // @typecheck — present by default; files without it are skipped # // @zod-version: v3|v4 — (optional) restrict to one zod major; omit for both -# -# Usage: -# ./typecheck/docker-typecheck.sh set -euo pipefail From 58be5adabd3b10b2f957d3a872be2bd70f4a6a44 Mon Sep 17 00:00:00 2001 From: Ramil Amparo Date: Thu, 2 Apr 2026 20:12:52 +0400 Subject: [PATCH 06/35] Refactor string schema generation and consolidate tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit String schema generation: - Replace chunk-based approach with semantic validators (stringValidator) that separate parsing from rendering - Remove stringSchemaParts struct — renderStringSchema returns string directly - Add renderV3Chain, renderV4Chain, renderV4FormatBase for version-specific output - Skip redundant .min(1) from required when format validators are present (they already reject empty strings), except base64/hex in v4 which accept empty - Panic on impossible combinations: format+union (email,ip) and multiple formats (email,url) - Add AddTypeWithName for registering anonymous structs with custom names Tests: - Add buildValidatorConverter and assertValidators shared helpers for table-driven golden file tests with dynamic structs - Consolidate TestStringValidations from 30+ subtests into table-driven test (2 golden files) - Consolidate TestNumberValidations, TestMapWithValidations, TestConvertSliceWithValidations into table-driven tests - Consolidate TestFormatValidators: all 25 format + 2 union tags tested bare and with required modifier (8 golden files) - Add test for dive,oneof on slices - Add tests for format+union panic and multiple formats panic - Remove 24 redundant individual format tests from TestStringValidations - Remove 4 redundant tests from TestZodV4Defaults - Golden files reduced from 171 to 75 Co-Authored-By: Claude Opus 4.6 (1M context) --- .../TestConvertSliceWithValidations.golden | 51 ++ .../dive1.golden | 6 - .../dive2.golden | 6 - .../dive_nested.golden | 11 + .../dive_oneof.golden | 6 + .../TestConvertSliceWithValidations/eq.golden | 6 - .../TestConvertSliceWithValidations/gt.golden | 6 - .../gte.golden | 6 - .../len.golden | 6 - .../TestConvertSliceWithValidations/lt.golden | 6 - .../lte.golden | 6 - .../max.golden | 6 - .../min.golden | 6 - .../TestConvertSliceWithValidations/ne.golden | 6 - .../required.golden | 6 - .../format_only/v3.golden | 117 +++ .../format_only/v4.golden | 117 +++ .../format_with_required/v3.golden | 117 +++ .../format_with_required/v4.golden | 117 +++ .../TestFormatValidators/union_only/v3.golden | 12 + .../TestFormatValidators/union_only/v4.golden | 12 + .../union_with_required/v3.golden | 12 + .../union_with_required/v4.golden | 12 + testdata/TestMapWithValidations.golden | 61 ++ testdata/TestMapWithValidations/dive1.golden | 6 - testdata/TestMapWithValidations/dive2.golden | 6 - testdata/TestMapWithValidations/dive3.golden | 6 - .../TestMapWithValidations/dive_nested.golden | 11 + testdata/TestMapWithValidations/eq.golden | 6 - testdata/TestMapWithValidations/gt.golden | 6 - testdata/TestMapWithValidations/gte.golden | 6 - testdata/TestMapWithValidations/len.golden | 6 - testdata/TestMapWithValidations/lt.golden | 6 - testdata/TestMapWithValidations/lte.golden | 6 - testdata/TestMapWithValidations/max.golden | 6 - testdata/TestMapWithValidations/min.golden | 6 - testdata/TestMapWithValidations/minmax.golden | 6 - testdata/TestMapWithValidations/ne.golden | 6 - .../TestMapWithValidations/required.golden | 6 - testdata/TestNumberValidations.golden | 36 + testdata/TestNumberValidations/eq.golden | 6 - testdata/TestNumberValidations/gt_lt.golden | 6 - testdata/TestNumberValidations/gte_lte.golden | 6 - testdata/TestNumberValidations/len.golden | 6 - testdata/TestNumberValidations/min_max.golden | 6 - testdata/TestNumberValidations/ne.golden | 6 - testdata/TestNumberValidations/oneof.golden | 6 - testdata/TestStringValidations.golden | Bin 0 -> 5206 bytes testdata/TestStringValidations/alpha.golden | 6 - .../TestStringValidations/alphanum.golden | 6 - .../alphanumunicode.golden | 6 - .../TestStringValidations/alphaunicode.golden | 6 - testdata/TestStringValidations/ascii.golden | Bin 142 -> 0 bytes .../TestStringValidations/base64/v3.golden | 7 - .../TestStringValidations/base64/v4.golden | 7 - testdata/TestStringValidations/boolean.golden | 6 - .../TestStringValidations/contains.golden | 6 - .../TestStringValidations/datetime/v3.golden | 7 - .../TestStringValidations/datetime/v4.golden | 7 - .../TestStringValidations/email/v3.golden | 7 - .../TestStringValidations/email/v4.golden | 7 - .../TestStringValidations/endswith.golden | 6 - testdata/TestStringValidations/eq.golden | 6 - testdata/TestStringValidations/gt.golden | 6 - testdata/TestStringValidations/gte.golden | 6 - .../hexadecimal/v3.golden | 7 - .../hexadecimal/v4.golden | 7 - .../TestStringValidations/http_url/v3.golden | 7 - .../TestStringValidations/http_url/v4.golden | 7 - testdata/TestStringValidations/ip/v3.golden | 7 - testdata/TestStringValidations/ip/v4.golden | 7 - .../TestStringValidations/ip4_addr/v3.golden | 7 - .../TestStringValidations/ip4_addr/v4.golden | 7 - .../TestStringValidations/ip6_addr/v3.golden | 7 - .../TestStringValidations/ip6_addr/v4.golden | 7 - .../TestStringValidations/ip_addr/v3.golden | 7 - .../TestStringValidations/ip_addr/v4.golden | 7 - testdata/TestStringValidations/ipv4/v3.golden | 7 - testdata/TestStringValidations/ipv4/v4.golden | 7 - testdata/TestStringValidations/ipv6/v3.golden | 7 - testdata/TestStringValidations/ipv6/v4.golden | 7 - testdata/TestStringValidations/json.golden | 6 - .../TestStringValidations/latitude.golden | 6 - testdata/TestStringValidations/len.golden | 6 - .../TestStringValidations/longitude.golden | 6 - .../TestStringValidations/lowercase.golden | 6 - testdata/TestStringValidations/lt.golden | 6 - testdata/TestStringValidations/lte.golden | 6 - testdata/TestStringValidations/max.golden | 6 - testdata/TestStringValidations/md4.golden | 6 - testdata/TestStringValidations/md5/v3.golden | 7 - testdata/TestStringValidations/md5/v4.golden | 7 - testdata/TestStringValidations/min.golden | 6 - testdata/TestStringValidations/minmax.golden | 6 - testdata/TestStringValidations/mongodb.golden | 6 - testdata/TestStringValidations/ne.golden | 6 - testdata/TestStringValidations/number.golden | 6 - testdata/TestStringValidations/numeric.golden | 6 - testdata/TestStringValidations/oneof.golden | 6 - .../oneof_separated.golden | 6 - .../TestStringValidations/required.golden | 6 - .../TestStringValidations/sha256/v3.golden | 7 - .../TestStringValidations/sha256/v4.golden | 7 - .../TestStringValidations/sha384/v3.golden | 7 - .../TestStringValidations/sha384/v4.golden | 7 - .../TestStringValidations/sha512/v3.golden | 7 - .../TestStringValidations/sha512/v4.golden | 7 - .../TestStringValidations/startswith.golden | 6 - .../TestStringValidations/uppercase.golden | 6 - testdata/TestStringValidations/url/v3.golden | 7 - testdata/TestStringValidations/url/v4.golden | 7 - .../TestStringValidations/url_encoded.golden | 6 - testdata/TestStringValidations/uuid/v3.golden | 7 - testdata/TestStringValidations/uuid/v4.golden | 7 - .../TestStringValidations/uuid3/v3.golden | 7 - .../TestStringValidations/uuid3/v4.golden | 7 - .../uuid3_rfc4122/v3.golden | 7 - .../uuid3_rfc4122/v4.golden | 7 - .../TestStringValidations/uuid4/v3.golden | 7 - .../TestStringValidations/uuid4/v4.golden | 7 - .../uuid4_rfc4122/v3.golden | 7 - .../uuid4_rfc4122/v4.golden | 7 - .../TestStringValidations/uuid5/v3.golden | 7 - .../TestStringValidations/uuid5/v4.golden | 7 - .../uuid5_rfc4122/v3.golden | 7 - .../uuid5_rfc4122/v4.golden | 7 - .../uuid_rfc4122/v3.golden | 7 - .../uuid_rfc4122/v4.golden | 7 - ..._inherit_generic_string_constraints.golden | 2 +- .../v4.golden | 7 - .../v3.golden | 2 +- .../v4.golden} | 2 +- zod.go | 620 +++++++++---- zod_test.go | 844 ++++-------------- 134 files changed, 1324 insertions(+), 1565 deletions(-) create mode 100644 testdata/TestConvertSliceWithValidations.golden delete mode 100644 testdata/TestConvertSliceWithValidations/dive1.golden delete mode 100644 testdata/TestConvertSliceWithValidations/dive2.golden create mode 100644 testdata/TestConvertSliceWithValidations/dive_nested.golden create mode 100644 testdata/TestConvertSliceWithValidations/dive_oneof.golden delete mode 100644 testdata/TestConvertSliceWithValidations/eq.golden delete mode 100644 testdata/TestConvertSliceWithValidations/gt.golden delete mode 100644 testdata/TestConvertSliceWithValidations/gte.golden delete mode 100644 testdata/TestConvertSliceWithValidations/len.golden delete mode 100644 testdata/TestConvertSliceWithValidations/lt.golden delete mode 100644 testdata/TestConvertSliceWithValidations/lte.golden delete mode 100644 testdata/TestConvertSliceWithValidations/max.golden delete mode 100644 testdata/TestConvertSliceWithValidations/min.golden delete mode 100644 testdata/TestConvertSliceWithValidations/ne.golden delete mode 100644 testdata/TestConvertSliceWithValidations/required.golden create mode 100644 testdata/TestFormatValidators/format_only/v3.golden create mode 100644 testdata/TestFormatValidators/format_only/v4.golden create mode 100644 testdata/TestFormatValidators/format_with_required/v3.golden create mode 100644 testdata/TestFormatValidators/format_with_required/v4.golden create mode 100644 testdata/TestFormatValidators/union_only/v3.golden create mode 100644 testdata/TestFormatValidators/union_only/v4.golden create mode 100644 testdata/TestFormatValidators/union_with_required/v3.golden create mode 100644 testdata/TestFormatValidators/union_with_required/v4.golden create mode 100644 testdata/TestMapWithValidations.golden delete mode 100644 testdata/TestMapWithValidations/dive1.golden delete mode 100644 testdata/TestMapWithValidations/dive2.golden delete mode 100644 testdata/TestMapWithValidations/dive3.golden create mode 100644 testdata/TestMapWithValidations/dive_nested.golden delete mode 100644 testdata/TestMapWithValidations/eq.golden delete mode 100644 testdata/TestMapWithValidations/gt.golden delete mode 100644 testdata/TestMapWithValidations/gte.golden delete mode 100644 testdata/TestMapWithValidations/len.golden delete mode 100644 testdata/TestMapWithValidations/lt.golden delete mode 100644 testdata/TestMapWithValidations/lte.golden delete mode 100644 testdata/TestMapWithValidations/max.golden delete mode 100644 testdata/TestMapWithValidations/min.golden delete mode 100644 testdata/TestMapWithValidations/minmax.golden delete mode 100644 testdata/TestMapWithValidations/ne.golden delete mode 100644 testdata/TestMapWithValidations/required.golden create mode 100644 testdata/TestNumberValidations.golden delete mode 100644 testdata/TestNumberValidations/eq.golden delete mode 100644 testdata/TestNumberValidations/gt_lt.golden delete mode 100644 testdata/TestNumberValidations/gte_lte.golden delete mode 100644 testdata/TestNumberValidations/len.golden delete mode 100644 testdata/TestNumberValidations/min_max.golden delete mode 100644 testdata/TestNumberValidations/ne.golden delete mode 100644 testdata/TestNumberValidations/oneof.golden create mode 100644 testdata/TestStringValidations.golden delete mode 100644 testdata/TestStringValidations/alpha.golden delete mode 100644 testdata/TestStringValidations/alphanum.golden delete mode 100644 testdata/TestStringValidations/alphanumunicode.golden delete mode 100644 testdata/TestStringValidations/alphaunicode.golden delete mode 100644 testdata/TestStringValidations/ascii.golden delete mode 100644 testdata/TestStringValidations/base64/v3.golden delete mode 100644 testdata/TestStringValidations/base64/v4.golden delete mode 100644 testdata/TestStringValidations/boolean.golden delete mode 100644 testdata/TestStringValidations/contains.golden delete mode 100644 testdata/TestStringValidations/datetime/v3.golden delete mode 100644 testdata/TestStringValidations/datetime/v4.golden delete mode 100644 testdata/TestStringValidations/email/v3.golden delete mode 100644 testdata/TestStringValidations/email/v4.golden delete mode 100644 testdata/TestStringValidations/endswith.golden delete mode 100644 testdata/TestStringValidations/eq.golden delete mode 100644 testdata/TestStringValidations/gt.golden delete mode 100644 testdata/TestStringValidations/gte.golden delete mode 100644 testdata/TestStringValidations/hexadecimal/v3.golden delete mode 100644 testdata/TestStringValidations/hexadecimal/v4.golden delete mode 100644 testdata/TestStringValidations/http_url/v3.golden delete mode 100644 testdata/TestStringValidations/http_url/v4.golden delete mode 100644 testdata/TestStringValidations/ip/v3.golden delete mode 100644 testdata/TestStringValidations/ip/v4.golden delete mode 100644 testdata/TestStringValidations/ip4_addr/v3.golden delete mode 100644 testdata/TestStringValidations/ip4_addr/v4.golden delete mode 100644 testdata/TestStringValidations/ip6_addr/v3.golden delete mode 100644 testdata/TestStringValidations/ip6_addr/v4.golden delete mode 100644 testdata/TestStringValidations/ip_addr/v3.golden delete mode 100644 testdata/TestStringValidations/ip_addr/v4.golden delete mode 100644 testdata/TestStringValidations/ipv4/v3.golden delete mode 100644 testdata/TestStringValidations/ipv4/v4.golden delete mode 100644 testdata/TestStringValidations/ipv6/v3.golden delete mode 100644 testdata/TestStringValidations/ipv6/v4.golden delete mode 100644 testdata/TestStringValidations/json.golden delete mode 100644 testdata/TestStringValidations/latitude.golden delete mode 100644 testdata/TestStringValidations/len.golden delete mode 100644 testdata/TestStringValidations/longitude.golden delete mode 100644 testdata/TestStringValidations/lowercase.golden delete mode 100644 testdata/TestStringValidations/lt.golden delete mode 100644 testdata/TestStringValidations/lte.golden delete mode 100644 testdata/TestStringValidations/max.golden delete mode 100644 testdata/TestStringValidations/md4.golden delete mode 100644 testdata/TestStringValidations/md5/v3.golden delete mode 100644 testdata/TestStringValidations/md5/v4.golden delete mode 100644 testdata/TestStringValidations/min.golden delete mode 100644 testdata/TestStringValidations/minmax.golden delete mode 100644 testdata/TestStringValidations/mongodb.golden delete mode 100644 testdata/TestStringValidations/ne.golden delete mode 100644 testdata/TestStringValidations/number.golden delete mode 100644 testdata/TestStringValidations/numeric.golden delete mode 100644 testdata/TestStringValidations/oneof.golden delete mode 100644 testdata/TestStringValidations/oneof_separated.golden delete mode 100644 testdata/TestStringValidations/required.golden delete mode 100644 testdata/TestStringValidations/sha256/v3.golden delete mode 100644 testdata/TestStringValidations/sha256/v4.golden delete mode 100644 testdata/TestStringValidations/sha384/v3.golden delete mode 100644 testdata/TestStringValidations/sha384/v4.golden delete mode 100644 testdata/TestStringValidations/sha512/v3.golden delete mode 100644 testdata/TestStringValidations/sha512/v4.golden delete mode 100644 testdata/TestStringValidations/startswith.golden delete mode 100644 testdata/TestStringValidations/uppercase.golden delete mode 100644 testdata/TestStringValidations/url/v3.golden delete mode 100644 testdata/TestStringValidations/url/v4.golden delete mode 100644 testdata/TestStringValidations/url_encoded.golden delete mode 100644 testdata/TestStringValidations/uuid/v3.golden delete mode 100644 testdata/TestStringValidations/uuid/v4.golden delete mode 100644 testdata/TestStringValidations/uuid3/v3.golden delete mode 100644 testdata/TestStringValidations/uuid3/v4.golden delete mode 100644 testdata/TestStringValidations/uuid3_rfc4122/v3.golden delete mode 100644 testdata/TestStringValidations/uuid3_rfc4122/v4.golden delete mode 100644 testdata/TestStringValidations/uuid4/v3.golden delete mode 100644 testdata/TestStringValidations/uuid4/v4.golden delete mode 100644 testdata/TestStringValidations/uuid4_rfc4122/v3.golden delete mode 100644 testdata/TestStringValidations/uuid4_rfc4122/v4.golden delete mode 100644 testdata/TestStringValidations/uuid5/v3.golden delete mode 100644 testdata/TestStringValidations/uuid5/v4.golden delete mode 100644 testdata/TestStringValidations/uuid5_rfc4122/v3.golden delete mode 100644 testdata/TestStringValidations/uuid5_rfc4122/v4.golden delete mode 100644 testdata/TestStringValidations/uuid_rfc4122/v3.golden delete mode 100644 testdata/TestStringValidations/uuid_rfc4122/v4.golden delete mode 100644 testdata/TestZodV4Defaults/ip_unions_work_when_chain_constraints_precede_ip_tag/v4.golden rename testdata/TestZodV4Defaults/{ip_unions_work_when_chain_constraints_precede_ip_tag => optional_format_with_nullable_pointer}/v3.golden (76%) rename testdata/TestZodV4Defaults/{ip_mixed_with_another_format_falls_back_to_legacy_chain_semantics.golden => optional_format_with_nullable_pointer/v4.golden} (80%) diff --git a/testdata/TestConvertSliceWithValidations.golden b/testdata/TestConvertSliceWithValidations.golden new file mode 100644 index 0000000..2514c8c --- /dev/null +++ b/testdata/TestConvertSliceWithValidations.golden @@ -0,0 +1,51 @@ +// @typecheck +export const requiredSchema = z.object({ + value: z.string().array(), +}) +export type required = z.infer + +export const minSchema = z.object({ + value: z.string().array().min(1), +}) +export type min = z.infer + +export const maxSchema = z.object({ + value: z.string().array().max(1), +}) +export type max = z.infer + +export const lenSchema = z.object({ + value: z.string().array().length(1), +}) +export type len = z.infer + +export const eqSchema = z.object({ + value: z.string().array().length(1), +}) +export type eq = z.infer + +export const gtSchema = z.object({ + value: z.string().array().min(2), +}) +export type gt = z.infer + +export const gteSchema = z.object({ + value: z.string().array().min(1), +}) +export type gte = z.infer + +export const ltSchema = z.object({ + value: z.string().array().max(0), +}) +export type lt = z.infer + +export const lteSchema = z.object({ + value: z.string().array().max(1), +}) +export type lte = z.infer + +export const neSchema = z.object({ + value: z.string().array().refine((val) => val.length !== 0), +}) +export type ne = z.infer + diff --git a/testdata/TestConvertSliceWithValidations/dive1.golden b/testdata/TestConvertSliceWithValidations/dive1.golden deleted file mode 100644 index ea90650..0000000 --- a/testdata/TestConvertSliceWithValidations/dive1.golden +++ /dev/null @@ -1,6 +0,0 @@ -// @typecheck -export const Dive1Schema = z.object({ - Slice: z.string().array().array().nullable(), -}) -export type Dive1 = z.infer - diff --git a/testdata/TestConvertSliceWithValidations/dive2.golden b/testdata/TestConvertSliceWithValidations/dive2.golden deleted file mode 100644 index adef972..0000000 --- a/testdata/TestConvertSliceWithValidations/dive2.golden +++ /dev/null @@ -1,6 +0,0 @@ -// @typecheck -export const Dive2Schema = z.object({ - Slice: z.string().array().min(1).array(), -}) -export type Dive2 = z.infer - diff --git a/testdata/TestConvertSliceWithValidations/dive_nested.golden b/testdata/TestConvertSliceWithValidations/dive_nested.golden new file mode 100644 index 0000000..9e68e8d --- /dev/null +++ b/testdata/TestConvertSliceWithValidations/dive_nested.golden @@ -0,0 +1,11 @@ +// @typecheck +export const dive1Schema = z.object({ + value: z.string().array().array().nullable(), +}) +export type dive1 = z.infer + +export const dive2Schema = z.object({ + value: z.string().array().min(1).array(), +}) +export type dive2 = z.infer + diff --git a/testdata/TestConvertSliceWithValidations/dive_oneof.golden b/testdata/TestConvertSliceWithValidations/dive_oneof.golden new file mode 100644 index 0000000..bacea2d --- /dev/null +++ b/testdata/TestConvertSliceWithValidations/dive_oneof.golden @@ -0,0 +1,6 @@ +// @typecheck +export const dive_oneofSchema = z.object({ + value: z.enum(["a", "b", "c"] as const).array().nullable(), +}) +export type dive_oneof = z.infer + diff --git a/testdata/TestConvertSliceWithValidations/eq.golden b/testdata/TestConvertSliceWithValidations/eq.golden deleted file mode 100644 index 384b49a..0000000 --- a/testdata/TestConvertSliceWithValidations/eq.golden +++ /dev/null @@ -1,6 +0,0 @@ -// @typecheck -export const EqSchema = z.object({ - Slice: z.string().array().length(1), -}) -export type Eq = z.infer - diff --git a/testdata/TestConvertSliceWithValidations/gt.golden b/testdata/TestConvertSliceWithValidations/gt.golden deleted file mode 100644 index 16f1329..0000000 --- a/testdata/TestConvertSliceWithValidations/gt.golden +++ /dev/null @@ -1,6 +0,0 @@ -// @typecheck -export const GtSchema = z.object({ - Slice: z.string().array().min(2), -}) -export type Gt = z.infer - diff --git a/testdata/TestConvertSliceWithValidations/gte.golden b/testdata/TestConvertSliceWithValidations/gte.golden deleted file mode 100644 index 263ba9c..0000000 --- a/testdata/TestConvertSliceWithValidations/gte.golden +++ /dev/null @@ -1,6 +0,0 @@ -// @typecheck -export const GteSchema = z.object({ - Slice: z.string().array().min(1), -}) -export type Gte = z.infer - diff --git a/testdata/TestConvertSliceWithValidations/len.golden b/testdata/TestConvertSliceWithValidations/len.golden deleted file mode 100644 index c52a3cf..0000000 --- a/testdata/TestConvertSliceWithValidations/len.golden +++ /dev/null @@ -1,6 +0,0 @@ -// @typecheck -export const LenSchema = z.object({ - Slice: z.string().array().length(1), -}) -export type Len = z.infer - diff --git a/testdata/TestConvertSliceWithValidations/lt.golden b/testdata/TestConvertSliceWithValidations/lt.golden deleted file mode 100644 index 5c89d96..0000000 --- a/testdata/TestConvertSliceWithValidations/lt.golden +++ /dev/null @@ -1,6 +0,0 @@ -// @typecheck -export const LtSchema = z.object({ - Slice: z.string().array().max(0), -}) -export type Lt = z.infer - diff --git a/testdata/TestConvertSliceWithValidations/lte.golden b/testdata/TestConvertSliceWithValidations/lte.golden deleted file mode 100644 index cef88a9..0000000 --- a/testdata/TestConvertSliceWithValidations/lte.golden +++ /dev/null @@ -1,6 +0,0 @@ -// @typecheck -export const LteSchema = z.object({ - Slice: z.string().array().max(1), -}) -export type Lte = z.infer - diff --git a/testdata/TestConvertSliceWithValidations/max.golden b/testdata/TestConvertSliceWithValidations/max.golden deleted file mode 100644 index dd94985..0000000 --- a/testdata/TestConvertSliceWithValidations/max.golden +++ /dev/null @@ -1,6 +0,0 @@ -// @typecheck -export const MaxSchema = z.object({ - Slice: z.string().array().max(1), -}) -export type Max = z.infer - diff --git a/testdata/TestConvertSliceWithValidations/min.golden b/testdata/TestConvertSliceWithValidations/min.golden deleted file mode 100644 index 79c069f..0000000 --- a/testdata/TestConvertSliceWithValidations/min.golden +++ /dev/null @@ -1,6 +0,0 @@ -// @typecheck -export const MinSchema = z.object({ - Slice: z.string().array().min(1), -}) -export type Min = z.infer - diff --git a/testdata/TestConvertSliceWithValidations/ne.golden b/testdata/TestConvertSliceWithValidations/ne.golden deleted file mode 100644 index 00208f2..0000000 --- a/testdata/TestConvertSliceWithValidations/ne.golden +++ /dev/null @@ -1,6 +0,0 @@ -// @typecheck -export const NeSchema = z.object({ - Slice: z.string().array().refine((val) => val.length !== 0), -}) -export type Ne = z.infer - diff --git a/testdata/TestConvertSliceWithValidations/required.golden b/testdata/TestConvertSliceWithValidations/required.golden deleted file mode 100644 index bdbea1f..0000000 --- a/testdata/TestConvertSliceWithValidations/required.golden +++ /dev/null @@ -1,6 +0,0 @@ -// @typecheck -export const RequiredSchema = z.object({ - Slice: z.string().array(), -}) -export type Required = z.infer - diff --git a/testdata/TestFormatValidators/format_only/v3.golden b/testdata/TestFormatValidators/format_only/v3.golden new file mode 100644 index 0000000..6c3af71 --- /dev/null +++ b/testdata/TestFormatValidators/format_only/v3.golden @@ -0,0 +1,117 @@ +// @zod-version: v3 +// @typecheck +export const emailSchema = z.object({ + value: z.string().email(), +}) +export type email = z.infer + +export const urlSchema = z.object({ + value: z.string().url(), +}) +export type url = z.infer + +export const http_urlSchema = z.object({ + value: z.string().url(), +}) +export type http_url = z.infer + +export const ipv4Schema = z.object({ + value: z.string().ip({ version: "v4" }), +}) +export type ipv4 = z.infer + +export const ip4_addrSchema = z.object({ + value: z.string().ip({ version: "v4" }), +}) +export type ip4_addr = z.infer + +export const ipv6Schema = z.object({ + value: z.string().ip({ version: "v6" }), +}) +export type ipv6 = z.infer + +export const ip6_addrSchema = z.object({ + value: z.string().ip({ version: "v6" }), +}) +export type ip6_addr = z.infer + +export const base64Schema = z.object({ + value: z.string().regex(/^(?:[A-Za-z0-9+\/]{4})*(?:[A-Za-z0-9+\/]{2}==|[A-Za-z0-9+\/]{3}=|[A-Za-z0-9+\/]{4})$/), +}) +export type base64 = z.infer + +export const datetimeSchema = z.object({ + value: z.string().datetime(), +}) +export type datetime = z.infer + +export const hexadecimalSchema = z.object({ + value: z.string().regex(/^(0[xX])?[0-9a-fA-F]+$/), +}) +export type hexadecimal = z.infer + +export const jwtSchema = z.object({ + value: z.string().regex(/^[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]*$/), +}) +export type jwt = z.infer + +export const uuidSchema = z.object({ + value: z.string().regex(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/), +}) +export type uuid = z.infer + +export const uuid3Schema = z.object({ + value: z.string().regex(/^[0-9a-f]{8}-[0-9a-f]{4}-3[0-9a-f]{3}-[0-9a-f]{4}-[0-9a-f]{12}$/), +}) +export type uuid3 = z.infer + +export const uuid3_rfc4122Schema = z.object({ + value: z.string().regex(/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-3[0-9a-fA-F]{3}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/), +}) +export type uuid3_rfc4122 = z.infer + +export const uuid4Schema = z.object({ + value: z.string().regex(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/), +}) +export type uuid4 = z.infer + +export const uuid4_rfc4122Schema = z.object({ + value: z.string().regex(/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$/), +}) +export type uuid4_rfc4122 = z.infer + +export const uuid5Schema = z.object({ + value: z.string().regex(/^[0-9a-f]{8}-[0-9a-f]{4}-5[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/), +}) +export type uuid5 = z.infer + +export const uuid5_rfc4122Schema = z.object({ + value: z.string().regex(/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-5[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$/), +}) +export type uuid5_rfc4122 = z.infer + +export const uuid_rfc4122Schema = z.object({ + value: z.string().regex(/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/), +}) +export type uuid_rfc4122 = z.infer + +export const md5Schema = z.object({ + value: z.string().regex(/^[0-9a-f]{32}$/), +}) +export type md5 = z.infer + +export const sha256Schema = z.object({ + value: z.string().regex(/^[0-9a-f]{64}$/), +}) +export type sha256 = z.infer + +export const sha384Schema = z.object({ + value: z.string().regex(/^[0-9a-f]{96}$/), +}) +export type sha384 = z.infer + +export const sha512Schema = z.object({ + value: z.string().regex(/^[0-9a-f]{128}$/), +}) +export type sha512 = z.infer + diff --git a/testdata/TestFormatValidators/format_only/v4.golden b/testdata/TestFormatValidators/format_only/v4.golden new file mode 100644 index 0000000..01127bb --- /dev/null +++ b/testdata/TestFormatValidators/format_only/v4.golden @@ -0,0 +1,117 @@ +// @zod-version: v4 +// @typecheck +export const emailSchema = z.object({ + value: z.email(), +}) +export type email = z.infer + +export const urlSchema = z.object({ + value: z.url(), +}) +export type url = z.infer + +export const http_urlSchema = z.object({ + value: z.httpUrl(), +}) +export type http_url = z.infer + +export const ipv4Schema = z.object({ + value: z.ipv4(), +}) +export type ipv4 = z.infer + +export const ip4_addrSchema = z.object({ + value: z.ipv4(), +}) +export type ip4_addr = z.infer + +export const ipv6Schema = z.object({ + value: z.ipv6(), +}) +export type ipv6 = z.infer + +export const ip6_addrSchema = z.object({ + value: z.ipv6(), +}) +export type ip6_addr = z.infer + +export const base64Schema = z.object({ + value: z.base64(), +}) +export type base64 = z.infer + +export const datetimeSchema = z.object({ + value: z.iso.datetime(), +}) +export type datetime = z.infer + +export const hexadecimalSchema = z.object({ + value: z.hex(), +}) +export type hexadecimal = z.infer + +export const jwtSchema = z.object({ + value: z.jwt(), +}) +export type jwt = z.infer + +export const uuidSchema = z.object({ + value: z.uuid(), +}) +export type uuid = z.infer + +export const uuid3Schema = z.object({ + value: z.uuid({ version: "v3" }), +}) +export type uuid3 = z.infer + +export const uuid3_rfc4122Schema = z.object({ + value: z.uuid({ version: "v3" }), +}) +export type uuid3_rfc4122 = z.infer + +export const uuid4Schema = z.object({ + value: z.uuid({ version: "v4" }), +}) +export type uuid4 = z.infer + +export const uuid4_rfc4122Schema = z.object({ + value: z.uuid({ version: "v4" }), +}) +export type uuid4_rfc4122 = z.infer + +export const uuid5Schema = z.object({ + value: z.uuid({ version: "v5" }), +}) +export type uuid5 = z.infer + +export const uuid5_rfc4122Schema = z.object({ + value: z.uuid({ version: "v5" }), +}) +export type uuid5_rfc4122 = z.infer + +export const uuid_rfc4122Schema = z.object({ + value: z.uuid(), +}) +export type uuid_rfc4122 = z.infer + +export const md5Schema = z.object({ + value: z.hash("md5"), +}) +export type md5 = z.infer + +export const sha256Schema = z.object({ + value: z.hash("sha256"), +}) +export type sha256 = z.infer + +export const sha384Schema = z.object({ + value: z.hash("sha384"), +}) +export type sha384 = z.infer + +export const sha512Schema = z.object({ + value: z.hash("sha512"), +}) +export type sha512 = z.infer + diff --git a/testdata/TestFormatValidators/format_with_required/v3.golden b/testdata/TestFormatValidators/format_with_required/v3.golden new file mode 100644 index 0000000..6c3af71 --- /dev/null +++ b/testdata/TestFormatValidators/format_with_required/v3.golden @@ -0,0 +1,117 @@ +// @zod-version: v3 +// @typecheck +export const emailSchema = z.object({ + value: z.string().email(), +}) +export type email = z.infer + +export const urlSchema = z.object({ + value: z.string().url(), +}) +export type url = z.infer + +export const http_urlSchema = z.object({ + value: z.string().url(), +}) +export type http_url = z.infer + +export const ipv4Schema = z.object({ + value: z.string().ip({ version: "v4" }), +}) +export type ipv4 = z.infer + +export const ip4_addrSchema = z.object({ + value: z.string().ip({ version: "v4" }), +}) +export type ip4_addr = z.infer + +export const ipv6Schema = z.object({ + value: z.string().ip({ version: "v6" }), +}) +export type ipv6 = z.infer + +export const ip6_addrSchema = z.object({ + value: z.string().ip({ version: "v6" }), +}) +export type ip6_addr = z.infer + +export const base64Schema = z.object({ + value: z.string().regex(/^(?:[A-Za-z0-9+\/]{4})*(?:[A-Za-z0-9+\/]{2}==|[A-Za-z0-9+\/]{3}=|[A-Za-z0-9+\/]{4})$/), +}) +export type base64 = z.infer + +export const datetimeSchema = z.object({ + value: z.string().datetime(), +}) +export type datetime = z.infer + +export const hexadecimalSchema = z.object({ + value: z.string().regex(/^(0[xX])?[0-9a-fA-F]+$/), +}) +export type hexadecimal = z.infer + +export const jwtSchema = z.object({ + value: z.string().regex(/^[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]*$/), +}) +export type jwt = z.infer + +export const uuidSchema = z.object({ + value: z.string().regex(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/), +}) +export type uuid = z.infer + +export const uuid3Schema = z.object({ + value: z.string().regex(/^[0-9a-f]{8}-[0-9a-f]{4}-3[0-9a-f]{3}-[0-9a-f]{4}-[0-9a-f]{12}$/), +}) +export type uuid3 = z.infer + +export const uuid3_rfc4122Schema = z.object({ + value: z.string().regex(/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-3[0-9a-fA-F]{3}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/), +}) +export type uuid3_rfc4122 = z.infer + +export const uuid4Schema = z.object({ + value: z.string().regex(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/), +}) +export type uuid4 = z.infer + +export const uuid4_rfc4122Schema = z.object({ + value: z.string().regex(/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$/), +}) +export type uuid4_rfc4122 = z.infer + +export const uuid5Schema = z.object({ + value: z.string().regex(/^[0-9a-f]{8}-[0-9a-f]{4}-5[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/), +}) +export type uuid5 = z.infer + +export const uuid5_rfc4122Schema = z.object({ + value: z.string().regex(/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-5[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$/), +}) +export type uuid5_rfc4122 = z.infer + +export const uuid_rfc4122Schema = z.object({ + value: z.string().regex(/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/), +}) +export type uuid_rfc4122 = z.infer + +export const md5Schema = z.object({ + value: z.string().regex(/^[0-9a-f]{32}$/), +}) +export type md5 = z.infer + +export const sha256Schema = z.object({ + value: z.string().regex(/^[0-9a-f]{64}$/), +}) +export type sha256 = z.infer + +export const sha384Schema = z.object({ + value: z.string().regex(/^[0-9a-f]{96}$/), +}) +export type sha384 = z.infer + +export const sha512Schema = z.object({ + value: z.string().regex(/^[0-9a-f]{128}$/), +}) +export type sha512 = z.infer + diff --git a/testdata/TestFormatValidators/format_with_required/v4.golden b/testdata/TestFormatValidators/format_with_required/v4.golden new file mode 100644 index 0000000..a2cdf46 --- /dev/null +++ b/testdata/TestFormatValidators/format_with_required/v4.golden @@ -0,0 +1,117 @@ +// @zod-version: v4 +// @typecheck +export const emailSchema = z.object({ + value: z.email(), +}) +export type email = z.infer + +export const urlSchema = z.object({ + value: z.url(), +}) +export type url = z.infer + +export const http_urlSchema = z.object({ + value: z.httpUrl(), +}) +export type http_url = z.infer + +export const ipv4Schema = z.object({ + value: z.ipv4(), +}) +export type ipv4 = z.infer + +export const ip4_addrSchema = z.object({ + value: z.ipv4(), +}) +export type ip4_addr = z.infer + +export const ipv6Schema = z.object({ + value: z.ipv6(), +}) +export type ipv6 = z.infer + +export const ip6_addrSchema = z.object({ + value: z.ipv6(), +}) +export type ip6_addr = z.infer + +export const base64Schema = z.object({ + value: z.base64().min(1), +}) +export type base64 = z.infer + +export const datetimeSchema = z.object({ + value: z.iso.datetime(), +}) +export type datetime = z.infer + +export const hexadecimalSchema = z.object({ + value: z.hex().min(1), +}) +export type hexadecimal = z.infer + +export const jwtSchema = z.object({ + value: z.jwt(), +}) +export type jwt = z.infer + +export const uuidSchema = z.object({ + value: z.uuid(), +}) +export type uuid = z.infer + +export const uuid3Schema = z.object({ + value: z.uuid({ version: "v3" }), +}) +export type uuid3 = z.infer + +export const uuid3_rfc4122Schema = z.object({ + value: z.uuid({ version: "v3" }), +}) +export type uuid3_rfc4122 = z.infer + +export const uuid4Schema = z.object({ + value: z.uuid({ version: "v4" }), +}) +export type uuid4 = z.infer + +export const uuid4_rfc4122Schema = z.object({ + value: z.uuid({ version: "v4" }), +}) +export type uuid4_rfc4122 = z.infer + +export const uuid5Schema = z.object({ + value: z.uuid({ version: "v5" }), +}) +export type uuid5 = z.infer + +export const uuid5_rfc4122Schema = z.object({ + value: z.uuid({ version: "v5" }), +}) +export type uuid5_rfc4122 = z.infer + +export const uuid_rfc4122Schema = z.object({ + value: z.uuid(), +}) +export type uuid_rfc4122 = z.infer + +export const md5Schema = z.object({ + value: z.hash("md5"), +}) +export type md5 = z.infer + +export const sha256Schema = z.object({ + value: z.hash("sha256"), +}) +export type sha256 = z.infer + +export const sha384Schema = z.object({ + value: z.hash("sha384"), +}) +export type sha384 = z.infer + +export const sha512Schema = z.object({ + value: z.hash("sha512"), +}) +export type sha512 = z.infer + diff --git a/testdata/TestFormatValidators/union_only/v3.golden b/testdata/TestFormatValidators/union_only/v3.golden new file mode 100644 index 0000000..0d4bbef --- /dev/null +++ b/testdata/TestFormatValidators/union_only/v3.golden @@ -0,0 +1,12 @@ +// @zod-version: v3 +// @typecheck +export const ipSchema = z.object({ + value: z.string().ip(), +}) +export type ip = z.infer + +export const ip_addrSchema = z.object({ + value: z.string().ip(), +}) +export type ip_addr = z.infer + diff --git a/testdata/TestFormatValidators/union_only/v4.golden b/testdata/TestFormatValidators/union_only/v4.golden new file mode 100644 index 0000000..47d8ae1 --- /dev/null +++ b/testdata/TestFormatValidators/union_only/v4.golden @@ -0,0 +1,12 @@ +// @zod-version: v4 +// @typecheck +export const ipSchema = z.object({ + value: z.union([z.ipv4(), z.ipv6()]), +}) +export type ip = z.infer + +export const ip_addrSchema = z.object({ + value: z.union([z.ipv4(), z.ipv6()]), +}) +export type ip_addr = z.infer + diff --git a/testdata/TestFormatValidators/union_with_required/v3.golden b/testdata/TestFormatValidators/union_with_required/v3.golden new file mode 100644 index 0000000..0d4bbef --- /dev/null +++ b/testdata/TestFormatValidators/union_with_required/v3.golden @@ -0,0 +1,12 @@ +// @zod-version: v3 +// @typecheck +export const ipSchema = z.object({ + value: z.string().ip(), +}) +export type ip = z.infer + +export const ip_addrSchema = z.object({ + value: z.string().ip(), +}) +export type ip_addr = z.infer + diff --git a/testdata/TestFormatValidators/union_with_required/v4.golden b/testdata/TestFormatValidators/union_with_required/v4.golden new file mode 100644 index 0000000..47d8ae1 --- /dev/null +++ b/testdata/TestFormatValidators/union_with_required/v4.golden @@ -0,0 +1,12 @@ +// @zod-version: v4 +// @typecheck +export const ipSchema = z.object({ + value: z.union([z.ipv4(), z.ipv6()]), +}) +export type ip = z.infer + +export const ip_addrSchema = z.object({ + value: z.union([z.ipv4(), z.ipv6()]), +}) +export type ip_addr = z.infer + diff --git a/testdata/TestMapWithValidations.golden b/testdata/TestMapWithValidations.golden new file mode 100644 index 0000000..6f1fd98 --- /dev/null +++ b/testdata/TestMapWithValidations.golden @@ -0,0 +1,61 @@ +// @typecheck +export const requiredSchema = z.object({ + value: z.record(z.string(), z.string()), +}) +export type required = z.infer + +export const minSchema = z.object({ + value: z.record(z.string(), z.string()).refine((val) => Object.keys(val).length >= 1, 'Map too small'), +}) +export type min = z.infer + +export const maxSchema = z.object({ + value: z.record(z.string(), z.string()).refine((val) => Object.keys(val).length <= 1, 'Map too large'), +}) +export type max = z.infer + +export const lenSchema = z.object({ + value: z.record(z.string(), z.string()).refine((val) => Object.keys(val).length === 1, 'Map wrong size'), +}) +export type len = z.infer + +export const minmaxSchema = z.object({ + value: z.record(z.string(), z.string()).refine((val) => Object.keys(val).length >= 1, 'Map too small').refine((val) => Object.keys(val).length <= 2, 'Map too large'), +}) +export type minmax = z.infer + +export const eqSchema = z.object({ + value: z.record(z.string(), z.string()).refine((val) => Object.keys(val).length === 1, 'Map wrong size'), +}) +export type eq = z.infer + +export const neSchema = z.object({ + value: z.record(z.string(), z.string()).refine((val) => Object.keys(val).length !== 1, 'Map wrong size'), +}) +export type ne = z.infer + +export const gtSchema = z.object({ + value: z.record(z.string(), z.string()).refine((val) => Object.keys(val).length > 1, 'Map too small'), +}) +export type gt = z.infer + +export const gteSchema = z.object({ + value: z.record(z.string(), z.string()).refine((val) => Object.keys(val).length >= 1, 'Map too small'), +}) +export type gte = z.infer + +export const ltSchema = z.object({ + value: z.record(z.string(), z.string()).refine((val) => Object.keys(val).length < 1, 'Map too large'), +}) +export type lt = z.infer + +export const lteSchema = z.object({ + value: z.record(z.string(), z.string()).refine((val) => Object.keys(val).length <= 1, 'Map too large'), +}) +export type lte = z.infer + +export const dive1Schema = z.object({ + value: 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 + diff --git a/testdata/TestMapWithValidations/dive1.golden b/testdata/TestMapWithValidations/dive1.golden deleted file mode 100644 index 5d2a240..0000000 --- a/testdata/TestMapWithValidations/dive1.golden +++ /dev/null @@ -1,6 +0,0 @@ -// @typecheck -export const Dive1Schema = z.object({ - 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 - diff --git a/testdata/TestMapWithValidations/dive2.golden b/testdata/TestMapWithValidations/dive2.golden deleted file mode 100644 index 9b95249..0000000 --- a/testdata/TestMapWithValidations/dive2.golden +++ /dev/null @@ -1,6 +0,0 @@ -// @typecheck -export const Dive2Schema = z.object({ - 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 - diff --git a/testdata/TestMapWithValidations/dive3.golden b/testdata/TestMapWithValidations/dive3.golden deleted file mode 100644 index ad0814b..0000000 --- a/testdata/TestMapWithValidations/dive3.golden +++ /dev/null @@ -1,6 +0,0 @@ -// @typecheck -export const Dive3Schema = z.object({ - 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 - diff --git a/testdata/TestMapWithValidations/dive_nested.golden b/testdata/TestMapWithValidations/dive_nested.golden new file mode 100644 index 0000000..f285098 --- /dev/null +++ b/testdata/TestMapWithValidations/dive_nested.golden @@ -0,0 +1,11 @@ +// @typecheck +export const dive2Schema = z.object({ + value: 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 + +export const dive3Schema = z.object({ + value: 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 + diff --git a/testdata/TestMapWithValidations/eq.golden b/testdata/TestMapWithValidations/eq.golden deleted file mode 100644 index f0d4847..0000000 --- a/testdata/TestMapWithValidations/eq.golden +++ /dev/null @@ -1,6 +0,0 @@ -// @typecheck -export const EqSchema = z.object({ - Map: z.record(z.string(), z.string()).refine((val) => Object.keys(val).length === 1, 'Map wrong size'), -}) -export type Eq = z.infer - diff --git a/testdata/TestMapWithValidations/gt.golden b/testdata/TestMapWithValidations/gt.golden deleted file mode 100644 index af73f29..0000000 --- a/testdata/TestMapWithValidations/gt.golden +++ /dev/null @@ -1,6 +0,0 @@ -// @typecheck -export const GtSchema = z.object({ - Map: z.record(z.string(), z.string()).refine((val) => Object.keys(val).length > 1, 'Map too small'), -}) -export type Gt = z.infer - diff --git a/testdata/TestMapWithValidations/gte.golden b/testdata/TestMapWithValidations/gte.golden deleted file mode 100644 index 5f6ac4d..0000000 --- a/testdata/TestMapWithValidations/gte.golden +++ /dev/null @@ -1,6 +0,0 @@ -// @typecheck -export const GteSchema = z.object({ - Map: z.record(z.string(), z.string()).refine((val) => Object.keys(val).length >= 1, 'Map too small'), -}) -export type Gte = z.infer - diff --git a/testdata/TestMapWithValidations/len.golden b/testdata/TestMapWithValidations/len.golden deleted file mode 100644 index 06d6ba0..0000000 --- a/testdata/TestMapWithValidations/len.golden +++ /dev/null @@ -1,6 +0,0 @@ -// @typecheck -export const LenSchema = z.object({ - Map: z.record(z.string(), z.string()).refine((val) => Object.keys(val).length === 1, 'Map wrong size'), -}) -export type Len = z.infer - diff --git a/testdata/TestMapWithValidations/lt.golden b/testdata/TestMapWithValidations/lt.golden deleted file mode 100644 index 7479753..0000000 --- a/testdata/TestMapWithValidations/lt.golden +++ /dev/null @@ -1,6 +0,0 @@ -// @typecheck -export const LtSchema = z.object({ - Map: z.record(z.string(), z.string()).refine((val) => Object.keys(val).length < 1, 'Map too large'), -}) -export type Lt = z.infer - diff --git a/testdata/TestMapWithValidations/lte.golden b/testdata/TestMapWithValidations/lte.golden deleted file mode 100644 index c91dba6..0000000 --- a/testdata/TestMapWithValidations/lte.golden +++ /dev/null @@ -1,6 +0,0 @@ -// @typecheck -export const LteSchema = z.object({ - Map: z.record(z.string(), z.string()).refine((val) => Object.keys(val).length <= 1, 'Map too large'), -}) -export type Lte = z.infer - diff --git a/testdata/TestMapWithValidations/max.golden b/testdata/TestMapWithValidations/max.golden deleted file mode 100644 index 864e47f..0000000 --- a/testdata/TestMapWithValidations/max.golden +++ /dev/null @@ -1,6 +0,0 @@ -// @typecheck -export const MaxSchema = z.object({ - Map: z.record(z.string(), z.string()).refine((val) => Object.keys(val).length <= 1, 'Map too large'), -}) -export type Max = z.infer - diff --git a/testdata/TestMapWithValidations/min.golden b/testdata/TestMapWithValidations/min.golden deleted file mode 100644 index 251062b..0000000 --- a/testdata/TestMapWithValidations/min.golden +++ /dev/null @@ -1,6 +0,0 @@ -// @typecheck -export const MinSchema = z.object({ - Map: z.record(z.string(), z.string()).refine((val) => Object.keys(val).length >= 1, 'Map too small'), -}) -export type Min = z.infer - diff --git a/testdata/TestMapWithValidations/minmax.golden b/testdata/TestMapWithValidations/minmax.golden deleted file mode 100644 index 97e9b27..0000000 --- a/testdata/TestMapWithValidations/minmax.golden +++ /dev/null @@ -1,6 +0,0 @@ -// @typecheck -export const MinMaxSchema = z.object({ - Map: z.record(z.string(), z.string()).refine((val) => Object.keys(val).length >= 1, 'Map too small').refine((val) => Object.keys(val).length <= 2, 'Map too large'), -}) -export type MinMax = z.infer - diff --git a/testdata/TestMapWithValidations/ne.golden b/testdata/TestMapWithValidations/ne.golden deleted file mode 100644 index ceceebe..0000000 --- a/testdata/TestMapWithValidations/ne.golden +++ /dev/null @@ -1,6 +0,0 @@ -// @typecheck -export const NeSchema = z.object({ - Map: z.record(z.string(), z.string()).refine((val) => Object.keys(val).length !== 1, 'Map wrong size'), -}) -export type Ne = z.infer - diff --git a/testdata/TestMapWithValidations/required.golden b/testdata/TestMapWithValidations/required.golden deleted file mode 100644 index 708a0c0..0000000 --- a/testdata/TestMapWithValidations/required.golden +++ /dev/null @@ -1,6 +0,0 @@ -// @typecheck -export const RequiredSchema = z.object({ - Map: z.record(z.string(), z.string()), -}) -export type Required = z.infer - diff --git a/testdata/TestNumberValidations.golden b/testdata/TestNumberValidations.golden new file mode 100644 index 0000000..53d7d3a --- /dev/null +++ b/testdata/TestNumberValidations.golden @@ -0,0 +1,36 @@ +// @typecheck +export const gte_lteSchema = z.object({ + value: z.number().gte(18).lte(60), +}) +export type gte_lte = z.infer + +export const gt_ltSchema = z.object({ + value: z.number().gt(18).lt(60), +}) +export type gt_lt = z.infer + +export const eqSchema = z.object({ + value: z.number().refine((val) => val === 18), +}) +export type eq = z.infer + +export const neSchema = z.object({ + value: z.number().refine((val) => val !== 18), +}) +export type ne = z.infer + +export const oneofSchema = z.object({ + value: z.number().refine((val) => [18, 19, 20].includes(val)), +}) +export type oneof = z.infer + +export const min_maxSchema = z.object({ + value: z.number().gte(18).lte(60), +}) +export type min_max = z.infer + +export const lenSchema = z.object({ + value: z.number().refine((val) => val === 18), +}) +export type len = z.infer + diff --git a/testdata/TestNumberValidations/eq.golden b/testdata/TestNumberValidations/eq.golden deleted file mode 100644 index c9a72c2..0000000 --- a/testdata/TestNumberValidations/eq.golden +++ /dev/null @@ -1,6 +0,0 @@ -// @typecheck -export const User3Schema = z.object({ - Age: z.number().refine((val) => val === 18), -}) -export type User3 = z.infer - diff --git a/testdata/TestNumberValidations/gt_lt.golden b/testdata/TestNumberValidations/gt_lt.golden deleted file mode 100644 index 44b5e1c..0000000 --- a/testdata/TestNumberValidations/gt_lt.golden +++ /dev/null @@ -1,6 +0,0 @@ -// @typecheck -export const User2Schema = z.object({ - Age: z.number().gt(18).lt(60), -}) -export type User2 = z.infer - diff --git a/testdata/TestNumberValidations/gte_lte.golden b/testdata/TestNumberValidations/gte_lte.golden deleted file mode 100644 index c4d7bb0..0000000 --- a/testdata/TestNumberValidations/gte_lte.golden +++ /dev/null @@ -1,6 +0,0 @@ -// @typecheck -export const User1Schema = z.object({ - Age: z.number().gte(18).lte(60), -}) -export type User1 = z.infer - diff --git a/testdata/TestNumberValidations/len.golden b/testdata/TestNumberValidations/len.golden deleted file mode 100644 index 29ef608..0000000 --- a/testdata/TestNumberValidations/len.golden +++ /dev/null @@ -1,6 +0,0 @@ -// @typecheck -export const User7Schema = z.object({ - Age: z.number().refine((val) => val === 18), -}) -export type User7 = z.infer - diff --git a/testdata/TestNumberValidations/min_max.golden b/testdata/TestNumberValidations/min_max.golden deleted file mode 100644 index 6056270..0000000 --- a/testdata/TestNumberValidations/min_max.golden +++ /dev/null @@ -1,6 +0,0 @@ -// @typecheck -export const User6Schema = z.object({ - Age: z.number().gte(18).lte(60), -}) -export type User6 = z.infer - diff --git a/testdata/TestNumberValidations/ne.golden b/testdata/TestNumberValidations/ne.golden deleted file mode 100644 index 33dfc93..0000000 --- a/testdata/TestNumberValidations/ne.golden +++ /dev/null @@ -1,6 +0,0 @@ -// @typecheck -export const User4Schema = z.object({ - Age: z.number().refine((val) => val !== 18), -}) -export type User4 = z.infer - diff --git a/testdata/TestNumberValidations/oneof.golden b/testdata/TestNumberValidations/oneof.golden deleted file mode 100644 index 1e2e0f3..0000000 --- a/testdata/TestNumberValidations/oneof.golden +++ /dev/null @@ -1,6 +0,0 @@ -// @typecheck -export const User5Schema = z.object({ - Age: z.number().refine((val) => [18, 19, 20].includes(val)), -}) -export type User5 = z.infer - diff --git a/testdata/TestStringValidations.golden b/testdata/TestStringValidations.golden new file mode 100644 index 0000000000000000000000000000000000000000..5ade99585ddcca4f87a197092be8e7dc0250d9f9 GIT binary patch literal 5206 zcmb`LQE$^a6vuhqr|_*VYI3(`8yk0wyRIN49uR_u30>*7VR93<4WxE)95(1GzWO~$ z>a_K7nuhgdihSJv`T6hT6KBWAk z0_DsP2>DLql)Z$pM9GMc9oLg=6mjM_5OGQOI-5bd-7aZOSR9L%dvvgHwRV;aq;t`T zkC?p5M#QL0cYUCz78sm82IG%y7@T1YozBD%93X2*7*D58f4S--(z+8e4qHB=6hZ^=XJKn7Te<`plN%oSM>OUw2wvrIJjg=MiYM7+LK*tKH@=~ zhAeR^FB@7BL+FTo{}~UHyGTuTCCTRh3-hZ4Rm@H# z*|#*3wbzgGB@cbiImNGMErS7c#GXEt@h#(l2zMutY|QSRq;vVY z^Okl-{`}d({p-(T%(s>YBd!JQlr)}AXc5YPUGqvS`1-W-^L6Jx|Io&hD`JwhMCNhI zLvStqO$_j83km2}z?2!Qcv2o^+t(OtFq^+$z@HC`j4Q=krWd?SP|deNV8z^+(O?I2 z#%*}p**4N5uq240#;!f-{PL{@PBS@+N2@F*z)%Rdz1H26i!Y(Jk|}G$ooPhlgtdLV zTT3-eu#v`Oh{YX~0Zkf|kNpD`zB4=}-e+=eK+bV7VVMgvmZ4%lOiDNdLna&VRppT@ z^f1PH9t#7LQP3EobLjW72I(I{zZ`hQquaCfBuJm7uyU<(n_%f|w$Cm6l*v_XVVMgv zmLdBVP6Z!}aM;*Bc&Q9Rc&(f-Y>q8eG0jFg)3Gm!;I~k)QAm|&P*$6jKPO85AanBW z=Hmws{)YfX51Q^jL^7314$+j61qrAMCNOO|nx9`7Uf}bwiG|m+8MoUUQxz$=#5MLg zD^sU`+Iivk2B9G-#}jyRZSL!27*$6qAfXl>L3#Q@Q$0A&DIBN^e-OG4 r8U1BO@4A}bPOY - diff --git a/testdata/TestStringValidations/alphanum.golden b/testdata/TestStringValidations/alphanum.golden deleted file mode 100644 index 889e7f8..0000000 --- a/testdata/TestStringValidations/alphanum.golden +++ /dev/null @@ -1,6 +0,0 @@ -// @typecheck -export const AlphaNumSchema = z.object({ - Name: z.string().regex(/^[a-zA-Z0-9]+$/), -}) -export type AlphaNum = z.infer - diff --git a/testdata/TestStringValidations/alphanumunicode.golden b/testdata/TestStringValidations/alphanumunicode.golden deleted file mode 100644 index 72ecf5e..0000000 --- a/testdata/TestStringValidations/alphanumunicode.golden +++ /dev/null @@ -1,6 +0,0 @@ -// @typecheck -export const AlphaNumUnicodeSchema = z.object({ - Name: z.string().regex(/^[\p{L}\p{N}]+$/u), -}) -export type AlphaNumUnicode = z.infer - diff --git a/testdata/TestStringValidations/alphaunicode.golden b/testdata/TestStringValidations/alphaunicode.golden deleted file mode 100644 index 01f9f0f..0000000 --- a/testdata/TestStringValidations/alphaunicode.golden +++ /dev/null @@ -1,6 +0,0 @@ -// @typecheck -export const AlphaUnicodeSchema = z.object({ - Name: z.string().regex(/^[\p{L}]+$/u), -}) -export type AlphaUnicode = z.infer - diff --git a/testdata/TestStringValidations/ascii.golden b/testdata/TestStringValidations/ascii.golden deleted file mode 100644 index 34df04e04508e42f4736892546810631ecf05f4d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 142 zcmdPbS8yn)EJ#hxNKMY>O06i!FDg+;&d)0@QE&`)_Vf$}O5`Rg*eX=%XhnNO7Co?ZC Qwa5k}n4gAhi5(Xg0Pq?ua{vGU diff --git a/testdata/TestStringValidations/base64/v3.golden b/testdata/TestStringValidations/base64/v3.golden deleted file mode 100644 index 9826696..0000000 --- a/testdata/TestStringValidations/base64/v3.golden +++ /dev/null @@ -1,7 +0,0 @@ -// @zod-version: v3 -// @typecheck -export const Base64Schema = z.object({ - Name: z.string().regex(/^(?:[A-Za-z0-9+\/]{4})*(?:[A-Za-z0-9+\/]{2}==|[A-Za-z0-9+\/]{3}=|[A-Za-z0-9+\/]{4})$/), -}) -export type Base64 = z.infer - diff --git a/testdata/TestStringValidations/base64/v4.golden b/testdata/TestStringValidations/base64/v4.golden deleted file mode 100644 index b05e43f..0000000 --- a/testdata/TestStringValidations/base64/v4.golden +++ /dev/null @@ -1,7 +0,0 @@ -// @zod-version: v4 -// @typecheck -export const Base64Schema = z.object({ - Name: z.base64(), -}) -export type Base64 = z.infer - diff --git a/testdata/TestStringValidations/boolean.golden b/testdata/TestStringValidations/boolean.golden deleted file mode 100644 index 346e8bc..0000000 --- a/testdata/TestStringValidations/boolean.golden +++ /dev/null @@ -1,6 +0,0 @@ -// @typecheck -export const BooleanSchema = z.object({ - Name: z.enum(['true', 'false']), -}) -export type Boolean = z.infer - diff --git a/testdata/TestStringValidations/contains.golden b/testdata/TestStringValidations/contains.golden deleted file mode 100644 index 1e927ea..0000000 --- a/testdata/TestStringValidations/contains.golden +++ /dev/null @@ -1,6 +0,0 @@ -// @typecheck -export const ContainsSchema = z.object({ - Name: z.string().includes("hello"), -}) -export type Contains = z.infer - diff --git a/testdata/TestStringValidations/datetime/v3.golden b/testdata/TestStringValidations/datetime/v3.golden deleted file mode 100644 index fe9b9b8..0000000 --- a/testdata/TestStringValidations/datetime/v3.golden +++ /dev/null @@ -1,7 +0,0 @@ -// @zod-version: v3 -// @typecheck -export const datetimeSchema = z.object({ - Name: z.string().datetime(), -}) -export type datetime = z.infer - diff --git a/testdata/TestStringValidations/datetime/v4.golden b/testdata/TestStringValidations/datetime/v4.golden deleted file mode 100644 index 2513427..0000000 --- a/testdata/TestStringValidations/datetime/v4.golden +++ /dev/null @@ -1,7 +0,0 @@ -// @zod-version: v4 -// @typecheck -export const datetimeSchema = z.object({ - Name: z.iso.datetime(), -}) -export type datetime = z.infer - diff --git a/testdata/TestStringValidations/email/v3.golden b/testdata/TestStringValidations/email/v3.golden deleted file mode 100644 index 6ad5c15..0000000 --- a/testdata/TestStringValidations/email/v3.golden +++ /dev/null @@ -1,7 +0,0 @@ -// @zod-version: v3 -// @typecheck -export const EmailSchema = z.object({ - Name: z.string().email(), -}) -export type Email = z.infer - diff --git a/testdata/TestStringValidations/email/v4.golden b/testdata/TestStringValidations/email/v4.golden deleted file mode 100644 index 332bb10..0000000 --- a/testdata/TestStringValidations/email/v4.golden +++ /dev/null @@ -1,7 +0,0 @@ -// @zod-version: v4 -// @typecheck -export const EmailSchema = z.object({ - Name: z.email(), -}) -export type Email = z.infer - diff --git a/testdata/TestStringValidations/endswith.golden b/testdata/TestStringValidations/endswith.golden deleted file mode 100644 index 6858e71..0000000 --- a/testdata/TestStringValidations/endswith.golden +++ /dev/null @@ -1,6 +0,0 @@ -// @typecheck -export const EndsWithSchema = z.object({ - Name: z.string().endsWith("hello"), -}) -export type EndsWith = z.infer - diff --git a/testdata/TestStringValidations/eq.golden b/testdata/TestStringValidations/eq.golden deleted file mode 100644 index 77a779e..0000000 --- a/testdata/TestStringValidations/eq.golden +++ /dev/null @@ -1,6 +0,0 @@ -// @typecheck -export const EqSchema = z.object({ - Name: z.string().refine((val) => val === "hello"), -}) -export type Eq = z.infer - diff --git a/testdata/TestStringValidations/gt.golden b/testdata/TestStringValidations/gt.golden deleted file mode 100644 index da8c9c4..0000000 --- a/testdata/TestStringValidations/gt.golden +++ /dev/null @@ -1,6 +0,0 @@ -// @typecheck -export const GtSchema = z.object({ - Name: z.string().refine((val) => [...val].length > 5, 'String must contain at least 6 character(s)'), -}) -export type Gt = z.infer - diff --git a/testdata/TestStringValidations/gte.golden b/testdata/TestStringValidations/gte.golden deleted file mode 100644 index 3f3d021..0000000 --- a/testdata/TestStringValidations/gte.golden +++ /dev/null @@ -1,6 +0,0 @@ -// @typecheck -export const GteSchema = z.object({ - Name: z.string().refine((val) => [...val].length >= 5, 'String must contain at least 5 character(s)'), -}) -export type Gte = z.infer - diff --git a/testdata/TestStringValidations/hexadecimal/v3.golden b/testdata/TestStringValidations/hexadecimal/v3.golden deleted file mode 100644 index 9683e70..0000000 --- a/testdata/TestStringValidations/hexadecimal/v3.golden +++ /dev/null @@ -1,7 +0,0 @@ -// @zod-version: v3 -// @typecheck -export const HexadecimalSchema = z.object({ - Name: z.string().regex(/^(0[xX])?[0-9a-fA-F]+$/), -}) -export type Hexadecimal = z.infer - diff --git a/testdata/TestStringValidations/hexadecimal/v4.golden b/testdata/TestStringValidations/hexadecimal/v4.golden deleted file mode 100644 index 90d5622..0000000 --- a/testdata/TestStringValidations/hexadecimal/v4.golden +++ /dev/null @@ -1,7 +0,0 @@ -// @zod-version: v4 -// @typecheck -export const HexadecimalSchema = z.object({ - Name: z.hex(), -}) -export type Hexadecimal = z.infer - diff --git a/testdata/TestStringValidations/http_url/v3.golden b/testdata/TestStringValidations/http_url/v3.golden deleted file mode 100644 index ce2a6ef..0000000 --- a/testdata/TestStringValidations/http_url/v3.golden +++ /dev/null @@ -1,7 +0,0 @@ -// @zod-version: v3 -// @typecheck -export const HttpURLSchema = z.object({ - Name: z.string().url(), -}) -export type HttpURL = z.infer - diff --git a/testdata/TestStringValidations/http_url/v4.golden b/testdata/TestStringValidations/http_url/v4.golden deleted file mode 100644 index 71d1db2..0000000 --- a/testdata/TestStringValidations/http_url/v4.golden +++ /dev/null @@ -1,7 +0,0 @@ -// @zod-version: v4 -// @typecheck -export const HttpURLSchema = z.object({ - Name: z.httpUrl(), -}) -export type HttpURL = z.infer - diff --git a/testdata/TestStringValidations/ip/v3.golden b/testdata/TestStringValidations/ip/v3.golden deleted file mode 100644 index 2cb4fc8..0000000 --- a/testdata/TestStringValidations/ip/v3.golden +++ /dev/null @@ -1,7 +0,0 @@ -// @zod-version: v3 -// @typecheck -export const IPSchema = z.object({ - Name: z.string().ip(), -}) -export type IP = z.infer - diff --git a/testdata/TestStringValidations/ip/v4.golden b/testdata/TestStringValidations/ip/v4.golden deleted file mode 100644 index c4fb347..0000000 --- a/testdata/TestStringValidations/ip/v4.golden +++ /dev/null @@ -1,7 +0,0 @@ -// @zod-version: v4 -// @typecheck -export const IPSchema = z.object({ - Name: z.union([z.ipv4(), z.ipv6()]), -}) -export type IP = z.infer - diff --git a/testdata/TestStringValidations/ip4_addr/v3.golden b/testdata/TestStringValidations/ip4_addr/v3.golden deleted file mode 100644 index 518f229..0000000 --- a/testdata/TestStringValidations/ip4_addr/v3.golden +++ /dev/null @@ -1,7 +0,0 @@ -// @zod-version: v3 -// @typecheck -export const IP4AddrSchema = z.object({ - Name: z.string().ip({ version: "v4" }), -}) -export type IP4Addr = z.infer - diff --git a/testdata/TestStringValidations/ip4_addr/v4.golden b/testdata/TestStringValidations/ip4_addr/v4.golden deleted file mode 100644 index e5362a0..0000000 --- a/testdata/TestStringValidations/ip4_addr/v4.golden +++ /dev/null @@ -1,7 +0,0 @@ -// @zod-version: v4 -// @typecheck -export const IP4AddrSchema = z.object({ - Name: z.ipv4(), -}) -export type IP4Addr = z.infer - diff --git a/testdata/TestStringValidations/ip6_addr/v3.golden b/testdata/TestStringValidations/ip6_addr/v3.golden deleted file mode 100644 index 0305357..0000000 --- a/testdata/TestStringValidations/ip6_addr/v3.golden +++ /dev/null @@ -1,7 +0,0 @@ -// @zod-version: v3 -// @typecheck -export const IP6AddrSchema = z.object({ - Name: z.string().ip({ version: "v6" }), -}) -export type IP6Addr = z.infer - diff --git a/testdata/TestStringValidations/ip6_addr/v4.golden b/testdata/TestStringValidations/ip6_addr/v4.golden deleted file mode 100644 index c00967d..0000000 --- a/testdata/TestStringValidations/ip6_addr/v4.golden +++ /dev/null @@ -1,7 +0,0 @@ -// @zod-version: v4 -// @typecheck -export const IP6AddrSchema = z.object({ - Name: z.ipv6(), -}) -export type IP6Addr = z.infer - diff --git a/testdata/TestStringValidations/ip_addr/v3.golden b/testdata/TestStringValidations/ip_addr/v3.golden deleted file mode 100644 index 73e779f..0000000 --- a/testdata/TestStringValidations/ip_addr/v3.golden +++ /dev/null @@ -1,7 +0,0 @@ -// @zod-version: v3 -// @typecheck -export const IPAddrSchema = z.object({ - Name: z.string().ip(), -}) -export type IPAddr = z.infer - diff --git a/testdata/TestStringValidations/ip_addr/v4.golden b/testdata/TestStringValidations/ip_addr/v4.golden deleted file mode 100644 index aeb8f2c..0000000 --- a/testdata/TestStringValidations/ip_addr/v4.golden +++ /dev/null @@ -1,7 +0,0 @@ -// @zod-version: v4 -// @typecheck -export const IPAddrSchema = z.object({ - Name: z.union([z.ipv4(), z.ipv6()]), -}) -export type IPAddr = z.infer - diff --git a/testdata/TestStringValidations/ipv4/v3.golden b/testdata/TestStringValidations/ipv4/v3.golden deleted file mode 100644 index 90aa88d..0000000 --- a/testdata/TestStringValidations/ipv4/v3.golden +++ /dev/null @@ -1,7 +0,0 @@ -// @zod-version: v3 -// @typecheck -export const IPv4Schema = z.object({ - Name: z.string().ip({ version: "v4" }), -}) -export type IPv4 = z.infer - diff --git a/testdata/TestStringValidations/ipv4/v4.golden b/testdata/TestStringValidations/ipv4/v4.golden deleted file mode 100644 index aa37310..0000000 --- a/testdata/TestStringValidations/ipv4/v4.golden +++ /dev/null @@ -1,7 +0,0 @@ -// @zod-version: v4 -// @typecheck -export const IPv4Schema = z.object({ - Name: z.ipv4(), -}) -export type IPv4 = z.infer - diff --git a/testdata/TestStringValidations/ipv6/v3.golden b/testdata/TestStringValidations/ipv6/v3.golden deleted file mode 100644 index 518c581..0000000 --- a/testdata/TestStringValidations/ipv6/v3.golden +++ /dev/null @@ -1,7 +0,0 @@ -// @zod-version: v3 -// @typecheck -export const IPv6Schema = z.object({ - Name: z.string().ip({ version: "v6" }), -}) -export type IPv6 = z.infer - diff --git a/testdata/TestStringValidations/ipv6/v4.golden b/testdata/TestStringValidations/ipv6/v4.golden deleted file mode 100644 index bbfd382..0000000 --- a/testdata/TestStringValidations/ipv6/v4.golden +++ /dev/null @@ -1,7 +0,0 @@ -// @zod-version: v4 -// @typecheck -export const IPv6Schema = z.object({ - Name: z.ipv6(), -}) -export type IPv6 = z.infer - diff --git a/testdata/TestStringValidations/json.golden b/testdata/TestStringValidations/json.golden deleted file mode 100644 index ef2c945..0000000 --- a/testdata/TestStringValidations/json.golden +++ /dev/null @@ -1,6 +0,0 @@ -// @typecheck -export const jsonSchema = z.object({ - Name: z.string().refine((val) => { try { JSON.parse(val); return true } catch { return false } }), -}) -export type json = z.infer - diff --git a/testdata/TestStringValidations/latitude.golden b/testdata/TestStringValidations/latitude.golden deleted file mode 100644 index 18b4df2..0000000 --- a/testdata/TestStringValidations/latitude.golden +++ /dev/null @@ -1,6 +0,0 @@ -// @typecheck -export const LatitudeSchema = z.object({ - Name: z.string().regex(/^[-+]?([1-8]?\d(\.\d+)?|90(\.0+)?)$/), -}) -export type Latitude = z.infer - diff --git a/testdata/TestStringValidations/len.golden b/testdata/TestStringValidations/len.golden deleted file mode 100644 index 5a65da6..0000000 --- a/testdata/TestStringValidations/len.golden +++ /dev/null @@ -1,6 +0,0 @@ -// @typecheck -export const LenSchema = z.object({ - Name: z.string().refine((val) => [...val].length === 5, 'String must contain 5 character(s)'), -}) -export type Len = z.infer - diff --git a/testdata/TestStringValidations/longitude.golden b/testdata/TestStringValidations/longitude.golden deleted file mode 100644 index f77d5fd..0000000 --- a/testdata/TestStringValidations/longitude.golden +++ /dev/null @@ -1,6 +0,0 @@ -// @typecheck -export const LongitudeSchema = z.object({ - Name: z.string().regex(/^[-+]?(180(\.0+)?|((1[0-7]\d)|([1-9]?\d))(\.\d+)?)$/), -}) -export type Longitude = z.infer - diff --git a/testdata/TestStringValidations/lowercase.golden b/testdata/TestStringValidations/lowercase.golden deleted file mode 100644 index 750d188..0000000 --- a/testdata/TestStringValidations/lowercase.golden +++ /dev/null @@ -1,6 +0,0 @@ -// @typecheck -export const LowercaseSchema = z.object({ - Name: z.string().refine((val) => val === val.toLowerCase()), -}) -export type Lowercase = z.infer - diff --git a/testdata/TestStringValidations/lt.golden b/testdata/TestStringValidations/lt.golden deleted file mode 100644 index 6f4f134..0000000 --- a/testdata/TestStringValidations/lt.golden +++ /dev/null @@ -1,6 +0,0 @@ -// @typecheck -export const LtSchema = z.object({ - Name: z.string().refine((val) => [...val].length < 5, 'String must contain at most 4 character(s)'), -}) -export type Lt = z.infer - diff --git a/testdata/TestStringValidations/lte.golden b/testdata/TestStringValidations/lte.golden deleted file mode 100644 index 454e751..0000000 --- a/testdata/TestStringValidations/lte.golden +++ /dev/null @@ -1,6 +0,0 @@ -// @typecheck -export const LteSchema = z.object({ - Name: z.string().refine((val) => [...val].length <= 5, 'String must contain at most 5 character(s)'), -}) -export type Lte = z.infer - diff --git a/testdata/TestStringValidations/max.golden b/testdata/TestStringValidations/max.golden deleted file mode 100644 index c49ef0c..0000000 --- a/testdata/TestStringValidations/max.golden +++ /dev/null @@ -1,6 +0,0 @@ -// @typecheck -export const MaxSchema = z.object({ - Name: z.string().refine((val) => [...val].length <= 5, 'String must contain at most 5 character(s)'), -}) -export type Max = z.infer - diff --git a/testdata/TestStringValidations/md4.golden b/testdata/TestStringValidations/md4.golden deleted file mode 100644 index 01f2236..0000000 --- a/testdata/TestStringValidations/md4.golden +++ /dev/null @@ -1,6 +0,0 @@ -// @typecheck -export const MD4Schema = z.object({ - Name: z.string().regex(/^[0-9a-f]{32}$/), -}) -export type MD4 = z.infer - diff --git a/testdata/TestStringValidations/md5/v3.golden b/testdata/TestStringValidations/md5/v3.golden deleted file mode 100644 index e8efa20..0000000 --- a/testdata/TestStringValidations/md5/v3.golden +++ /dev/null @@ -1,7 +0,0 @@ -// @zod-version: v3 -// @typecheck -export const MD5Schema = z.object({ - Name: z.string().regex(/^[0-9a-f]{32}$/), -}) -export type MD5 = z.infer - diff --git a/testdata/TestStringValidations/md5/v4.golden b/testdata/TestStringValidations/md5/v4.golden deleted file mode 100644 index 1ec4bd9..0000000 --- a/testdata/TestStringValidations/md5/v4.golden +++ /dev/null @@ -1,7 +0,0 @@ -// @zod-version: v4 -// @typecheck -export const MD5Schema = z.object({ - Name: z.hash("md5"), -}) -export type MD5 = z.infer - diff --git a/testdata/TestStringValidations/min.golden b/testdata/TestStringValidations/min.golden deleted file mode 100644 index 656da20..0000000 --- a/testdata/TestStringValidations/min.golden +++ /dev/null @@ -1,6 +0,0 @@ -// @typecheck -export const MinSchema = z.object({ - Name: z.string().refine((val) => [...val].length >= 5, 'String must contain at least 5 character(s)'), -}) -export type Min = z.infer - diff --git a/testdata/TestStringValidations/minmax.golden b/testdata/TestStringValidations/minmax.golden deleted file mode 100644 index 67daa11..0000000 --- a/testdata/TestStringValidations/minmax.golden +++ /dev/null @@ -1,6 +0,0 @@ -// @typecheck -export const MinMaxSchema = z.object({ - 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 - diff --git a/testdata/TestStringValidations/mongodb.golden b/testdata/TestStringValidations/mongodb.golden deleted file mode 100644 index 983efff..0000000 --- a/testdata/TestStringValidations/mongodb.golden +++ /dev/null @@ -1,6 +0,0 @@ -// @typecheck -export const mongodbSchema = z.object({ - Name: z.string().regex(/^[a-f\d]{24}$/), -}) -export type mongodb = z.infer - diff --git a/testdata/TestStringValidations/ne.golden b/testdata/TestStringValidations/ne.golden deleted file mode 100644 index adf5719..0000000 --- a/testdata/TestStringValidations/ne.golden +++ /dev/null @@ -1,6 +0,0 @@ -// @typecheck -export const NeSchema = z.object({ - Name: z.string().refine((val) => val !== "hello"), -}) -export type Ne = z.infer - diff --git a/testdata/TestStringValidations/number.golden b/testdata/TestStringValidations/number.golden deleted file mode 100644 index a03709d..0000000 --- a/testdata/TestStringValidations/number.golden +++ /dev/null @@ -1,6 +0,0 @@ -// @typecheck -export const NumberSchema = z.object({ - Name: z.string().regex(/^[0-9]+$/), -}) -export type Number = z.infer - diff --git a/testdata/TestStringValidations/numeric.golden b/testdata/TestStringValidations/numeric.golden deleted file mode 100644 index 67b4d02..0000000 --- a/testdata/TestStringValidations/numeric.golden +++ /dev/null @@ -1,6 +0,0 @@ -// @typecheck -export const NumericSchema = z.object({ - Name: z.string().regex(/^[-+]?[0-9]+(?:\.[0-9]+)?$/), -}) -export type Numeric = z.infer - diff --git a/testdata/TestStringValidations/oneof.golden b/testdata/TestStringValidations/oneof.golden deleted file mode 100644 index 29dc59f..0000000 --- a/testdata/TestStringValidations/oneof.golden +++ /dev/null @@ -1,6 +0,0 @@ -// @typecheck -export const OneOfSchema = z.object({ - Name: z.enum(["hello", "world"] as const), -}) -export type OneOf = z.infer - diff --git a/testdata/TestStringValidations/oneof_separated.golden b/testdata/TestStringValidations/oneof_separated.golden deleted file mode 100644 index fd9d6ec..0000000 --- a/testdata/TestStringValidations/oneof_separated.golden +++ /dev/null @@ -1,6 +0,0 @@ -// @typecheck -export const OneOfSeparatedSchema = z.object({ - Name: z.enum(["a b c", "d e f"] as const), -}) -export type OneOfSeparated = z.infer - diff --git a/testdata/TestStringValidations/required.golden b/testdata/TestStringValidations/required.golden deleted file mode 100644 index 5c1e0a8..0000000 --- a/testdata/TestStringValidations/required.golden +++ /dev/null @@ -1,6 +0,0 @@ -// @typecheck -export const RequiredSchema = z.object({ - Name: z.string().min(1), -}) -export type Required = z.infer - diff --git a/testdata/TestStringValidations/sha256/v3.golden b/testdata/TestStringValidations/sha256/v3.golden deleted file mode 100644 index e588501..0000000 --- a/testdata/TestStringValidations/sha256/v3.golden +++ /dev/null @@ -1,7 +0,0 @@ -// @zod-version: v3 -// @typecheck -export const SHA256Schema = z.object({ - Name: z.string().regex(/^[0-9a-f]{64}$/), -}) -export type SHA256 = z.infer - diff --git a/testdata/TestStringValidations/sha256/v4.golden b/testdata/TestStringValidations/sha256/v4.golden deleted file mode 100644 index 672c660..0000000 --- a/testdata/TestStringValidations/sha256/v4.golden +++ /dev/null @@ -1,7 +0,0 @@ -// @zod-version: v4 -// @typecheck -export const SHA256Schema = z.object({ - Name: z.hash("sha256"), -}) -export type SHA256 = z.infer - diff --git a/testdata/TestStringValidations/sha384/v3.golden b/testdata/TestStringValidations/sha384/v3.golden deleted file mode 100644 index 333832b..0000000 --- a/testdata/TestStringValidations/sha384/v3.golden +++ /dev/null @@ -1,7 +0,0 @@ -// @zod-version: v3 -// @typecheck -export const SHA384Schema = z.object({ - Name: z.string().regex(/^[0-9a-f]{96}$/), -}) -export type SHA384 = z.infer - diff --git a/testdata/TestStringValidations/sha384/v4.golden b/testdata/TestStringValidations/sha384/v4.golden deleted file mode 100644 index c14e4c7..0000000 --- a/testdata/TestStringValidations/sha384/v4.golden +++ /dev/null @@ -1,7 +0,0 @@ -// @zod-version: v4 -// @typecheck -export const SHA384Schema = z.object({ - Name: z.hash("sha384"), -}) -export type SHA384 = z.infer - diff --git a/testdata/TestStringValidations/sha512/v3.golden b/testdata/TestStringValidations/sha512/v3.golden deleted file mode 100644 index b9c410c..0000000 --- a/testdata/TestStringValidations/sha512/v3.golden +++ /dev/null @@ -1,7 +0,0 @@ -// @zod-version: v3 -// @typecheck -export const SHA512Schema = z.object({ - Name: z.string().regex(/^[0-9a-f]{128}$/), -}) -export type SHA512 = z.infer - diff --git a/testdata/TestStringValidations/sha512/v4.golden b/testdata/TestStringValidations/sha512/v4.golden deleted file mode 100644 index a62e7d3..0000000 --- a/testdata/TestStringValidations/sha512/v4.golden +++ /dev/null @@ -1,7 +0,0 @@ -// @zod-version: v4 -// @typecheck -export const SHA512Schema = z.object({ - Name: z.hash("sha512"), -}) -export type SHA512 = z.infer - diff --git a/testdata/TestStringValidations/startswith.golden b/testdata/TestStringValidations/startswith.golden deleted file mode 100644 index 52f555f..0000000 --- a/testdata/TestStringValidations/startswith.golden +++ /dev/null @@ -1,6 +0,0 @@ -// @typecheck -export const StartsWithSchema = z.object({ - Name: z.string().startsWith("hello"), -}) -export type StartsWith = z.infer - diff --git a/testdata/TestStringValidations/uppercase.golden b/testdata/TestStringValidations/uppercase.golden deleted file mode 100644 index 31a86a6..0000000 --- a/testdata/TestStringValidations/uppercase.golden +++ /dev/null @@ -1,6 +0,0 @@ -// @typecheck -export const UppercaseSchema = z.object({ - Name: z.string().refine((val) => val === val.toUpperCase()), -}) -export type Uppercase = z.infer - diff --git a/testdata/TestStringValidations/url/v3.golden b/testdata/TestStringValidations/url/v3.golden deleted file mode 100644 index bd27184..0000000 --- a/testdata/TestStringValidations/url/v3.golden +++ /dev/null @@ -1,7 +0,0 @@ -// @zod-version: v3 -// @typecheck -export const URLSchema = z.object({ - Name: z.string().url(), -}) -export type URL = z.infer - diff --git a/testdata/TestStringValidations/url/v4.golden b/testdata/TestStringValidations/url/v4.golden deleted file mode 100644 index d2c3e4c..0000000 --- a/testdata/TestStringValidations/url/v4.golden +++ /dev/null @@ -1,7 +0,0 @@ -// @zod-version: v4 -// @typecheck -export const URLSchema = z.object({ - Name: z.url(), -}) -export type URL = z.infer - diff --git a/testdata/TestStringValidations/url_encoded.golden b/testdata/TestStringValidations/url_encoded.golden deleted file mode 100644 index 93dc6ea..0000000 --- a/testdata/TestStringValidations/url_encoded.golden +++ /dev/null @@ -1,6 +0,0 @@ -// @typecheck -export const URLEncodedSchema = z.object({ - Name: z.string().regex(/^(?:[^%]|%[0-9A-Fa-f]{2})*$/), -}) -export type URLEncoded = z.infer - diff --git a/testdata/TestStringValidations/uuid/v3.golden b/testdata/TestStringValidations/uuid/v3.golden deleted file mode 100644 index 641eff6..0000000 --- a/testdata/TestStringValidations/uuid/v3.golden +++ /dev/null @@ -1,7 +0,0 @@ -// @zod-version: v3 -// @typecheck -export const UUIDSchema = z.object({ - Name: z.string().regex(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/), -}) -export type UUID = z.infer - diff --git a/testdata/TestStringValidations/uuid/v4.golden b/testdata/TestStringValidations/uuid/v4.golden deleted file mode 100644 index 0a92948..0000000 --- a/testdata/TestStringValidations/uuid/v4.golden +++ /dev/null @@ -1,7 +0,0 @@ -// @zod-version: v4 -// @typecheck -export const UUIDSchema = z.object({ - Name: z.uuid(), -}) -export type UUID = z.infer - diff --git a/testdata/TestStringValidations/uuid3/v3.golden b/testdata/TestStringValidations/uuid3/v3.golden deleted file mode 100644 index ec05fbd..0000000 --- a/testdata/TestStringValidations/uuid3/v3.golden +++ /dev/null @@ -1,7 +0,0 @@ -// @zod-version: v3 -// @typecheck -export const UUID3Schema = z.object({ - Name: z.string().regex(/^[0-9a-f]{8}-[0-9a-f]{4}-3[0-9a-f]{3}-[0-9a-f]{4}-[0-9a-f]{12}$/), -}) -export type UUID3 = z.infer - diff --git a/testdata/TestStringValidations/uuid3/v4.golden b/testdata/TestStringValidations/uuid3/v4.golden deleted file mode 100644 index 67d4af5..0000000 --- a/testdata/TestStringValidations/uuid3/v4.golden +++ /dev/null @@ -1,7 +0,0 @@ -// @zod-version: v4 -// @typecheck -export const UUID3Schema = z.object({ - Name: z.uuid({ version: "v3" }), -}) -export type UUID3 = z.infer - diff --git a/testdata/TestStringValidations/uuid3_rfc4122/v3.golden b/testdata/TestStringValidations/uuid3_rfc4122/v3.golden deleted file mode 100644 index 9f955dc..0000000 --- a/testdata/TestStringValidations/uuid3_rfc4122/v3.golden +++ /dev/null @@ -1,7 +0,0 @@ -// @zod-version: v3 -// @typecheck -export const UUID3RFC4122Schema = z.object({ - Name: z.string().regex(/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-3[0-9a-fA-F]{3}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/), -}) -export type UUID3RFC4122 = z.infer - diff --git a/testdata/TestStringValidations/uuid3_rfc4122/v4.golden b/testdata/TestStringValidations/uuid3_rfc4122/v4.golden deleted file mode 100644 index 5e2a32d..0000000 --- a/testdata/TestStringValidations/uuid3_rfc4122/v4.golden +++ /dev/null @@ -1,7 +0,0 @@ -// @zod-version: v4 -// @typecheck -export const UUID3RFC4122Schema = z.object({ - Name: z.uuid({ version: "v3" }), -}) -export type UUID3RFC4122 = z.infer - diff --git a/testdata/TestStringValidations/uuid4/v3.golden b/testdata/TestStringValidations/uuid4/v3.golden deleted file mode 100644 index 8d2959d..0000000 --- a/testdata/TestStringValidations/uuid4/v3.golden +++ /dev/null @@ -1,7 +0,0 @@ -// @zod-version: v3 -// @typecheck -export const UUID4Schema = z.object({ - Name: z.string().regex(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/), -}) -export type UUID4 = z.infer - diff --git a/testdata/TestStringValidations/uuid4/v4.golden b/testdata/TestStringValidations/uuid4/v4.golden deleted file mode 100644 index 5375789..0000000 --- a/testdata/TestStringValidations/uuid4/v4.golden +++ /dev/null @@ -1,7 +0,0 @@ -// @zod-version: v4 -// @typecheck -export const UUID4Schema = z.object({ - Name: z.uuid({ version: "v4" }), -}) -export type UUID4 = z.infer - diff --git a/testdata/TestStringValidations/uuid4_rfc4122/v3.golden b/testdata/TestStringValidations/uuid4_rfc4122/v3.golden deleted file mode 100644 index 557d7a9..0000000 --- a/testdata/TestStringValidations/uuid4_rfc4122/v3.golden +++ /dev/null @@ -1,7 +0,0 @@ -// @zod-version: v3 -// @typecheck -export const UUID4RFC4122Schema = z.object({ - Name: z.string().regex(/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$/), -}) -export type UUID4RFC4122 = z.infer - diff --git a/testdata/TestStringValidations/uuid4_rfc4122/v4.golden b/testdata/TestStringValidations/uuid4_rfc4122/v4.golden deleted file mode 100644 index 856aaf1..0000000 --- a/testdata/TestStringValidations/uuid4_rfc4122/v4.golden +++ /dev/null @@ -1,7 +0,0 @@ -// @zod-version: v4 -// @typecheck -export const UUID4RFC4122Schema = z.object({ - Name: z.uuid({ version: "v4" }), -}) -export type UUID4RFC4122 = z.infer - diff --git a/testdata/TestStringValidations/uuid5/v3.golden b/testdata/TestStringValidations/uuid5/v3.golden deleted file mode 100644 index 78f090c..0000000 --- a/testdata/TestStringValidations/uuid5/v3.golden +++ /dev/null @@ -1,7 +0,0 @@ -// @zod-version: v3 -// @typecheck -export const UUID5Schema = z.object({ - Name: z.string().regex(/^[0-9a-f]{8}-[0-9a-f]{4}-5[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/), -}) -export type UUID5 = z.infer - diff --git a/testdata/TestStringValidations/uuid5/v4.golden b/testdata/TestStringValidations/uuid5/v4.golden deleted file mode 100644 index c637452..0000000 --- a/testdata/TestStringValidations/uuid5/v4.golden +++ /dev/null @@ -1,7 +0,0 @@ -// @zod-version: v4 -// @typecheck -export const UUID5Schema = z.object({ - Name: z.uuid({ version: "v5" }), -}) -export type UUID5 = z.infer - diff --git a/testdata/TestStringValidations/uuid5_rfc4122/v3.golden b/testdata/TestStringValidations/uuid5_rfc4122/v3.golden deleted file mode 100644 index d58d435..0000000 --- a/testdata/TestStringValidations/uuid5_rfc4122/v3.golden +++ /dev/null @@ -1,7 +0,0 @@ -// @zod-version: v3 -// @typecheck -export const UUID5RFC4122Schema = z.object({ - Name: z.string().regex(/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-5[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$/), -}) -export type UUID5RFC4122 = z.infer - diff --git a/testdata/TestStringValidations/uuid5_rfc4122/v4.golden b/testdata/TestStringValidations/uuid5_rfc4122/v4.golden deleted file mode 100644 index f258e67..0000000 --- a/testdata/TestStringValidations/uuid5_rfc4122/v4.golden +++ /dev/null @@ -1,7 +0,0 @@ -// @zod-version: v4 -// @typecheck -export const UUID5RFC4122Schema = z.object({ - Name: z.uuid({ version: "v5" }), -}) -export type UUID5RFC4122 = z.infer - diff --git a/testdata/TestStringValidations/uuid_rfc4122/v3.golden b/testdata/TestStringValidations/uuid_rfc4122/v3.golden deleted file mode 100644 index 2dfc34f..0000000 --- a/testdata/TestStringValidations/uuid_rfc4122/v3.golden +++ /dev/null @@ -1,7 +0,0 @@ -// @zod-version: v3 -// @typecheck -export const UUIDRFC4122Schema = z.object({ - Name: z.string().regex(/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/), -}) -export type UUIDRFC4122 = z.infer - diff --git a/testdata/TestStringValidations/uuid_rfc4122/v4.golden b/testdata/TestStringValidations/uuid_rfc4122/v4.golden deleted file mode 100644 index 3805ebb..0000000 --- a/testdata/TestStringValidations/uuid_rfc4122/v4.golden +++ /dev/null @@ -1,7 +0,0 @@ -// @zod-version: v4 -// @typecheck -export const UUIDRFC4122Schema = z.object({ - Name: z.uuid(), -}) -export type UUIDRFC4122 = z.infer - diff --git a/testdata/TestZodV4Defaults/ip_unions_inherit_generic_string_constraints.golden b/testdata/TestZodV4Defaults/ip_unions_inherit_generic_string_constraints.golden index 302d5e5..90fda32 100644 --- a/testdata/TestZodV4Defaults/ip_unions_inherit_generic_string_constraints.golden +++ b/testdata/TestZodV4Defaults/ip_unions_inherit_generic_string_constraints.golden @@ -1,7 +1,7 @@ // @zod-version: v4 // @typecheck export const PayloadSchema = z.object({ - Address: z.union([z.ipv4().min(1).refine((val) => [...val].length <= 45, 'String must contain at most 45 character(s)'), z.ipv6().min(1).refine((val) => [...val].length <= 45, 'String must contain at most 45 character(s)')]), + Address: z.union([z.ipv4().refine((val) => [...val].length <= 45, 'String must contain at most 45 character(s)'), z.ipv6().refine((val) => [...val].length <= 45, 'String must contain at most 45 character(s)')]), }) export type Payload = z.infer diff --git a/testdata/TestZodV4Defaults/ip_unions_work_when_chain_constraints_precede_ip_tag/v4.golden b/testdata/TestZodV4Defaults/ip_unions_work_when_chain_constraints_precede_ip_tag/v4.golden deleted file mode 100644 index 7dc027b..0000000 --- a/testdata/TestZodV4Defaults/ip_unions_work_when_chain_constraints_precede_ip_tag/v4.golden +++ /dev/null @@ -1,7 +0,0 @@ -// @zod-version: v4 -// @typecheck -export const PayloadSchema = z.object({ - Address: z.union([z.ipv4().min(1), z.ipv6().min(1)]), -}) -export type Payload = z.infer - diff --git a/testdata/TestZodV4Defaults/ip_unions_work_when_chain_constraints_precede_ip_tag/v3.golden b/testdata/TestZodV4Defaults/optional_format_with_nullable_pointer/v3.golden similarity index 76% rename from testdata/TestZodV4Defaults/ip_unions_work_when_chain_constraints_precede_ip_tag/v3.golden rename to testdata/TestZodV4Defaults/optional_format_with_nullable_pointer/v3.golden index ccc58b7..64f34fb 100644 --- a/testdata/TestZodV4Defaults/ip_unions_work_when_chain_constraints_precede_ip_tag/v3.golden +++ b/testdata/TestZodV4Defaults/optional_format_with_nullable_pointer/v3.golden @@ -1,7 +1,7 @@ // @zod-version: v3 // @typecheck export const PayloadSchema = z.object({ - Address: z.string().min(1).ip(), + email: z.string().email().nullable(), }) export type Payload = z.infer diff --git a/testdata/TestZodV4Defaults/ip_mixed_with_another_format_falls_back_to_legacy_chain_semantics.golden b/testdata/TestZodV4Defaults/optional_format_with_nullable_pointer/v4.golden similarity index 80% rename from testdata/TestZodV4Defaults/ip_mixed_with_another_format_falls_back_to_legacy_chain_semantics.golden rename to testdata/TestZodV4Defaults/optional_format_with_nullable_pointer/v4.golden index 3125525..c5154ba 100644 --- a/testdata/TestZodV4Defaults/ip_mixed_with_another_format_falls_back_to_legacy_chain_semantics.golden +++ b/testdata/TestZodV4Defaults/optional_format_with_nullable_pointer/v4.golden @@ -1,7 +1,7 @@ // @zod-version: v4 // @typecheck export const PayloadSchema = z.object({ - Address: z.string().email(), + email: z.email().nullable(), }) export type Payload = z.infer diff --git a/zod.go b/zod.go index 5d236a0..29d6c4e 100644 --- a/zod.go +++ b/zod.go @@ -81,21 +81,29 @@ func NewConverter(customTypes map[string]CustomFn) Converter { return *NewConverterWithOpts(WithCustomTypes(customTypes)) } +// AddTypeWithName converts a struct type to corresponding zod schema using a custom name +// instead of the struct's type name. Useful for anonymous structs from reflect.StructOf. +func (c *Converter) AddTypeWithName(input interface{}, name string) { + c.addType(reflect.TypeOf(input), name) +} + // AddType converts a struct type to corresponding zod schema. AddType can be called // multiple times, followed by Export to get the corresponding zod schemas. func (c *Converter) AddType(input interface{}) { t := reflect.TypeOf(input) + c.addType(t, typeName(t)) +} +func (c *Converter) addType(t reflect.Type, name string) { if t.Kind() != reflect.Struct { panic("input must be a struct") } - name := typeName(t) if _, ok := c.outputs[name]; ok { return } - data, selfRef := c.convertStructTopLevel(t) + data, selfRef := c.convertStructTopLevel(t, name) c.addSchema(name, data, selfRef) } @@ -166,18 +174,9 @@ type meta struct { selfRef bool } -type stringSchemaParts struct { - base string - chain string - enumLike bool - isIPUnion bool -} - -type stringSchemaChunk struct { - kind string - text string - v4Base string - legacyChain string +type stringValidator struct { + tag string // "email", "ip", "required", "trim", "max", "_custom", etc. + arg string // "45" for max=45, raw text for _custom } type Converter struct { @@ -263,10 +262,9 @@ func typeName(t reflect.Type) string { return "UNKNOWN" } -func (c *Converter) convertStructTopLevel(t reflect.Type) (string, bool) { +func (c *Converter) convertStructTopLevel(t reflect.Type, name string) (string, bool) { output := strings.Builder{} - name := typeName(t) c.stack = append(c.stack, meta{name, false}) data := c.convertStruct(t, 0) @@ -522,7 +520,7 @@ func (c *Converter) ConvertType(t reflect.Type, validate string, indent int) str } else { // throws panic if there is a cycle detectCycle(name, c.stack) - data, selfRef := c.convertStructTopLevel(t) + data, selfRef := c.convertStructTopLevel(t, name) c.addSchema(name, data, selfRef) validateStr.WriteString(schemaName(c.prefix, name)) } @@ -562,17 +560,7 @@ func (c *Converter) ConvertType(t reflect.Type, validate string, indent int) str if validate != "" { switch zodType { case "string": - stringParts := c.validateString(validate) - switch { - case stringParts.enumLike: - return stringParts.base + stringParts.chain - case stringParts.isIPUnion: - return stringParts.base - case stringParts.base != "": - return stringParts.base + stringParts.chain - default: - return "z.string()" + stringParts.chain - } + return c.validateString(validate) case "number": validateStr = c.validateNumber(validate) } @@ -822,17 +810,7 @@ func (c *Converter) convertKeyType(t reflect.Type, validate string) string { if validate != "" { switch zodType { case "string": - stringParts := c.validateString(validate) - switch { - case stringParts.enumLike: - return stringParts.base + stringParts.chain - case stringParts.isIPUnion: - return stringParts.base - case stringParts.base != "": - return stringParts.base + stringParts.chain - default: - return "z.string()" + stringParts.chain - } + return c.validateString(validate) case "number": validateStr = c.validateNumber(validate) } @@ -1043,9 +1021,35 @@ func (c *Converter) validateNumber(validate string) string { return validateStr.String() } -func (c *Converter) validateString(validate string) stringSchemaParts { - var chunks []stringSchemaChunk - var refines []string +// Tag classification sets for string validators. +var formatTags = map[string]bool{ + "email": true, "url": true, "http_url": true, + "ipv4": true, "ip4_addr": true, "ipv6": true, "ip6_addr": true, + "base64": true, "datetime": true, "hexadecimal": true, "jwt": true, + "uuid": true, "uuid3": true, "uuid3_rfc4122": true, + "uuid4": true, "uuid4_rfc4122": true, + "uuid5": true, "uuid5_rfc4122": true, + "uuid_rfc4122": true, + "md5": true, "sha256": true, "sha384": true, "sha512": true, +} + +var unionTags = map[string]bool{ + "ip": true, "ip_addr": true, +} + +// Tags where generated Zod schemas accepts an empty string +// unless `.min(1)` is added. +var v4AcceptsEmpty = map[string]bool{ + "base64": true, "hexadecimal": true, +} + +func (c *Converter) validateString(validate string) string { + validators := c.parseStringValidators(validate) + return c.renderStringSchema(validators) +} + +func (c *Converter) parseStringValidators(validate string) []stringValidator { + var validators []stringValidator parts := strings.Split(validate, ",") for _, rawPart := range parts { @@ -1056,11 +1060,7 @@ func (c *Converter) validateString(validate string) stringSchemaParts { if h, ok := c.customTags[valName]; ok { v := h(c, reflect.TypeOf(0), valValue, 0) - if strings.HasPrefix(v, ".refine") { - refines = append(refines, v) - } else { - chunks = append(chunks, stringSchemaChunk{kind: "chain", text: v}) - } + validators = append(validators, stringValidator{tag: "_custom", arg: v}) continue } @@ -1074,42 +1074,32 @@ func (c *Converter) validateString(validate string) stringSchemaParts { if len(vals) == 0 { panic("oneof= must be followed by a list of values") } - chunks = append(chunks, stringSchemaChunk{ - kind: "enum", - text: fmt.Sprintf("z.enum([\"%s\"] as const)", strings.Join(vals, "\", \"")), - }) + enumText := fmt.Sprintf("z.enum([\"%s\"] as const)", strings.Join(vals, "\", \"")) + validators = append(validators, stringValidator{tag: "oneof", arg: enumText}) + case "contains": + validators = append(validators, stringValidator{tag: "contains", arg: valValue}) + case "endswith": + validators = append(validators, stringValidator{tag: "endswith", arg: valValue}) + case "startswith": + validators = append(validators, stringValidator{tag: "startswith", arg: valValue}) + case "eq": + validators = append(validators, stringValidator{tag: "eq", arg: valValue}) + case "ne": + validators = append(validators, stringValidator{tag: "ne", arg: valValue}) case "len": - refines = append(refines, fmt.Sprintf(".refine((val) => [...val].length === %s, 'String must contain %s character(s)')", valValue, valValue)) + validators = append(validators, stringValidator{tag: "len", arg: valValue}) case "min": - refines = append(refines, fmt.Sprintf(".refine((val) => [...val].length >= %s, 'String must contain at least %s character(s)')", valValue, valValue)) + validators = append(validators, stringValidator{tag: "min", arg: valValue}) case "max": - refines = append(refines, fmt.Sprintf(".refine((val) => [...val].length <= %s, 'String must contain at most %s character(s)')", valValue, valValue)) + validators = append(validators, stringValidator{tag: "max", arg: valValue}) case "gt": - val, err := strconv.Atoi(valValue) - if err != nil { - panic("gt= must be followed by a number") - } - refines = append(refines, fmt.Sprintf(".refine((val) => [...val].length > %d, 'String must contain at least %d character(s)')", val, val+1)) + validators = append(validators, stringValidator{tag: "gt", arg: valValue}) case "gte": - refines = append(refines, fmt.Sprintf(".refine((val) => [...val].length >= %s, 'String must contain at least %s character(s)')", valValue, valValue)) + validators = append(validators, stringValidator{tag: "gte", arg: valValue}) case "lt": - val, err := strconv.Atoi(valValue) - if err != nil { - panic("lt= must be followed by a number") - } - refines = append(refines, fmt.Sprintf(".refine((val) => [...val].length < %d, 'String must contain at most %d character(s)')", val, val-1)) + validators = append(validators, stringValidator{tag: "lt", arg: valValue}) case "lte": - refines = append(refines, fmt.Sprintf(".refine((val) => [...val].length <= %s, 'String must contain at most %s character(s)')", valValue, valValue)) - case "contains": - chunks = append(chunks, stringSchemaChunk{kind: "chain", text: fmt.Sprintf(".includes(\"%s\")", valValue)}) - case "endswith": - chunks = append(chunks, stringSchemaChunk{kind: "chain", text: fmt.Sprintf(".endsWith(\"%s\")", valValue)}) - case "startswith": - chunks = append(chunks, stringSchemaChunk{kind: "chain", text: fmt.Sprintf(".startsWith(\"%s\")", valValue)}) - case "eq": - refines = append(refines, fmt.Sprintf(".refine((val) => val === \"%s\")", valValue)) - case "ne": - refines = append(refines, fmt.Sprintf(".refine((val) => val !== \"%s\")", valValue)) + validators = append(validators, stringValidator{tag: "lte", arg: valValue}) default: panic(fmt.Sprintf("unknown validation: %s", rawPart)) } @@ -1118,196 +1108,454 @@ func (c *Converter) validateString(validate string) stringSchemaParts { switch valName { case "omitempty": + // skip case "required": - chunks = append(chunks, stringSchemaChunk{kind: "chain", text: ".min(1)"}) + validators = append(validators, stringValidator{tag: "required"}) case "email": - chunks = append(chunks, stringSchemaChunk{kind: "format", v4Base: "z.email()", legacyChain: ".email()"}) + validators = append(validators, stringValidator{tag: "email"}) case "url": - chunks = append(chunks, stringSchemaChunk{kind: "format", v4Base: "z.url()", legacyChain: ".url()"}) + validators = append(validators, stringValidator{tag: "url"}) case "ipv4", "ip4_addr": - chunks = append(chunks, stringSchemaChunk{kind: "format", v4Base: "z.ipv4()", legacyChain: `.ip({ version: "v4" })`}) + validators = append(validators, stringValidator{tag: valName}) case "ipv6", "ip6_addr": - chunks = append(chunks, stringSchemaChunk{kind: "format", v4Base: "z.ipv6()", legacyChain: `.ip({ version: "v6" })`}) + validators = append(validators, stringValidator{tag: valName}) case "ip", "ip_addr": - chunks = append(chunks, stringSchemaChunk{kind: "ip", legacyChain: ".ip()"}) + validators = append(validators, stringValidator{tag: valName}) case "http_url": - chunks = append(chunks, stringSchemaChunk{kind: "format", v4Base: "z.httpUrl()", legacyChain: ".url()"}) + validators = append(validators, stringValidator{tag: "http_url"}) case "url_encoded": - chunks = append(chunks, stringSchemaChunk{kind: "chain", text: fmt.Sprintf(".regex(/%s/)", uRLEncodedRegexString)}) + validators = append(validators, stringValidator{tag: "url_encoded"}) case "alpha": - chunks = append(chunks, stringSchemaChunk{kind: "chain", text: fmt.Sprintf(".regex(/%s/)", alphaRegexString)}) + validators = append(validators, stringValidator{tag: "alpha"}) case "alphanum": - chunks = append(chunks, stringSchemaChunk{kind: "chain", text: fmt.Sprintf(".regex(/%s/)", alphaNumericRegexString)}) + validators = append(validators, stringValidator{tag: "alphanum"}) case "alphanumunicode": - chunks = append(chunks, stringSchemaChunk{kind: "chain", text: fmt.Sprintf(".regex(/%s/u)", alphaUnicodeNumericRegexString)}) + validators = append(validators, stringValidator{tag: "alphanumunicode"}) case "alphaunicode": - chunks = append(chunks, stringSchemaChunk{kind: "chain", text: fmt.Sprintf(".regex(/%s/u)", alphaUnicodeRegexString)}) + validators = append(validators, stringValidator{tag: "alphaunicode"}) case "ascii": - chunks = append(chunks, stringSchemaChunk{kind: "chain", text: fmt.Sprintf(".regex(/%s/)", aSCIIRegexString)}) + validators = append(validators, stringValidator{tag: "ascii"}) case "boolean": - chunks = append(chunks, stringSchemaChunk{kind: "enum", text: "z.enum(['true', 'false'])"}) + validators = append(validators, stringValidator{tag: "boolean", arg: "z.enum(['true', 'false'])"}) case "lowercase": - refines = append(refines, ".refine((val) => val === val.toLowerCase())") + validators = append(validators, stringValidator{tag: "lowercase"}) case "number": - chunks = append(chunks, stringSchemaChunk{kind: "chain", text: fmt.Sprintf(".regex(/%s/)", numberRegexString)}) + validators = append(validators, stringValidator{tag: "number"}) case "numeric": - chunks = append(chunks, stringSchemaChunk{kind: "chain", text: fmt.Sprintf(".regex(/%s/)", numericRegexString)}) + validators = append(validators, stringValidator{tag: "numeric"}) case "uppercase": - refines = append(refines, ".refine((val) => val === val.toUpperCase())") + validators = append(validators, stringValidator{tag: "uppercase"}) case "base64": - chunks = append(chunks, stringSchemaChunk{kind: "format", v4Base: "z.base64()", legacyChain: fmt.Sprintf(".regex(/%s/)", base64RegexString)}) + validators = append(validators, stringValidator{tag: "base64"}) case "mongodb": - chunks = append(chunks, stringSchemaChunk{kind: "chain", text: fmt.Sprintf(".regex(/%s/)", mongodbRegexString)}) + validators = append(validators, stringValidator{tag: "mongodb"}) case "datetime": - chunks = append(chunks, stringSchemaChunk{kind: "format", v4Base: "z.iso.datetime()", legacyChain: ".datetime()"}) + validators = append(validators, stringValidator{tag: "datetime"}) case "hexadecimal": - chunks = append(chunks, stringSchemaChunk{kind: "format", v4Base: "z.hex()", legacyChain: fmt.Sprintf(".regex(/%s/)", hexadecimalRegexString)}) + validators = append(validators, stringValidator{tag: "hexadecimal"}) case "json": - refines = append(refines, ".refine((val) => { try { JSON.parse(val); return true } catch { return false } })") + validators = append(validators, stringValidator{tag: "json"}) case "jwt": - chunks = append(chunks, stringSchemaChunk{kind: "format", v4Base: "z.jwt()", legacyChain: fmt.Sprintf(".regex(/%s/)", jWTRegexString)}) + validators = append(validators, stringValidator{tag: "jwt"}) case "latitude": - chunks = append(chunks, stringSchemaChunk{kind: "chain", text: fmt.Sprintf(".regex(/%s/)", latitudeRegexString)}) + validators = append(validators, stringValidator{tag: "latitude"}) case "longitude": - chunks = append(chunks, stringSchemaChunk{kind: "chain", text: fmt.Sprintf(".regex(/%s/)", longitudeRegexString)}) + validators = append(validators, stringValidator{tag: "longitude"}) case "uuid": - chunks = append(chunks, stringSchemaChunk{kind: "format", v4Base: "z.uuid()", legacyChain: fmt.Sprintf(".regex(/%s/)", uUIDRegexString)}) + validators = append(validators, stringValidator{tag: "uuid"}) case "uuid3": - chunks = append(chunks, stringSchemaChunk{kind: "format", v4Base: `z.uuid({ version: "v3" })`, legacyChain: fmt.Sprintf(".regex(/%s/)", uUID3RegexString)}) + validators = append(validators, stringValidator{tag: "uuid3"}) case "uuid3_rfc4122": - chunks = append(chunks, stringSchemaChunk{kind: "format", v4Base: `z.uuid({ version: "v3" })`, legacyChain: fmt.Sprintf(".regex(/%s/)", uUID3RFC4122RegexString)}) + validators = append(validators, stringValidator{tag: "uuid3_rfc4122"}) case "uuid4": - chunks = append(chunks, stringSchemaChunk{kind: "format", v4Base: `z.uuid({ version: "v4" })`, legacyChain: fmt.Sprintf(".regex(/%s/)", uUID4RegexString)}) + validators = append(validators, stringValidator{tag: "uuid4"}) case "uuid4_rfc4122": - chunks = append(chunks, stringSchemaChunk{kind: "format", v4Base: `z.uuid({ version: "v4" })`, legacyChain: fmt.Sprintf(".regex(/%s/)", uUID4RFC4122RegexString)}) + validators = append(validators, stringValidator{tag: "uuid4_rfc4122"}) case "uuid5": - chunks = append(chunks, stringSchemaChunk{kind: "format", v4Base: `z.uuid({ version: "v5" })`, legacyChain: fmt.Sprintf(".regex(/%s/)", uUID5RegexString)}) + validators = append(validators, stringValidator{tag: "uuid5"}) case "uuid5_rfc4122": - chunks = append(chunks, stringSchemaChunk{kind: "format", v4Base: `z.uuid({ version: "v5" })`, legacyChain: fmt.Sprintf(".regex(/%s/)", uUID5RFC4122RegexString)}) + validators = append(validators, stringValidator{tag: "uuid5_rfc4122"}) case "uuid_rfc4122": - chunks = append(chunks, stringSchemaChunk{kind: "format", v4Base: "z.uuid()", legacyChain: fmt.Sprintf(".regex(/%s/)", uUIDRFC4122RegexString)}) + validators = append(validators, stringValidator{tag: "uuid_rfc4122"}) case "md4": - chunks = append(chunks, stringSchemaChunk{kind: "chain", text: fmt.Sprintf(".regex(/%s/)", md4RegexString)}) + validators = append(validators, stringValidator{tag: "md4"}) case "md5": - chunks = append(chunks, stringSchemaChunk{kind: "format", v4Base: `z.hash("md5")`, legacyChain: fmt.Sprintf(".regex(/%s/)", md5RegexString)}) + validators = append(validators, stringValidator{tag: "md5"}) case "sha256": - chunks = append(chunks, stringSchemaChunk{kind: "format", v4Base: `z.hash("sha256")`, legacyChain: fmt.Sprintf(".regex(/%s/)", sha256RegexString)}) + validators = append(validators, stringValidator{tag: "sha256"}) case "sha384": - chunks = append(chunks, stringSchemaChunk{kind: "format", v4Base: `z.hash("sha384")`, legacyChain: fmt.Sprintf(".regex(/%s/)", sha384RegexString)}) + validators = append(validators, stringValidator{tag: "sha384"}) case "sha512": - chunks = append(chunks, stringSchemaChunk{kind: "format", v4Base: `z.hash("sha512")`, legacyChain: fmt.Sprintf(".regex(/%s/)", sha512RegexString)}) + validators = append(validators, stringValidator{tag: "sha512"}) default: panic(fmt.Sprintf("unknown validation: %s", rawPart)) } } - for _, refine := range refines { - chunks = append(chunks, stringSchemaChunk{kind: "chain", text: refine}) - } - - return c.lowerStringSchemaChunks(chunks) + return validators } -func (c *Converter) lowerStringSchemaChunks(chunks []stringSchemaChunk) stringSchemaParts { - schemaParts := stringSchemaParts{} - enumIdx := -1 - firstFormatIdx := -1 - firstIPIdx := -1 - hasNonIPFormat := false +func (c *Converter) renderStringSchema(validators []stringValidator) string { + // Phase 1: Classify validators + hasFormat := false + hasUnion := false + hasRequired := false + hasEnum := false + formatIdx := -1 + formatCount := 0 + + for i, v := range validators { + if formatTags[v.tag] { + hasFormat = true + formatCount++ + if formatIdx == -1 { + formatIdx = i + } + } + if unionTags[v.tag] { + hasUnion = true + } + if v.tag == "required" { + hasRequired = true + } + if v.tag == "oneof" || v.tag == "boolean" { + hasEnum = true + } + } - for i, chunk := range chunks { - switch chunk.kind { - case "enum": - if enumIdx == -1 { - enumIdx = i + // Phase 2: Validate combinations + if hasFormat && hasUnion { + panic("cannot combine format validator with union validator (e.g. email + ip)") + } + if formatCount > 1 { + panic("cannot combine multiple format validators (e.g. email + url)") + } + + // Phase 3: Handle enum — return early + if hasEnum { + base := "" + chain := "" + for _, v := range validators { + if v.tag == "oneof" || v.tag == "boolean" { + base = v.arg + break } - case "format": - if firstFormatIdx == -1 { - firstFormatIdx = i + } + for _, v := range validators { + if v.tag == "oneof" || v.tag == "boolean" { + continue } - hasNonIPFormat = true - case "ip": - if firstIPIdx == -1 { - firstIPIdx = i + rendered := c.renderV3Chain(v) + if strings.HasPrefix(rendered, ".refine") { + chain += rendered } } + return base + chain } - if enumIdx != -1 { - schemaParts.base = chunks[enumIdx].text - schemaParts.enumLike = true - for i := enumIdx + 1; i < len(chunks); i++ { - if chunks[i].kind == "chain" && strings.HasPrefix(chunks[i].text, ".refine") { - schemaParts.chain += chunks[i].text + // Phase 4: Render v3 + if c.zodV3 { + // Skip required when a format or union is present — format validators + // already reject empty strings in both v3 and v4. + skipRequired := hasFormat || hasUnion + chain := "" + for _, v := range validators { + if v.tag == "required" && skipRequired { + continue } + chain += c.renderV3Chain(v) } - return schemaParts + return "z.string()" + chain } - if c.zodV3 { - for _, chunk := range chunks { - schemaParts.chain += legacyStringSchemaChunk(chunk) + // Phase 5: Render v4 + + // Case 1: Union (ip/ip_addr) + if hasUnion { + armChain := "" + for _, v := range validators { + if v.tag == "required" || unionTags[v.tag] { + continue + } + armChain += c.renderV4Chain(v) } - return schemaParts + return fmt.Sprintf("z.union([z.ipv4()%s, z.ipv6()%s])", armChain, armChain) } - if firstIPIdx != -1 { - if hasNonIPFormat { - // In v4, .ip() doesn't exist as a chain method. Since combining - // ip with another format (e.g. email) is semantically nonsensical, - // drop the ip chunk and keep only the other format + chain pieces. - for _, chunk := range chunks { - if chunk.kind == "ip" { + // Case 2: Format present + if hasFormat { + // Check if anything (non-required, non-omitempty) precedes the format + hasTransformBefore := false + for i := 0; i < formatIdx; i++ { + v := validators[i] + if v.tag != "required" && v.tag != "omitempty" { + hasTransformBefore = true + break + } + } + + // Determine if required should be kept (base64/hex accept empty in v4) + keepRequired := hasRequired && v4AcceptsEmpty[validators[formatIdx].tag] + + if hasTransformBefore { + // Fall back to z.string() + chains (format becomes a chain method via v3 form) + chain := "" + for _, v := range validators { + if v.tag == "required" && !keepRequired { continue } - schemaParts.chain += legacyStringSchemaChunk(chunk) + if formatTags[v.tag] { + chain += c.renderV3Chain(v) + } else { + chain += c.renderV4Chain(v) + } } - return schemaParts + return "z.string()" + chain } - armChain := "" - for _, chunk := range chunks { - if chunk.kind == "chain" { - armChain += chunk.text + // Format as base + base := c.renderV4FormatBase(validators[formatIdx]) + chain := "" + for i := formatIdx + 1; i < len(validators); i++ { + v := validators[i] + if v.tag == "required" && !keepRequired { + continue } + chain += c.renderV4Chain(v) + } + if keepRequired { + chain = ".min(1)" + chain } - schemaParts.base = fmt.Sprintf("z.union([z.ipv4()%s, z.ipv6()%s])", armChain, armChain) - schemaParts.isIPUnion = true - return schemaParts + return base + chain } - if firstFormatIdx == -1 || hasChainBeforeStringSchemaChunk(chunks, firstFormatIdx) { - for _, chunk := range chunks { - schemaParts.chain += legacyStringSchemaChunk(chunk) - } - return schemaParts + // Case 3: No format/union — plain string + chain := "" + for _, v := range validators { + chain += c.renderV4Chain(v) } + return "z.string()" + chain +} - schemaParts.base = chunks[firstFormatIdx].v4Base - for i := firstFormatIdx + 1; i < len(chunks); i++ { - schemaParts.chain += legacyStringSchemaChunk(chunks[i]) +func (c *Converter) renderV3Chain(v stringValidator) string { + switch v.tag { + case "required": + return ".min(1)" + case "email": + return ".email()" + case "url": + return ".url()" + case "ip", "ip_addr": + return ".ip()" + case "ipv4", "ip4_addr": + return `.ip({ version: "v4" })` + case "ipv6", "ip6_addr": + return `.ip({ version: "v6" })` + case "http_url": + return ".url()" + case "base64": + return fmt.Sprintf(".regex(/%s/)", base64RegexString) + case "datetime": + return ".datetime()" + case "hexadecimal": + return fmt.Sprintf(".regex(/%s/)", hexadecimalRegexString) + case "jwt": + return fmt.Sprintf(".regex(/%s/)", jWTRegexString) + case "uuid": + return fmt.Sprintf(".regex(/%s/)", uUIDRegexString) + case "uuid3": + return fmt.Sprintf(".regex(/%s/)", uUID3RegexString) + case "uuid3_rfc4122": + return fmt.Sprintf(".regex(/%s/)", uUID3RFC4122RegexString) + case "uuid4": + return fmt.Sprintf(".regex(/%s/)", uUID4RegexString) + case "uuid4_rfc4122": + return fmt.Sprintf(".regex(/%s/)", uUID4RFC4122RegexString) + case "uuid5": + return fmt.Sprintf(".regex(/%s/)", uUID5RegexString) + case "uuid5_rfc4122": + return fmt.Sprintf(".regex(/%s/)", uUID5RFC4122RegexString) + case "uuid_rfc4122": + return fmt.Sprintf(".regex(/%s/)", uUIDRFC4122RegexString) + case "md4": + return fmt.Sprintf(".regex(/%s/)", md4RegexString) + case "md5": + return fmt.Sprintf(".regex(/%s/)", md5RegexString) + case "sha256": + return fmt.Sprintf(".regex(/%s/)", sha256RegexString) + case "sha384": + return fmt.Sprintf(".regex(/%s/)", sha384RegexString) + case "sha512": + return fmt.Sprintf(".regex(/%s/)", sha512RegexString) + case "contains": + return fmt.Sprintf(`.includes("%s")`, v.arg) + case "startswith": + return fmt.Sprintf(`.startsWith("%s")`, v.arg) + case "endswith": + return fmt.Sprintf(`.endsWith("%s")`, v.arg) + case "url_encoded": + return fmt.Sprintf(".regex(/%s/)", uRLEncodedRegexString) + case "alpha": + return fmt.Sprintf(".regex(/%s/)", alphaRegexString) + case "alphanum": + return fmt.Sprintf(".regex(/%s/)", alphaNumericRegexString) + case "alphanumunicode": + return fmt.Sprintf(".regex(/%s/u)", alphaUnicodeNumericRegexString) + case "alphaunicode": + return fmt.Sprintf(".regex(/%s/u)", alphaUnicodeRegexString) + case "ascii": + return fmt.Sprintf(".regex(/%s/)", aSCIIRegexString) + case "number": + return fmt.Sprintf(".regex(/%s/)", numberRegexString) + case "numeric": + return fmt.Sprintf(".regex(/%s/)", numericRegexString) + case "mongodb": + return fmt.Sprintf(".regex(/%s/)", mongodbRegexString) + case "latitude": + return fmt.Sprintf(".regex(/%s/)", latitudeRegexString) + case "longitude": + return fmt.Sprintf(".regex(/%s/)", longitudeRegexString) + case "eq": + return fmt.Sprintf(`.refine((val) => val === "%s")`, v.arg) + case "ne": + return fmt.Sprintf(`.refine((val) => val !== "%s")`, v.arg) + case "len": + return fmt.Sprintf(".refine((val) => [...val].length === %s, 'String must contain %s character(s)')", v.arg, v.arg) + case "min": + return fmt.Sprintf(".refine((val) => [...val].length >= %s, 'String must contain at least %s character(s)')", v.arg, v.arg) + case "max": + return fmt.Sprintf(".refine((val) => [...val].length <= %s, 'String must contain at most %s character(s)')", v.arg, v.arg) + case "gt": + val, _ := strconv.Atoi(v.arg) + return fmt.Sprintf(".refine((val) => [...val].length > %d, 'String must contain at least %d character(s)')", val, val+1) + case "gte": + return fmt.Sprintf(".refine((val) => [...val].length >= %s, 'String must contain at least %s character(s)')", v.arg, v.arg) + case "lt": + val, _ := strconv.Atoi(v.arg) + return fmt.Sprintf(".refine((val) => [...val].length < %d, 'String must contain at most %d character(s)')", val, val-1) + case "lte": + return fmt.Sprintf(".refine((val) => [...val].length <= %s, 'String must contain at most %s character(s)')", v.arg, v.arg) + case "lowercase": + return ".refine((val) => val === val.toLowerCase())" + case "uppercase": + return ".refine((val) => val === val.toUpperCase())" + case "json": + return ".refine((val) => { try { JSON.parse(val); return true } catch { return false } })" + case "_custom": + return v.arg + default: + return "" } - return schemaParts } -func legacyStringSchemaChunk(chunk stringSchemaChunk) string { - switch chunk.kind { - case "chain": - return chunk.text - case "format", "ip": - return chunk.legacyChain +func (c *Converter) renderV4FormatBase(v stringValidator) string { + switch v.tag { + case "email": + return "z.email()" + case "url": + return "z.url()" + case "http_url": + return "z.httpUrl()" + case "ipv4", "ip4_addr": + return "z.ipv4()" + case "ipv6", "ip6_addr": + return "z.ipv6()" + case "base64": + return "z.base64()" + case "datetime": + return "z.iso.datetime()" + case "hexadecimal": + return "z.hex()" + case "jwt": + return "z.jwt()" + case "uuid": + return "z.uuid()" + case "uuid3", "uuid3_rfc4122": + return `z.uuid({ version: "v3" })` + case "uuid4", "uuid4_rfc4122": + return `z.uuid({ version: "v4" })` + case "uuid5", "uuid5_rfc4122": + return `z.uuid({ version: "v5" })` + case "uuid_rfc4122": + return "z.uuid()" + case "md5": + return `z.hash("md5")` + case "sha256": + return `z.hash("sha256")` + case "sha384": + return `z.hash("sha384")` + case "sha512": + return `z.hash("sha512")` default: return "" } } -func hasChainBeforeStringSchemaChunk(chunks []stringSchemaChunk, idx int) bool { - for i := 0; i < idx; i++ { - if chunks[i].kind == "chain" { - return true - } +func (c *Converter) renderV4Chain(v stringValidator) string { + switch v.tag { + case "required": + return ".min(1)" + case "contains": + return fmt.Sprintf(`.includes("%s")`, v.arg) + case "startswith": + return fmt.Sprintf(`.startsWith("%s")`, v.arg) + case "endswith": + return fmt.Sprintf(`.endsWith("%s")`, v.arg) + case "url_encoded": + return fmt.Sprintf(".regex(/%s/)", uRLEncodedRegexString) + case "alpha": + return fmt.Sprintf(".regex(/%s/)", alphaRegexString) + case "alphanum": + return fmt.Sprintf(".regex(/%s/)", alphaNumericRegexString) + case "alphanumunicode": + return fmt.Sprintf(".regex(/%s/u)", alphaUnicodeNumericRegexString) + case "alphaunicode": + return fmt.Sprintf(".regex(/%s/u)", alphaUnicodeRegexString) + case "ascii": + return fmt.Sprintf(".regex(/%s/)", aSCIIRegexString) + case "number": + return fmt.Sprintf(".regex(/%s/)", numberRegexString) + case "numeric": + return fmt.Sprintf(".regex(/%s/)", numericRegexString) + case "mongodb": + return fmt.Sprintf(".regex(/%s/)", mongodbRegexString) + case "latitude": + return fmt.Sprintf(".regex(/%s/)", latitudeRegexString) + case "longitude": + return fmt.Sprintf(".regex(/%s/)", longitudeRegexString) + case "md4": + return fmt.Sprintf(".regex(/%s/)", md4RegexString) + case "eq": + return fmt.Sprintf(`.refine((val) => val === "%s")`, v.arg) + case "ne": + return fmt.Sprintf(`.refine((val) => val !== "%s")`, v.arg) + case "len": + return fmt.Sprintf(".refine((val) => [...val].length === %s, 'String must contain %s character(s)')", v.arg, v.arg) + case "min": + return fmt.Sprintf(".refine((val) => [...val].length >= %s, 'String must contain at least %s character(s)')", v.arg, v.arg) + case "max": + return fmt.Sprintf(".refine((val) => [...val].length <= %s, 'String must contain at most %s character(s)')", v.arg, v.arg) + case "gt": + val, _ := strconv.Atoi(v.arg) + return fmt.Sprintf(".refine((val) => [...val].length > %d, 'String must contain at least %d character(s)')", val, val+1) + case "gte": + return fmt.Sprintf(".refine((val) => [...val].length >= %s, 'String must contain at least %s character(s)')", v.arg, v.arg) + case "lt": + val, _ := strconv.Atoi(v.arg) + return fmt.Sprintf(".refine((val) => [...val].length < %d, 'String must contain at most %d character(s)')", val, val-1) + case "lte": + return fmt.Sprintf(".refine((val) => [...val].length <= %s, 'String must contain at most %s character(s)')", v.arg, v.arg) + case "lowercase": + return ".refine((val) => val === val.toLowerCase())" + case "uppercase": + return ".refine((val) => val === val.toUpperCase())" + case "json": + return ".refine((val) => { try { JSON.parse(val); return true } catch { return false } })" + case "_custom": + return v.arg + default: + return "" } - return false } func isPartialRecordKeySchema(schema string) bool { diff --git a/zod_test.go b/zod_test.go index b368423..e15d9c0 100644 --- a/zod_test.go +++ b/zod_test.go @@ -85,6 +85,47 @@ func assertSchema(t *testing.T, schema any, versions ...string) { } } +// buildValidatorConverter creates a converter with dynamically-built single-field structs. +// Each entry maps a name to a validate tag. The field type is determined by fieldType. +func buildValidatorConverter(fieldType reflect.Type, validators []struct{ name, tag string }, opts ...Opt) *Converter { + c := NewConverterWithOpts(opts...) + for _, v := range validators { + field := reflect.StructField{ + Name: "Value", + Type: fieldType, + Tag: reflect.StructTag(fmt.Sprintf(`validate:"%s" json:"value"`, v.tag)), + } + st := reflect.StructOf([]reflect.StructField{field}) + c.AddTypeWithName(reflect.New(st).Elem().Interface(), v.name) + } + return c +} + +// assertValidators golden-tests a list of validators. +// With no versions: asserts v3==v4, writes one golden file. +// With versions specified: writes separate golden files per version. +func assertValidators(t *testing.T, fieldType reflect.Type, validators []struct{ name, tag string }, versions ...string) { + t.Helper() + switch len(versions) { + case 0: + v3 := buildValidatorConverter(fieldType, validators, WithZodV3()) + v4 := buildValidatorConverter(fieldType, validators) + assert.Equal(t, v3.Export(), v4.Export()) + goldenAssert(t, []byte(v4.Export())) + default: + for _, ver := range versions { + t.Run(ver, func(t *testing.T) { + var opts []Opt + if ver == "v3" { + opts = append(opts, WithZodV3()) + } + c := buildValidatorConverter(fieldType, validators, opts...) + goldenAssert(t, []byte(c.Export()), withGoldenZodVersion(ver)) + }) + } + } +} + func TestFieldName(t *testing.T) { assert.Equal(t, fieldName(reflect.StructField{Name: "RCONPassword"}), @@ -322,414 +363,53 @@ func TestNullableWithValidations(t *testing.T) { } func TestStringValidations(t *testing.T) { - t.Run("eq", func(t *testing.T) { - type Eq struct { - Name string `validate:"eq=hello"` - } - assertSchema(t, Eq{}) - }) - - t.Run("ne", func(t *testing.T) { - type Ne struct { - Name string `validate:"ne=hello"` - } - assertSchema(t, Ne{}) - }) - - t.Run("oneof", func(t *testing.T) { - type OneOf struct { - Name string `validate:"oneof=hello world"` - } - assertSchema(t, OneOf{}) - }) - - t.Run("oneof_separated", func(t *testing.T) { - type OneOfSeparated struct { - Name string `validate:"oneof='a b c' 'd e f'"` - } - assertSchema(t, OneOfSeparated{}) - }) - - t.Run("len", func(t *testing.T) { - type Len struct { - Name string `validate:"len=5"` - } - assertSchema(t, Len{}) - }) - - t.Run("min", func(t *testing.T) { - type Min struct { - Name string `validate:"min=5"` - } - assertSchema(t, Min{}) - }) - - t.Run("max", func(t *testing.T) { - type Max struct { - Name string `validate:"max=5"` - } - assertSchema(t, Max{}) - }) - - t.Run("minmax", func(t *testing.T) { - type MinMax struct { - Name string `validate:"min=3,max=7"` - } - assertSchema(t, MinMax{}) - }) - - t.Run("gt", func(t *testing.T) { - type Gt struct { - Name string `validate:"gt=5"` - } - assertSchema(t, Gt{}) - }) - - t.Run("gte", func(t *testing.T) { - type Gte struct { - Name string `validate:"gte=5"` - } - assertSchema(t, Gte{}) - }) - - t.Run("lt", func(t *testing.T) { - type Lt struct { - Name string `validate:"lt=5"` - } - assertSchema(t, Lt{}) - }) - - t.Run("lte", func(t *testing.T) { - type Lte struct { - Name string `validate:"lte=5"` - } - assertSchema(t, Lte{}) - }) - - t.Run("contains", func(t *testing.T) { - type Contains struct { - Name string `validate:"contains=hello"` - } - assertSchema(t, Contains{}) - }) - - t.Run("endswith", func(t *testing.T) { - type EndsWith struct { - Name string `validate:"endswith=hello"` - } - assertSchema(t, EndsWith{}) - }) - - t.Run("startswith", func(t *testing.T) { - type StartsWith struct { - Name string `validate:"startswith=hello"` - } - assertSchema(t, StartsWith{}) - }) - - t.Run("bad", func(t *testing.T) { + assertValidators(t, reflect.TypeOf(""), []struct{ name, tag string }{ + {"eq", "eq=hello"}, + {"ne", "ne=hello"}, + {"oneof", "oneof=hello world"}, + {"oneof_separated", "oneof='a b c' 'd e f'"}, + {"len", "len=5"}, + {"min", "min=5"}, + {"max", "max=5"}, + {"minmax", "min=3,max=7"}, + {"gt", "gt=5"}, + {"gte", "gte=5"}, + {"lt", "lt=5"}, + {"lte", "lte=5"}, + {"contains", "contains=hello"}, + {"endswith", "endswith=hello"}, + {"startswith", "startswith=hello"}, + {"required", "required"}, + {"url_encoded", "url_encoded"}, + {"alpha", "alpha"}, + {"alphanum", "alphanum"}, + {"alphanumunicode", "alphanumunicode"}, + {"alphaunicode", "alphaunicode"}, + {"ascii", "ascii"}, + {"boolean", "boolean"}, + {"lowercase", "lowercase"}, + {"number", "number"}, + {"numeric", "numeric"}, + {"uppercase", "uppercase"}, + {"mongodb", "mongodb"}, + {"json_validator", "json"}, + {"latitude", "latitude"}, + {"longitude", "longitude"}, + {"md4", "md4"}, + }) + + t.Run("bad tag panics", func(t *testing.T) { type Bad struct { Name string `validate:"bad=hello"` } - assert.Panics(t, func() { - StructToZodSchema(Bad{}) - }) - }) - - t.Run("required", func(t *testing.T) { - type Required struct { - Name string `validate:"required"` - } - assertSchema(t, Required{}) - }) - - t.Run("email", func(t *testing.T) { - type Email struct { - Name string `validate:"email"` - } - assertSchema(t, Email{}, "v3", "v4") - }) - - t.Run("url", func(t *testing.T) { - type URL struct { - Name string `validate:"url"` - } - assertSchema(t, URL{}, "v3", "v4") - }) - - t.Run("ipv4", func(t *testing.T) { - type IPv4 struct { - Name string `validate:"ipv4"` - } - assertSchema(t, IPv4{}, "v3", "v4") - }) - - t.Run("ipv6", func(t *testing.T) { - type IPv6 struct { - Name string `validate:"ipv6"` - } - assertSchema(t, IPv6{}, "v3", "v4") - }) - - t.Run("ip4_addr", func(t *testing.T) { - type IP4Addr struct { - Name string `validate:"ip4_addr"` - } - assertSchema(t, IP4Addr{}, "v3", "v4") - }) - - t.Run("ip6_addr", func(t *testing.T) { - type IP6Addr struct { - Name string `validate:"ip6_addr"` - } - assertSchema(t, IP6Addr{}, "v3", "v4") - }) - - t.Run("ip", func(t *testing.T) { - type IP struct { - Name string `validate:"ip"` - } - assertSchema(t, IP{}, "v3", "v4") - }) - - t.Run("ip_addr", func(t *testing.T) { - type IPAddr struct { - Name string `validate:"ip_addr"` - } - assertSchema(t, IPAddr{}, "v3", "v4") - }) - - t.Run("http_url", func(t *testing.T) { - type HttpURL struct { - Name string `validate:"http_url"` - } - assertSchema(t, HttpURL{}, "v3", "v4") - }) - - t.Run("url_encoded", func(t *testing.T) { - type URLEncoded struct { - Name string `validate:"url_encoded"` - } - assertSchema(t, URLEncoded{}) - }) - - t.Run("alpha", func(t *testing.T) { - type Alpha struct { - Name string `validate:"alpha"` - } - assertSchema(t, Alpha{}) - }) - - t.Run("alphanum", func(t *testing.T) { - type AlphaNum struct { - Name string `validate:"alphanum"` - } - assertSchema(t, AlphaNum{}) - }) - - t.Run("alphanumunicode", func(t *testing.T) { - type AlphaNumUnicode struct { - Name string `validate:"alphanumunicode"` - } - assertSchema(t, AlphaNumUnicode{}) - }) - - t.Run("alphaunicode", func(t *testing.T) { - type AlphaUnicode struct { - Name string `validate:"alphaunicode"` - } - assertSchema(t, AlphaUnicode{}) - }) - - t.Run("ascii", func(t *testing.T) { - type ASCII struct { - Name string `validate:"ascii"` - } - assertSchema(t, ASCII{}) - }) - - t.Run("boolean", func(t *testing.T) { - type Boolean struct { - Name string `validate:"boolean"` - } - assertSchema(t, Boolean{}) - }) - - t.Run("lowercase", func(t *testing.T) { - type Lowercase struct { - Name string `validate:"lowercase"` - } - assertSchema(t, Lowercase{}) - }) - - t.Run("number", func(t *testing.T) { - type Number struct { - Name string `validate:"number"` - } - assertSchema(t, Number{}) - }) - - t.Run("numeric", func(t *testing.T) { - type Numeric struct { - Name string `validate:"numeric"` - } - assertSchema(t, Numeric{}) - }) - - t.Run("uppercase", func(t *testing.T) { - type Uppercase struct { - Name string `validate:"uppercase"` - } - assertSchema(t, Uppercase{}) - }) - - t.Run("base64", func(t *testing.T) { - type Base64 struct { - Name string `validate:"base64"` - } - assertSchema(t, Base64{}, "v3", "v4") - }) - - t.Run("mongodb", func(t *testing.T) { - type mongodb struct { - Name string `validate:"mongodb"` - } - assertSchema(t, mongodb{}) - }) - - t.Run("datetime", func(t *testing.T) { - type datetime struct { - Name string `validate:"datetime"` - } - assertSchema(t, datetime{}, "v3", "v4") - }) - - t.Run("hexadecimal", func(t *testing.T) { - type Hexadecimal struct { - Name string `validate:"hexadecimal"` - } - assertSchema(t, Hexadecimal{}, "v3", "v4") - }) - - t.Run("json", func(t *testing.T) { - type json struct { - Name string `validate:"json"` - } - assertSchema(t, json{}) - }) - - t.Run("latitude", func(t *testing.T) { - type Latitude struct { - Name string `validate:"latitude"` - } - assertSchema(t, Latitude{}) - }) - - t.Run("longitude", func(t *testing.T) { - type Longitude struct { - Name string `validate:"longitude"` - } - assertSchema(t, Longitude{}) - }) - - t.Run("uuid", func(t *testing.T) { - type UUID struct { - Name string `validate:"uuid"` - } - assertSchema(t, UUID{}, "v3", "v4") - }) - - t.Run("uuid3", func(t *testing.T) { - type UUID3 struct { - Name string `validate:"uuid3"` - } - assertSchema(t, UUID3{}, "v3", "v4") - }) - - t.Run("uuid3_rfc4122", func(t *testing.T) { - type UUID3RFC4122 struct { - Name string `validate:"uuid3_rfc4122"` - } - assertSchema(t, UUID3RFC4122{}, "v3", "v4") - }) - - t.Run("uuid4", func(t *testing.T) { - type UUID4 struct { - Name string `validate:"uuid4"` - } - assertSchema(t, UUID4{}, "v3", "v4") - }) - - t.Run("uuid4_rfc4122", func(t *testing.T) { - type UUID4RFC4122 struct { - Name string `validate:"uuid4_rfc4122"` - } - assertSchema(t, UUID4RFC4122{}, "v3", "v4") - }) - - t.Run("uuid5", func(t *testing.T) { - type UUID5 struct { - Name string `validate:"uuid5"` - } - assertSchema(t, UUID5{}, "v3", "v4") - }) - - t.Run("uuid5_rfc4122", func(t *testing.T) { - type UUID5RFC4122 struct { - Name string `validate:"uuid5_rfc4122"` - } - assertSchema(t, UUID5RFC4122{}, "v3", "v4") - }) - - t.Run("uuid_rfc4122", func(t *testing.T) { - type UUIDRFC4122 struct { - Name string `validate:"uuid_rfc4122"` - } - assertSchema(t, UUIDRFC4122{}, "v3", "v4") - }) - - t.Run("md4", func(t *testing.T) { - type MD4 struct { - Name string `validate:"md4"` - } - assertSchema(t, MD4{}) - }) - - t.Run("md5", func(t *testing.T) { - type MD5 struct { - Name string `validate:"md5"` - } - assertSchema(t, MD5{}, "v3", "v4") - }) - - t.Run("sha256", func(t *testing.T) { - type SHA256 struct { - Name string `validate:"sha256"` - } - assertSchema(t, SHA256{}, "v3", "v4") - }) - - t.Run("sha384", func(t *testing.T) { - type SHA384 struct { - Name string `validate:"sha384"` - } - assertSchema(t, SHA384{}, "v3", "v4") - }) - - t.Run("sha512", func(t *testing.T) { - type SHA512 struct { - Name string `validate:"sha512"` - } - assertSchema(t, SHA512{}, "v3", "v4") + assert.Panics(t, func() { StructToZodSchema(Bad{}) }) }) - t.Run("bad2", func(t *testing.T) { + t.Run("unknown tag panics", func(t *testing.T) { type Bad2 struct { Name string `validate:"bad2"` } - assert.Panics(t, func() { - StructToZodSchema(Bad2{}) - }) + assert.Panics(t, func() { StructToZodSchema(Bad2{}) }) }) } @@ -785,28 +465,36 @@ func TestZodV4Defaults(t *testing.T) { assertSchema(t, Payload{}, "v4") }) - t.Run("ip unions work when chain constraints precede ip tag", func(t *testing.T) { + t.Run("oneof takes precedence over ip specialization", func(t *testing.T) { type Payload struct { - Address string `validate:"required,ip"` + Address string `validate:"oneof='127.0.0.1' '::1',ip"` } - assertSchema(t, Payload{}, "v3", "v4") + assertSchema(t, Payload{}, "v4") }) - t.Run("oneof takes precedence over ip specialization", func(t *testing.T) { + t.Run("format combined with union panics", func(t *testing.T) { type Payload struct { - Address string `validate:"oneof='127.0.0.1' '::1',ip"` + Address string `validate:"email,ip"` } - assertSchema(t, Payload{}, "v4") + assert.Panics(t, func() { StructToZodSchema(Payload{}) }) }) - t.Run("ip mixed with another format falls back to legacy chain semantics", func(t *testing.T) { + t.Run("multiple formats panics", func(t *testing.T) { type Payload struct { - Address string `validate:"email,ip"` + Value string `validate:"email,url"` + } + + assert.Panics(t, func() { StructToZodSchema(Payload{}) }) + }) + + t.Run("optional format with nullable pointer", func(t *testing.T) { + type Payload struct { + Email *string `validate:"omitempty,email" json:"email"` } - goldenAssert(t, []byte(StructToZodSchema(Payload{})), withGoldenZodVersion("v4")) + assertSchema(t, Payload{}, "v3", "v4") }) t.Run("enum keyed maps become partial records", func(t *testing.T) { @@ -848,62 +536,19 @@ func TestZodV4Defaults(t *testing.T) { } func TestNumberValidations(t *testing.T) { - t.Run("gte_lte", func(t *testing.T) { - type User1 struct { - Age int `validate:"gte=18,lte=60"` - } - assertSchema(t, User1{}) - }) - - t.Run("gt_lt", func(t *testing.T) { - type User2 struct { - Age int `validate:"gt=18,lt=60"` - } - assertSchema(t, User2{}) - }) - - t.Run("eq", func(t *testing.T) { - type User3 struct { - Age int `validate:"eq=18"` - } - assertSchema(t, User3{}) - }) - - t.Run("ne", func(t *testing.T) { - type User4 struct { - Age int `validate:"ne=18"` - } - assertSchema(t, User4{}) - }) - - t.Run("oneof", func(t *testing.T) { - type User5 struct { - Age int `validate:"oneof=18 19 20"` - } - assertSchema(t, User5{}) - }) - - t.Run("min_max", func(t *testing.T) { - type User6 struct { - Age int `validate:"min=18,max=60"` - } - assertSchema(t, User6{}) - }) - - t.Run("len", func(t *testing.T) { - type User7 struct { - Age int `validate:"len=18"` - } - assertSchema(t, User7{}) - }) - - t.Run("bad", func(t *testing.T) { - type User8 struct { - Age int `validate:"bad=18"` - } - assert.Panics(t, func() { - StructToZodSchema(User8{}) - }) + assertValidators(t, reflect.TypeOf(0), []struct{ name, tag string }{ + {"gte_lte", "gte=18,lte=60"}, + {"gt_lt", "gt=18,lt=60"}, + {"eq", "eq=18"}, + {"ne", "ne=18"}, + {"oneof", "oneof=18 19 20"}, + {"min_max", "min=18,max=60"}, + {"len", "len=18"}, + }) + + t.Run("bad tag panics", func(t *testing.T) { + type Bad struct{ Age int `validate:"bad=18"` } + assert.Panics(t, func() { StructToZodSchema(Bad{}) }) }) } @@ -966,110 +611,32 @@ func TestMapWithStruct(t *testing.T) { } func TestMapWithValidations(t *testing.T) { - t.Run("required", func(t *testing.T) { - type Required struct { - Map map[string]string `validate:"required"` - } - assertSchema(t, Required{}) - }) - - t.Run("min", func(t *testing.T) { - type Min struct { - Map map[string]string `validate:"min=1"` - } - assertSchema(t, Min{}) - }) - - t.Run("max", func(t *testing.T) { - type Max struct { - Map map[string]string `validate:"max=1"` - } - assertSchema(t, Max{}) - }) - - t.Run("len", func(t *testing.T) { - type Len struct { - Map map[string]string `validate:"len=1"` - } - assertSchema(t, Len{}) - }) - - t.Run("minmax", func(t *testing.T) { - type MinMax struct { - Map map[string]string `validate:"min=1,max=2"` - } - assertSchema(t, MinMax{}) - }) - - t.Run("eq", func(t *testing.T) { - type Eq struct { - Map map[string]string `validate:"eq=1"` - } - assertSchema(t, Eq{}) - }) - - t.Run("ne", func(t *testing.T) { - type Ne struct { - Map map[string]string `validate:"ne=1"` - } - assertSchema(t, Ne{}) - }) - - t.Run("gt", func(t *testing.T) { - type Gt struct { - Map map[string]string `validate:"gt=1"` - } - assertSchema(t, Gt{}) - }) - - t.Run("gte", func(t *testing.T) { - type Gte struct { - Map map[string]string `validate:"gte=1"` - } - assertSchema(t, Gte{}) - }) - - t.Run("lt", func(t *testing.T) { - type Lt struct { - Map map[string]string `validate:"lt=1"` - } - assertSchema(t, Lt{}) - }) - - t.Run("lte", func(t *testing.T) { - type Lte struct { - Map map[string]string `validate:"lte=1"` - } - assertSchema(t, Lte{}) + assertValidators(t, reflect.TypeOf(map[string]string{}), []struct{ name, tag string }{ + {"required", "required"}, + {"min", "min=1"}, + {"max", "max=1"}, + {"len", "len=1"}, + {"minmax", "min=1,max=2"}, + {"eq", "eq=1"}, + {"ne", "ne=1"}, + {"gt", "gt=1"}, + {"gte", "gte=1"}, + {"lt", "lt=1"}, + {"lte", "lte=1"}, + {"dive1", "dive,min=2"}, + }) + + t.Run("dive_nested", func(t *testing.T) { + assertValidators(t, reflect.TypeOf([]map[string]string{}), []struct{ name, tag string }{ + {"dive2", "required,dive,min=2,dive,min=3"}, + {"dive3", "required,dive,min=2,dive,keys,min=3,endkeys,max=4"}, + }) }) - t.Run("bad", func(t *testing.T) { - type Bad struct { - Map map[string]string `validate:"bad=1"` - } + t.Run("bad tag panics", func(t *testing.T) { + type Bad struct{ Map map[string]string `validate:"bad=1"` } assert.Panics(t, func() { StructToZodSchema(Bad{}) }) }) - - t.Run("dive1", func(t *testing.T) { - type Dive1 struct { - Map map[string]string `validate:"dive,min=2"` - } - assertSchema(t, Dive1{}) - }) - - t.Run("dive2", func(t *testing.T) { - type Dive2 struct { - Map []map[string]string `validate:"required,dive,min=2,dive,min=3"` - } - assertSchema(t, Dive2{}) - }) - - t.Run("dive3", func(t *testing.T) { - type Dive3 struct { - Map []map[string]string `validate:"required,dive,min=2,dive,keys,min=3,endkeys,max=4"` - } - assertSchema(t, Dive3{}) - }) } func TestMapWithNonStringKey(t *testing.T) { @@ -1316,98 +883,38 @@ func TestConvertSlice(t *testing.T) { } func TestConvertSliceWithValidations(t *testing.T) { - t.Run("required", func(t *testing.T) { - type Required struct { - Slice []string `validate:"required"` - } - assertSchema(t, Required{}) - }) - - t.Run("min", func(t *testing.T) { - type Min struct { - Slice []string `validate:"min=1"` - } - assertSchema(t, Min{}) - }) - - t.Run("max", func(t *testing.T) { - type Max struct { - Slice []string `validate:"max=1"` - } - assertSchema(t, Max{}) - }) - - t.Run("len", func(t *testing.T) { - type Len struct { - Slice []string `validate:"len=1"` - } - assertSchema(t, Len{}) - }) - - t.Run("eq", func(t *testing.T) { - type Eq struct { - Slice []string `validate:"eq=1"` - } - assertSchema(t, Eq{}) - }) - - t.Run("gt", func(t *testing.T) { - type Gt struct { - Slice []string `validate:"gt=1"` - } - assertSchema(t, Gt{}) - }) - - t.Run("gte", func(t *testing.T) { - type Gte struct { - Slice []string `validate:"gte=1"` - } - assertSchema(t, Gte{}) - }) - - t.Run("lt", func(t *testing.T) { - type Lt struct { - Slice []string `validate:"lt=1"` - } - assertSchema(t, Lt{}) - }) - - t.Run("lte", func(t *testing.T) { - type Lte struct { - Slice []string `validate:"lte=1"` - } - assertSchema(t, Lte{}) + assertValidators(t, reflect.TypeOf([]string{}), []struct{ name, tag string }{ + {"required", "required"}, + {"min", "min=1"}, + {"max", "max=1"}, + {"len", "len=1"}, + {"eq", "eq=1"}, + {"gt", "gt=1"}, + {"gte", "gte=1"}, + {"lt", "lt=1"}, + {"lte", "lte=1"}, + {"ne", "ne=0"}, + }) + + t.Run("dive_nested", func(t *testing.T) { + assertValidators(t, reflect.TypeOf([][]string{}), []struct{ name, tag string }{ + {"dive1", "dive,required"}, + {"dive2", "required,dive,min=1"}, + }) }) - t.Run("ne", func(t *testing.T) { - type Ne struct { - Slice []string `validate:"ne=0"` - } - assertSchema(t, Ne{}) + t.Run("dive_oneof", func(t *testing.T) { + assertValidators(t, reflect.TypeOf([]string{}), []struct{ name, tag string }{ + {"dive_oneof", "dive,oneof=a b c"}, + }) }) - t.Run("bad_oneof", func(t *testing.T) { + t.Run("oneof without dive panics", func(t *testing.T) { assert.Panics(t, func() { - type Bad struct { - Slice []string `validate:"oneof=a b c"` - } + type Bad struct{ Slice []string `validate:"oneof=a b c"` } StructToZodSchema(Bad{}) }) }) - - t.Run("dive1", func(t *testing.T) { - type Dive1 struct { - Slice [][]string `validate:"dive,required"` - } - assertSchema(t, Dive1{}) - }) - - t.Run("dive2", func(t *testing.T) { - type Dive2 struct { - Slice [][]string `validate:"required,dive,min=1"` - } - assertSchema(t, Dive2{}) - }) } func TestRecursive1(t *testing.T) { @@ -1612,3 +1119,42 @@ func TestRecursiveEmbeddedWithPointersAndDates(t *testing.T) { assertSchema(t, Article{}, "v3", "v4") }) } + +func TestFormatValidators(t *testing.T) { + allFormats := []string{ + "email", "url", "http_url", + "ipv4", "ip4_addr", "ipv6", "ip6_addr", + "base64", "datetime", "hexadecimal", "jwt", + "uuid", "uuid3", "uuid3_rfc4122", + "uuid4", "uuid4_rfc4122", + "uuid5", "uuid5_rfc4122", + "uuid_rfc4122", + "md5", "sha256", "sha384", "sha512", + } + + unionFormats := []string{"ip", "ip_addr"} + + toValidators := func(tags []string, prefix string) []struct{ name, tag string } { + out := make([]struct{ name, tag string }, len(tags)) + for i, tag := range tags { + out[i] = struct{ name, tag string }{tag, prefix + tag} + } + return out + } + + t.Run("format only", func(t *testing.T) { + assertValidators(t, reflect.TypeOf(""), toValidators(allFormats, ""), "v3", "v4") + }) + + t.Run("format with required", func(t *testing.T) { + assertValidators(t, reflect.TypeOf(""), toValidators(allFormats, "required,"), "v3", "v4") + }) + + t.Run("union only", func(t *testing.T) { + assertValidators(t, reflect.TypeOf(""), toValidators(unionFormats, ""), "v3", "v4") + }) + + t.Run("union with required", func(t *testing.T) { + assertValidators(t, reflect.TypeOf(""), toValidators(unionFormats, "required,"), "v3", "v4") + }) +} From d67b838d816468127d5a62f39620102a91b5bde2 Mon Sep 17 00:00:00 2001 From: Ramil Amparo Date: Thu, 2 Apr 2026 20:15:42 +0400 Subject: [PATCH 07/35] Fix typecheck --- testdata/TestStringValidations.golden | Bin 5206 -> 5266 bytes zod_test.go | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/testdata/TestStringValidations.golden b/testdata/TestStringValidations.golden index 5ade99585ddcca4f87a197092be8e7dc0250d9f9..689ba05b18795d9a8ca49164c506b360cd2b3443 100644 GIT binary patch delta 119 zcmcbnF-dcS1>fX#f|`@>GqO#N<KOG_=XDXA<-%}-NE z%FoY9P0T|mne50UK6xA8YM==wlLZ9WCr{#+=Y&g7-oP)(1?Lq36^m`Y&tJ|005n7^ AE&u=k delta 85 zcmbQFc}-)31>fX@T=J9eGqO!i=hK}0h)IY&DL+3aH8F3pBa`^#D}1XbJMqa+b`TMr pyq8}_Lcvy{N-r}nEw#v|q_QA2KTRR8G&d==2&h Date: Thu, 2 Apr 2026 21:23:59 +0400 Subject: [PATCH 08/35] Improve tests --- .github/workflows/ci.yml | 8 +- .gitignore | 3 +- Makefile | 6 +- docker-typecheck.sh => docker-test.sh | 71 +- tests/cases.ts | 1602 +++++++++++++++++++++++++ tests/golden.test.ts | 90 ++ tests/zod.test.ts | 292 ----- 7 files changed, 1769 insertions(+), 303 deletions(-) rename docker-typecheck.sh => docker-test.sh (63%) create mode 100644 tests/cases.ts create mode 100644 tests/golden.test.ts delete mode 100644 tests/zod.test.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8010cc7..d29c9e8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,7 +2,7 @@ name: CI on: push: - branches: ['main'] + branches: ["main"] pull_request: types: [opened, synchronize] @@ -23,7 +23,7 @@ jobs: - uses: actions/setup-go@v5 with: - go-version: '^1.23.5' + go-version: "^1.23.5" - run: go version - name: Install gofumpt @@ -44,5 +44,5 @@ jobs: - name: Test run: make test - - name: Type-check golden files - run: make typecheck + - name: Run docker tests + run: make docker-test diff --git a/.gitignore b/.gitignore index 3a5ac12..59676ea 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .idea/ tests/ -!tests/zod.test.ts +!tests/cases.ts +!tests/golden.test.ts diff --git a/Makefile b/Makefile index ea02ca3..6a261ea 100644 --- a/Makefile +++ b/Makefile @@ -16,10 +16,10 @@ test-update: GOLDEN_UPDATE=true $(GOCMD) test ./... -typecheck: - ./docker-typecheck.sh +docker-test: + ./docker-test.sh bench: $(GOCMD) test -bench=. -benchmem ./... -.PHONY: test test-update lint linters-install typecheck bench +.PHONY: test test-update lint linters-install docker-test bench diff --git a/docker-typecheck.sh b/docker-test.sh similarity index 63% rename from docker-typecheck.sh rename to docker-test.sh index dc062bd..5081e27 100755 --- a/docker-typecheck.sh +++ b/docker-test.sh @@ -1,6 +1,6 @@ #!/bin/bash # -# Type-checks golden files against the correct zod version inside Docker. +# Type-checks and runtime-tests golden files inside Docker (zod v3 + v4). # # Golden files must contain these metadata comments to be included: # // @typecheck — present by default; files without it are skipped @@ -17,11 +17,12 @@ echo "" docker run --rm \ -v "${PROJECT_DIR}/testdata:/golden:ro" \ + -v "${PROJECT_DIR}/tests:/tests:ro" \ node:22-alpine \ sh -c ' set -e -mkdir -p /test/zod3 /test/zod4 +mkdir -p /test/zod3 /test/zod4 /test/golden zod3_count=0 zod4_count=0 @@ -58,6 +59,9 @@ for file in $(find /golden -name "*.golden" -type f); do zod4_count=$((zod4_count + 1)) ;; esac + + # Also copy to /test/golden/ for runtime tests (all versions) + prepare_ts "/test/golden/${ts_name}" done echo "Found ${zod3_count} files for zod@3, ${zod4_count} files for zod@4" @@ -119,6 +123,67 @@ for dir in zod3 zod4; do done echo "========================================" -echo "All type checks passed!" +echo "Type checks passed!" +echo "========================================" +echo "" + +# --- Phase 2: Runtime tests --- + +echo "========================================" +echo "Runtime Tests (vitest)" +echo "========================================" +echo "" + +for dir in zod3 zod4; do + label="zod@${dir#zod}" + version="${dir#zod}" # "3" or "4" + runtime_dir="/test/runtime-${dir}" + + mkdir -p "${runtime_dir}" + + # Copy test files + cp /tests/cases.ts "${runtime_dir}/" + cp /tests/golden.test.ts "${runtime_dir}/" + + zod_dep="^${version}" + + cat > "${runtime_dir}/package.json" < "${runtime_dir}/tsconfig.json" <&1 + ZOD_VERSION="v${version}" npx vitest run --reporter=verbose + + echo "" + echo "✓ ${label} runtime: PASSED" + echo "" +done + +echo "========================================" +echo "All checks passed!" echo "========================================" ' diff --git a/tests/cases.ts b/tests/cases.ts new file mode 100644 index 0000000..2eae563 --- /dev/null +++ b/tests/cases.ts @@ -0,0 +1,1602 @@ +/** + * Runtime test cases for golden file schemas. + * + * Each case references a golden file, a schema name exported from it, + * test input, whether parsing should succeed, and the expected output. + * + * Golden files are copied into the Docker test environment by docker-typecheck.sh. + * The import paths here are relative to the test runner's location in the container. + */ + +export interface TestCase { + /** Description of what this test verifies */ + name: string; + /** Path to golden file relative to testdata/ */ + golden: string; + /** Name of the exported schema to test (e.g. "UserSchema") */ + schema: string; + /** Input to pass to schema.safeParse() */ + input: unknown; + /** Whether parsing should succeed */ + success: boolean; + /** Expected output after parsing (only checked if success=true, if not provided expected output will be the same as the input) */ + output?: unknown; +} + +export const cases: TestCase[] = [ + // --------------------------------------------------------------------------- + // SIMPLE STRUCTS + // --------------------------------------------------------------------------- + + // --- TestStructSimple --- + { + name: "simple struct: parses valid object", + golden: "TestStructSimple.golden", + schema: "UserSchema", + input: { Name: "John", Age: 30, Height: 5.9 }, + success: true, + }, + { + name: "simple struct: rejects type error (string for Age)", + golden: "TestStructSimple.golden", + schema: "UserSchema", + input: { Name: "John", Age: "thirty", Height: 5.9 }, + success: false, + }, + + // --- TestStructSimplePrefix --- + { + name: "simple struct prefix: parses valid BotUser", + golden: "TestStructSimplePrefix.golden", + schema: "BotUserSchema", + input: { Name: "Bot", Age: 1, Height: 3.0 }, + success: true, + }, + + // --- TestStructSimpleWithOmittedField --- + { + name: "omitted field: parses valid object (omitted field not in schema)", + golden: "TestStructSimpleWithOmittedField.golden", + schema: "UserSchema", + input: { Name: "John", Age: 30, Height: 5.9 }, + success: true, + }, + + // --- TestStringOptional --- + { + name: "string optional: parses with Nickname present", + golden: "TestStringOptional.golden", + schema: "UserSchema", + input: { Name: "John", Nickname: "Johnny" }, + success: true, + }, + { + name: "string optional: parses without Nickname (undefined)", + golden: "TestStringOptional.golden", + schema: "UserSchema", + input: { Name: "John" }, + success: true, + }, + + // --- TestStringNullable --- + { + name: "string nullable: parses with null Nickname", + golden: "TestStringNullable.golden", + schema: "UserSchema", + input: { Name: "John", Nickname: null }, + success: true, + }, + + // --- TestStringOptionalNotNullable --- + { + name: "string optional not nullable: parses with undefined Nickname", + golden: "TestStringOptionalNotNullable.golden", + schema: "UserSchema", + input: { Name: "John" }, + success: true, + }, + + // --- TestStringOptionalNullable --- + { + name: "string optional nullable: parses with null Nickname", + golden: "TestStringOptionalNullable.golden", + schema: "UserSchema", + input: { Name: "John", Nickname: null }, + success: true, + }, + { + name: "string optional nullable: parses with undefined Nickname", + golden: "TestStringOptionalNullable.golden", + schema: "UserSchema", + input: { Name: "John" }, + success: true, + }, + + // --- TestDuration --- + { + name: "duration: parses valid number", + golden: "TestDuration.golden", + schema: "UserSchema", + input: { HowLong: 3600 }, + success: true, + }, + + // --- TestStructTime --- + { + name: "time: parses ISO string to Date", + golden: "TestStructTime.golden", + schema: "UserSchema", + input: { Name: "John", When: "2021-01-01T00:00:00Z" }, + success: true, + output: { Name: "John", When: new Date("2021-01-01T00:00:00Z") }, + }, + { + name: "time: parses unix timestamp to Date", + golden: "TestStructTime.golden", + schema: "UserSchema", + input: { Name: "John", When: 1609459200000 }, + success: true, + output: { Name: "John", When: new Date("2021-01-01T00:00:00Z") }, + }, + { + name: "time: coerces null to epoch Date", + golden: "TestStructTime.golden", + schema: "UserSchema", + input: { Name: "John", When: null }, + success: true, + output: { Name: "John", When: new Date(0) }, + }, + { + name: "time: parses zero date string", + golden: "TestStructTime.golden", + schema: "UserSchema", + input: { Name: "John", When: "0001-01-01T00:00:00Z" }, + success: true, + output: { Name: "John", When: new Date("0001-01-01T00:00:00Z") }, + }, + { + name: "time: rejects empty string", + golden: "TestStructTime.golden", + schema: "UserSchema", + input: { Name: "John", When: "" }, + success: false, + }, + + // --- TestTimeWithRequired --- + { + name: "required time: parses valid date", + golden: "TestTimeWithRequired.golden", + schema: "UserSchema", + input: { When: "2021-01-01T00:00:00Z" }, + success: true, + output: { When: new Date("2021-01-01T00:00:00Z") }, + }, + { + name: "required time: parses unix timestamp", + golden: "TestTimeWithRequired.golden", + schema: "UserSchema", + input: { When: 1609459200000 }, + success: true, + output: { When: new Date("2021-01-01T00:00:00Z") }, + }, + { + name: "required time: rejects null (zero date)", + golden: "TestTimeWithRequired.golden", + schema: "UserSchema", + input: { When: null }, + success: false, + }, + { + name: "required time: rejects zero date string", + golden: "TestTimeWithRequired.golden", + schema: "UserSchema", + input: { When: "0001-01-01T00:00:00Z" }, + success: false, + }, + { + name: "required time: rejects empty string", + golden: "TestTimeWithRequired.golden", + schema: "UserSchema", + input: { When: "" }, + success: false, + }, + + // --- TestCustom --- + { + name: "custom type: parses valid object", + golden: "TestCustom.golden", + schema: "UserSchema", + input: { Name: "John", Money: "100.00" }, + success: true, + }, + + // --------------------------------------------------------------------------- + // ARRAYS + // --------------------------------------------------------------------------- + + // --- TestStringArray --- + { + name: "string array: parses valid array", + golden: "TestStringArray.golden", + schema: "UserSchema", + input: { Tags: ["a", "b", "c"] }, + success: true, + }, + { + name: "string array: parses null", + golden: "TestStringArray.golden", + schema: "UserSchema", + input: { Tags: null }, + success: true, + }, + + // --- TestStringArrayNullable --- + { + name: "string array nullable: parses valid array", + golden: "TestStringArrayNullable.golden", + schema: "UserSchema", + input: { Name: "John", Tags: ["x"] }, + success: true, + }, + + // --- TestStringNestedArray --- + { + name: "nested array: parses valid nested array (inner length 2)", + golden: "TestStringNestedArray.golden", + schema: "UserSchema", + input: { + TagPairs: [ + ["a", "b"], + ["c", "d"], + ], + }, + success: true, + }, + + // --- TestConvertArray/single --- + { + name: "fixed array: parses array of length 10", + golden: "TestConvertArray/single.golden", + schema: "ArraySchema", + input: { Arr: ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j"] }, + success: true, + }, + { + name: "fixed array: rejects wrong count", + golden: "TestConvertArray/single.golden", + schema: "ArraySchema", + input: { Arr: ["a", "b"] }, + success: false, + }, + + // --- TestConvertArray/multi --- + { + name: "multi-dim array: parses valid 3D array", + golden: "TestConvertArray/multi.golden", + schema: "MultiArraySchema", + input: { + Arr: Array.from({ length: 10 }, () => + Array.from({ length: 20 }, () => Array.from({ length: 30 }, () => "x")), + ), + }, + success: true, + }, + + // --- TestConvertSlice --- + { + name: "convert slice: ZipSchema with valid Foo", + golden: "TestConvertSlice.golden", + schema: "ZipSchema", + input: { Zap: { Bar: "a", Baz: "b", Quz: "c" } }, + success: true, + }, + { + name: "convert slice: ZipSchema with null", + golden: "TestConvertSlice.golden", + schema: "ZipSchema", + input: { Zap: null }, + success: true, + }, + { + name: "convert slice: WhimSchema with valid Foo", + golden: "TestConvertSlice.golden", + schema: "WhimSchema", + input: { Wham: { Bar: "a", Baz: "b", Quz: "c" } }, + success: true, + }, + + // --- TestStructSlice --- + { + name: "struct slice: parses valid array", + golden: "TestStructSlice.golden", + schema: "UserSchema", + input: { Favourites: [{ Name: "Alice" }, { Name: "Bob" }] }, + success: true, + }, + { + name: "struct slice: parses null", + golden: "TestStructSlice.golden", + schema: "UserSchema", + input: { Favourites: null }, + success: true, + }, + + // --- TestStructSliceOptional --- + { + name: "struct slice optional: parses valid array", + golden: "TestStructSliceOptional.golden", + schema: "UserSchema", + input: { Favourites: [{ Name: "Alice" }] }, + success: true, + }, + { + name: "struct slice optional: parses undefined", + golden: "TestStructSliceOptional.golden", + schema: "UserSchema", + input: {}, + success: true, + }, + + // --- TestStructSliceOptionalNullable --- + { + name: "struct slice optional nullable: parses valid array", + golden: "TestStructSliceOptionalNullable.golden", + schema: "UserSchema", + input: { Favourites: [{ Name: "Alice" }] }, + success: true, + }, + { + name: "struct slice optional nullable: parses null", + golden: "TestStructSliceOptionalNullable.golden", + schema: "UserSchema", + input: { Favourites: null }, + success: true, + }, + { + name: "struct slice optional nullable: parses undefined", + golden: "TestStructSliceOptionalNullable.golden", + schema: "UserSchema", + input: {}, + success: true, + }, + + // --- TestSliceFields --- + { + name: "slice fields: parses valid object with all fields", + golden: "TestSliceFields.golden", + schema: "TestSliceFieldsStructSchema", + input: { + NoValidate: [1, 2], + Required: [1], + Min: [1], + OmitEmpty: [1, 2], + JSONOmitEmpty: [1, 2], + MinOmitEmpty: [1], + JSONMinOmitEmpty: [1], + }, + success: true, + }, + + // --- TestConvertSliceWithValidations --- + { + name: "slice validations: requiredSchema accepts array", + golden: "TestConvertSliceWithValidations.golden", + schema: "requiredSchema", + input: { value: ["a"] }, + success: true, + }, + { + name: "slice validations: minSchema accepts array with >= 1 item", + golden: "TestConvertSliceWithValidations.golden", + schema: "minSchema", + input: { value: ["a"] }, + success: true, + }, + { + name: "slice validations: minSchema rejects empty array", + golden: "TestConvertSliceWithValidations.golden", + schema: "minSchema", + input: { value: [] }, + success: false, + }, + { + name: "slice validations: maxSchema accepts array with <= 1 item", + golden: "TestConvertSliceWithValidations.golden", + schema: "maxSchema", + input: { value: ["a"] }, + success: true, + }, + { + name: "slice validations: maxSchema rejects array with > 1 item", + golden: "TestConvertSliceWithValidations.golden", + schema: "maxSchema", + input: { value: ["a", "b"] }, + success: false, + }, + { + name: "slice validations: lenSchema accepts array of length 1", + golden: "TestConvertSliceWithValidations.golden", + schema: "lenSchema", + input: { value: ["a"] }, + success: true, + }, + { + name: "slice validations: lenSchema rejects wrong length", + golden: "TestConvertSliceWithValidations.golden", + schema: "lenSchema", + input: { value: ["a", "b"] }, + success: false, + }, + { + name: "slice validations: eqSchema accepts array of length 1", + golden: "TestConvertSliceWithValidations.golden", + schema: "eqSchema", + input: { value: ["a"] }, + success: true, + }, + { + name: "slice validations: gtSchema accepts array with >= 2 items", + golden: "TestConvertSliceWithValidations.golden", + schema: "gtSchema", + input: { value: ["a", "b"] }, + success: true, + }, + { + name: "slice validations: gtSchema rejects array with < 2 items", + golden: "TestConvertSliceWithValidations.golden", + schema: "gtSchema", + input: { value: ["a"] }, + success: false, + }, + { + name: "slice validations: gteSchema accepts array with >= 1 items", + golden: "TestConvertSliceWithValidations.golden", + schema: "gteSchema", + input: { value: ["a"] }, + success: true, + }, + { + name: "slice validations: ltSchema accepts empty array", + golden: "TestConvertSliceWithValidations.golden", + schema: "ltSchema", + input: { value: [] }, + success: true, + }, + { + name: "slice validations: ltSchema rejects array with >= 1 items", + golden: "TestConvertSliceWithValidations.golden", + schema: "ltSchema", + input: { value: ["a"] }, + success: false, + }, + { + name: "slice validations: lteSchema accepts array with <= 1 item", + golden: "TestConvertSliceWithValidations.golden", + schema: "lteSchema", + input: { value: ["a"] }, + success: true, + }, + { + name: "slice validations: neSchema accepts non-empty array", + golden: "TestConvertSliceWithValidations.golden", + schema: "neSchema", + input: { value: ["a"] }, + success: true, + }, + { + name: "slice validations: neSchema rejects empty array", + golden: "TestConvertSliceWithValidations.golden", + schema: "neSchema", + input: { value: [] }, + success: false, + }, + + // --- TestConvertSliceWithValidations/dive_nested --- + { + name: "dive nested: dive1Schema accepts nested array", + golden: "TestConvertSliceWithValidations/dive_nested.golden", + schema: "dive1Schema", + input: { value: [["a", "b"], ["c"]] }, + success: true, + }, + { + name: "dive nested: dive2Schema accepts array of arrays with min 1", + golden: "TestConvertSliceWithValidations/dive_nested.golden", + schema: "dive2Schema", + input: { value: [["a"], ["b", "c"]] }, + success: true, + }, + + // --- TestConvertSliceWithValidations/dive_oneof --- + { + name: "dive oneof: accepts array of valid enum values", + golden: "TestConvertSliceWithValidations/dive_oneof.golden", + schema: "dive_oneofSchema", + input: { value: ["a", "b", "c"] }, + success: true, + }, + { + name: "dive oneof: rejects array with invalid enum value", + golden: "TestConvertSliceWithValidations/dive_oneof.golden", + schema: "dive_oneofSchema", + input: { value: ["a", "d"] }, + success: false, + }, + + // --------------------------------------------------------------------------- + // MAPS + // --------------------------------------------------------------------------- + + // --- TestMapStringToString --- + { + name: "map string to string: parses valid map", + golden: "TestMapStringToString.golden", + schema: "UserSchema", + input: { Name: "John", Metadata: { key: "val" } }, + success: true, + }, + { + name: "map string to string: parses null", + golden: "TestMapStringToString.golden", + schema: "UserSchema", + input: { Name: "John", Metadata: null }, + success: true, + }, + + // --- TestMapStringToInterface --- + { + name: "map string to interface: parses valid map with any values", + golden: "TestMapStringToInterface.golden", + schema: "UserSchema", + input: { Name: "John", Metadata: { key: 42, nested: { a: true } } }, + success: true, + }, + + // --- TestMapWithStruct --- + { + name: "map with struct: parses valid map", + golden: "TestMapWithStruct.golden", + schema: "UserSchema", + input: { MapWithStruct: { hello: { Title: "World" } } }, + success: true, + }, + + // --- TestMapWithValidations --- + { + name: "map validations: requiredSchema accepts map", + golden: "TestMapWithValidations.golden", + schema: "requiredSchema", + input: { value: { a: "b" } }, + success: true, + }, + { + name: "map validations: minSchema accepts map with >= 1 keys", + golden: "TestMapWithValidations.golden", + schema: "minSchema", + input: { value: { a: "b" } }, + success: true, + }, + { + name: "map validations: minSchema rejects empty map", + golden: "TestMapWithValidations.golden", + schema: "minSchema", + input: { value: {} }, + success: false, + }, + { + name: "map validations: maxSchema accepts map with <= 1 keys", + golden: "TestMapWithValidations.golden", + schema: "maxSchema", + input: { value: { a: "b" } }, + success: true, + }, + { + name: "map validations: maxSchema rejects map with > 1 keys", + golden: "TestMapWithValidations.golden", + schema: "maxSchema", + input: { value: { a: "b", c: "d" } }, + success: false, + }, + { + name: "map validations: lenSchema accepts map with exactly 1 key", + golden: "TestMapWithValidations.golden", + schema: "lenSchema", + input: { value: { a: "b" } }, + success: true, + }, + { + name: "map validations: lenSchema rejects map with != 1 keys", + golden: "TestMapWithValidations.golden", + schema: "lenSchema", + input: { value: { a: "b", c: "d" } }, + success: false, + }, + { + name: "map validations: eqSchema accepts map with exactly 1 key", + golden: "TestMapWithValidations.golden", + schema: "eqSchema", + input: { value: { a: "b" } }, + success: true, + }, + { + name: "map validations: neSchema accepts map with != 1 keys", + golden: "TestMapWithValidations.golden", + schema: "neSchema", + input: { value: { a: "b", c: "d" } }, + success: true, + }, + { + name: "map validations: neSchema rejects map with exactly 1 key", + golden: "TestMapWithValidations.golden", + schema: "neSchema", + input: { value: { a: "b" } }, + success: false, + }, + { + name: "map validations: gtSchema accepts map with > 1 keys", + golden: "TestMapWithValidations.golden", + schema: "gtSchema", + input: { value: { a: "b", c: "d" } }, + success: true, + }, + { + name: "map validations: gtSchema rejects map with <= 1 keys", + golden: "TestMapWithValidations.golden", + schema: "gtSchema", + input: { value: { a: "b" } }, + success: false, + }, + { + name: "map validations: gteSchema accepts map with >= 1 keys", + golden: "TestMapWithValidations.golden", + schema: "gteSchema", + input: { value: { a: "b" } }, + success: true, + }, + { + name: "map validations: ltSchema accepts empty map", + golden: "TestMapWithValidations.golden", + schema: "ltSchema", + input: { value: {} }, + success: true, + }, + { + name: "map validations: ltSchema rejects map with >= 1 keys", + golden: "TestMapWithValidations.golden", + schema: "ltSchema", + input: { value: { a: "b" } }, + success: false, + }, + { + name: "map validations: lteSchema accepts map with <= 1 keys", + golden: "TestMapWithValidations.golden", + schema: "lteSchema", + input: { value: { a: "b" } }, + success: true, + }, + { + name: "map validations: dive1Schema accepts map with values >= 2 chars", + golden: "TestMapWithValidations.golden", + schema: "dive1Schema", + input: { value: { key: "ab" } }, + success: true, + }, + + // --- TestMapWithValidations/dive_nested --- + { + name: "map dive nested: dive2Schema accepts array of maps", + golden: "TestMapWithValidations/dive_nested.golden", + schema: "dive2Schema", + input: { value: [{ aaa: "bbb", ccc: "ddd" }] }, + success: true, + }, + { + name: "map dive nested: dive3Schema accepts array of maps with key/value constraints", + golden: "TestMapWithValidations/dive_nested.golden", + schema: "dive3Schema", + input: { value: [{ abc: "abcd", def: "ef" }] }, + success: true, + }, + + // --- TestMapWithNonStringKey/int_key --- + { + name: "map int key: parses valid map with coerced number keys", + golden: "TestMapWithNonStringKey/int_key.golden", + schema: "Map1Schema", + input: { Name: "John", Metadata: { "1": "one", "2": "two" } }, + success: true, + }, + + // --- TestMapWithNonStringKey/float_key --- + { + name: "map float key: parses valid map with coerced number keys", + golden: "TestMapWithNonStringKey/float_key.golden", + schema: "Map3Schema", + input: { Name: "John", Metadata: { "1.5": "one-half", "2.5": "two-half" } }, + success: true, + }, + + // --- TestMapWithNonStringKey/time_key --- + { + name: "map time key: parses valid map with string keys", + golden: "TestMapWithNonStringKey/time_key.golden", + schema: "Map2Schema", + input: { Name: "John", Metadata: { "2021-01-01T00:00:00Z": "new year" } }, + success: true, + }, + + // --- TestNullableWithValidations --- + { + name: "nullable with validations: parses full valid object", + golden: "TestNullableWithValidations.golden", + schema: "UserSchema", + input: { + Name: "John", + PtrMapOptionalNullable1: null, + PtrMapOptionalNullable2: null, + PtrMap1: { a: 1, b: 2, c: 3 }, + PtrMap2: { a: 1, b: 2, c: 3 }, + PtrMapNullable: { a: 1, b: 2, c: 3 }, + MapOptional1: undefined, + MapOptional2: undefined, + Map1: { a: 1, b: 2, c: 3 }, + Map2: { a: 1, b: 2, c: 3 }, + MapNullable: { a: 1, b: 2, c: 3 }, + PtrSliceOptionalNullable1: null, + PtrSliceOptionalNullable2: null, + PtrSlice1: ["a", "b", "c"], + PtrSlice2: ["a", "b", "c"], + PtrSliceNullable: ["a", "b", "c"], + SliceOptional1: undefined, + SliceOptional2: undefined, + Slice1: ["a", "b", "c"], + Slice2: ["a", "b", "c"], + SliceNullable: ["a", "b", "c"], + PtrIntOptional1: undefined, + PtrIntOptional2: undefined, + PtrInt1: 3, + PtrInt2: 3, + PtrIntNullable: 3, + PtrStringOptional1: undefined, + PtrStringOptional2: undefined, + PtrString1: "abc", + PtrString2: "abc", + PtrStringNullable: "abc", + }, + success: true, + }, + + // --------------------------------------------------------------------------- + // NESTED V4 + // --------------------------------------------------------------------------- + + // --- TestNestedStruct/v4 --- + { + name: "nested struct v4: parses valid object with spread shapes", + golden: "TestNestedStruct/v4.golden", + schema: "UserSchema", + input: { Tags: ["a", "b"], ID: "123", name: "John" }, + success: true, + }, + + // --- TestRecursive1/v4 --- + { + name: "recursive1 v4: parses nested children", + golden: "TestRecursive1/v4.golden", + schema: "NestedItemSchema", + input: { + id: 1, + title: "Root", + pos: 0, + parent_id: 0, + project_id: 1, + children: [ + { + id: 2, + title: "Child", + pos: 1, + parent_id: 1, + project_id: 1, + children: null, + }, + ], + }, + success: true, + }, + + // --- TestRecursive2/v4 --- + { + name: "recursive2 v4: parses ParentSchema with nested next", + golden: "TestRecursive2/v4.golden", + schema: "ParentSchema", + input: { + child: { + value: 1, + next: { + value: 2, + next: null, + }, + }, + }, + success: true, + }, + + // --- TestRecursiveEmbeddedStruct/v4 --- + { + name: "recursive embedded v4: parses ItemBSchema", + golden: "TestRecursiveEmbeddedStruct/v4.golden", + schema: "ItemBSchema", + input: { + Name: "root", + Children: [ + { Name: "child1", Children: null }, + { Name: "child2", Children: [{ Name: "grandchild", Children: null }] }, + ], + }, + success: true, + }, + + // --- TestRecursiveEmbeddedWithPointersAndDates/recursive_struct_with_pointer_field_and_date/v4 --- + { + name: "recursive with dates v4: parses TreeSchema", + golden: + "TestRecursiveEmbeddedWithPointersAndDates/recursive_struct_with_pointer_field_and_date/v4.golden", + schema: "TreeSchema", + input: { + UpdatedAt: "2021-01-01T00:00:00Z", + Value: "root", + CreatedAt: "2021-01-01T00:00:00Z", + Children: [ + { + Value: "child", + CreatedAt: "2021-02-01T00:00:00Z", + Children: null, + }, + ], + }, + success: true, + output: { + UpdatedAt: new Date("2021-01-01T00:00:00Z"), + Value: "root", + CreatedAt: new Date("2021-01-01T00:00:00Z"), + Children: [ + { + Value: "child", + CreatedAt: new Date("2021-02-01T00:00:00Z"), + Children: null, + }, + ], + }, + }, + + // --- TestRecursiveEmbeddedWithPointersAndDates/embedded_struct_with_pointer_to_self_and_date/v4 --- + { + name: "embedded self-pointer with dates v4: parses ArticleSchema", + golden: + "TestRecursiveEmbeddedWithPointersAndDates/embedded_struct_with_pointer_to_self_and_date/v4.golden", + schema: "ArticleSchema", + input: { + Title: "Article", + Text: "Hello", + Timestamp: "2021-01-01T00:00:00Z", + Reply: { + Text: "Reply", + Timestamp: "2021-02-01T00:00:00Z", + Reply: null, + }, + }, + success: true, + output: { + Title: "Article", + Text: "Hello", + Timestamp: new Date("2021-01-01T00:00:00Z"), + Reply: { + Text: "Reply", + Timestamp: new Date("2021-02-01T00:00:00Z"), + Reply: null, + }, + }, + }, + + // --------------------------------------------------------------------------- + // STRING VALIDATIONS + // --------------------------------------------------------------------------- + + // --- eqSchema --- + { + name: "string eq: accepts exact match 'hello'", + golden: "TestStringValidations.golden", + schema: "eqSchema", + input: { value: "hello" }, + success: true, + }, + { + name: "string eq: rejects non-match", + golden: "TestStringValidations.golden", + schema: "eqSchema", + input: { value: "world" }, + success: false, + }, + + // --- neSchema --- + { + name: "string ne: accepts value not 'hello'", + golden: "TestStringValidations.golden", + schema: "neSchema", + input: { value: "world" }, + success: true, + }, + { + name: "string ne: rejects 'hello'", + golden: "TestStringValidations.golden", + schema: "neSchema", + input: { value: "hello" }, + success: false, + }, + + // --- oneofSchema --- + { + name: "string oneof: accepts 'hello'", + golden: "TestStringValidations.golden", + schema: "oneofSchema", + input: { value: "hello" }, + success: true, + }, + { + name: "string oneof: rejects invalid value", + golden: "TestStringValidations.golden", + schema: "oneofSchema", + input: { value: "invalid" }, + success: false, + }, + + // --- lenSchema --- + { + name: "string len: accepts string of length 5", + golden: "TestStringValidations.golden", + schema: "lenSchema", + input: { value: "abcde" }, + success: true, + }, + { + name: "string len: rejects string of wrong length", + golden: "TestStringValidations.golden", + schema: "lenSchema", + input: { value: "abc" }, + success: false, + }, + + // --- minSchema --- + { + name: "string min: accepts string of length >= 5", + golden: "TestStringValidations.golden", + schema: "minSchema", + input: { value: "abcde" }, + success: true, + }, + { + name: "string min: rejects string of length < 5", + golden: "TestStringValidations.golden", + schema: "minSchema", + input: { value: "abc" }, + success: false, + }, + + // --- maxSchema --- + { + name: "string max: accepts string of length <= 5", + golden: "TestStringValidations.golden", + schema: "maxSchema", + input: { value: "abcde" }, + success: true, + }, + { + name: "string max: rejects string of length > 5", + golden: "TestStringValidations.golden", + schema: "maxSchema", + input: { value: "abcdef" }, + success: false, + }, + + // --- containsSchema --- + { + name: "string contains: accepts string containing 'hello'", + golden: "TestStringValidations.golden", + schema: "containsSchema", + input: { value: "say hello world" }, + success: true, + }, + { + name: "string contains: rejects string not containing 'hello'", + golden: "TestStringValidations.golden", + schema: "containsSchema", + input: { value: "goodbye" }, + success: false, + }, + + // --- startswithSchema --- + { + name: "string startswith: accepts string starting with 'hello'", + golden: "TestStringValidations.golden", + schema: "startswithSchema", + input: { value: "hello world" }, + success: true, + }, + { + name: "string startswith: rejects string not starting with 'hello'", + golden: "TestStringValidations.golden", + schema: "startswithSchema", + input: { value: "world hello" }, + success: false, + }, + + // --- endswithSchema --- + { + name: "string endswith: accepts string ending with 'hello'", + golden: "TestStringValidations.golden", + schema: "endswithSchema", + input: { value: "world hello" }, + success: true, + }, + { + name: "string endswith: rejects string not ending with 'hello'", + golden: "TestStringValidations.golden", + schema: "endswithSchema", + input: { value: "hello world" }, + success: false, + }, + + // --- requiredSchema --- + { + name: "string required: accepts non-empty string", + golden: "TestStringValidations.golden", + schema: "requiredSchema", + input: { value: "a" }, + success: true, + }, + { + name: "string required: rejects empty string", + golden: "TestStringValidations.golden", + schema: "requiredSchema", + input: { value: "" }, + success: false, + }, + + // --- lowercaseSchema --- + { + name: "string lowercase: accepts lowercase", + golden: "TestStringValidations.golden", + schema: "lowercaseSchema", + input: { value: "hello" }, + success: true, + }, + { + name: "string lowercase: rejects uppercase", + golden: "TestStringValidations.golden", + schema: "lowercaseSchema", + input: { value: "Hello" }, + success: false, + }, + + // --- uppercaseSchema --- + { + name: "string uppercase: accepts uppercase", + golden: "TestStringValidations.golden", + schema: "uppercaseSchema", + input: { value: "HELLO" }, + success: true, + }, + { + name: "string uppercase: rejects lowercase", + golden: "TestStringValidations.golden", + schema: "uppercaseSchema", + input: { value: "Hello" }, + success: false, + }, + + // --- boolean_validatorSchema --- + { + name: "string boolean: accepts 'true'", + golden: "TestStringValidations.golden", + schema: "boolean_validatorSchema", + input: { value: "true" }, + success: true, + }, + { + name: "string boolean: rejects 'yes'", + golden: "TestStringValidations.golden", + schema: "boolean_validatorSchema", + input: { value: "yes" }, + success: false, + }, + + // --- json_validatorSchema --- + { + name: "string json: accepts valid JSON", + golden: "TestStringValidations.golden", + schema: "json_validatorSchema", + input: { value: '{"key":"value"}' }, + success: true, + }, + { + name: "string json: rejects invalid JSON", + golden: "TestStringValidations.golden", + schema: "json_validatorSchema", + input: { value: "{invalid" }, + success: false, + }, + + // --- alphaSchema --- + { + name: "string alpha: accepts alpha-only", + golden: "TestStringValidations.golden", + schema: "alphaSchema", + input: { value: "hello" }, + success: true, + }, + { + name: "string alpha: rejects non-alpha", + golden: "TestStringValidations.golden", + schema: "alphaSchema", + input: { value: "hello123" }, + success: false, + }, + + // --- number_validatorSchema --- + { + name: "string number: accepts digits only", + golden: "TestStringValidations.golden", + schema: "number_validatorSchema", + input: { value: "12345" }, + success: true, + }, + { + name: "string number: rejects non-digit", + golden: "TestStringValidations.golden", + schema: "number_validatorSchema", + input: { value: "123abc" }, + success: false, + }, + + // --------------------------------------------------------------------------- + // NUMBER VALIDATIONS + // --------------------------------------------------------------------------- + + // --- gte_lteSchema --- + { + name: "number gte_lte: accepts 18", + golden: "TestNumberValidations.golden", + schema: "gte_lteSchema", + input: { value: 18 }, + success: true, + }, + { + name: "number gte_lte: accepts 60", + golden: "TestNumberValidations.golden", + schema: "gte_lteSchema", + input: { value: 60 }, + success: true, + }, + { + name: "number gte_lte: rejects 17", + golden: "TestNumberValidations.golden", + schema: "gte_lteSchema", + input: { value: 17 }, + success: false, + }, + { + name: "number gte_lte: rejects 61", + golden: "TestNumberValidations.golden", + schema: "gte_lteSchema", + input: { value: 61 }, + success: false, + }, + + // --- gt_ltSchema --- + { + name: "number gt_lt: accepts 19", + golden: "TestNumberValidations.golden", + schema: "gt_ltSchema", + input: { value: 19 }, + success: true, + }, + { + name: "number gt_lt: rejects 18 (not >18)", + golden: "TestNumberValidations.golden", + schema: "gt_ltSchema", + input: { value: 18 }, + success: false, + }, + { + name: "number gt_lt: rejects 60 (not <60)", + golden: "TestNumberValidations.golden", + schema: "gt_ltSchema", + input: { value: 60 }, + success: false, + }, + + // --- number eqSchema --- + { + name: "number eq: accepts 18", + golden: "TestNumberValidations.golden", + schema: "eqSchema", + input: { value: 18 }, + success: true, + }, + { + name: "number eq: rejects 19", + golden: "TestNumberValidations.golden", + schema: "eqSchema", + input: { value: 19 }, + success: false, + }, + + // --- number neSchema --- + { + name: "number ne: accepts 19", + golden: "TestNumberValidations.golden", + schema: "neSchema", + input: { value: 19 }, + success: true, + }, + { + name: "number ne: rejects 18", + golden: "TestNumberValidations.golden", + schema: "neSchema", + input: { value: 18 }, + success: false, + }, + + // --- number oneofSchema --- + { + name: "number oneof: accepts 18", + golden: "TestNumberValidations.golden", + schema: "oneofSchema", + input: { value: 18 }, + success: true, + }, + { + name: "number oneof: rejects 21", + golden: "TestNumberValidations.golden", + schema: "oneofSchema", + input: { value: 21 }, + success: false, + }, + + // --- number min_maxSchema --- + { + name: "number min_max: accepts 30", + golden: "TestNumberValidations.golden", + schema: "min_maxSchema", + input: { value: 30 }, + success: true, + }, + + // --- number lenSchema --- + { + name: "number len: accepts 18", + golden: "TestNumberValidations.golden", + schema: "lenSchema", + input: { value: 18 }, + success: true, + }, + + // --------------------------------------------------------------------------- + // FORMAT VALIDATORS V4 + // --------------------------------------------------------------------------- + + // --- emailSchema --- + { + name: "email: accepts valid email", + golden: "TestFormatValidators/format_only/v4.golden", + schema: "emailSchema", + input: { value: "test@example.com" }, + success: true, + }, + { + name: "email: rejects invalid email", + golden: "TestFormatValidators/format_only/v4.golden", + schema: "emailSchema", + input: { value: "notanemail" }, + success: false, + }, + + // --- urlSchema --- + { + name: "url: accepts valid url", + golden: "TestFormatValidators/format_only/v4.golden", + schema: "urlSchema", + input: { value: "https://example.com" }, + success: true, + }, + { + name: "url: rejects invalid url", + golden: "TestFormatValidators/format_only/v4.golden", + schema: "urlSchema", + input: { value: "not a url" }, + success: false, + }, + + // --- ipv4Schema --- + { + name: "ipv4: accepts valid ipv4", + golden: "TestFormatValidators/format_only/v4.golden", + schema: "ipv4Schema", + input: { value: "127.0.0.1" }, + success: true, + }, + { + name: "ipv4: rejects invalid ipv4", + golden: "TestFormatValidators/format_only/v4.golden", + schema: "ipv4Schema", + input: { value: "999.999.999.999" }, + success: false, + }, + + // --- ipv6Schema --- + { + name: "ipv6: accepts valid ipv6", + golden: "TestFormatValidators/format_only/v4.golden", + schema: "ipv6Schema", + input: { value: "::1" }, + success: true, + }, + { + name: "ipv6: rejects invalid ipv6", + golden: "TestFormatValidators/format_only/v4.golden", + schema: "ipv6Schema", + input: { value: "not-ipv6" }, + success: false, + }, + + // --- base64Schema --- + { + name: "base64: accepts valid base64", + golden: "TestFormatValidators/format_only/v4.golden", + schema: "base64Schema", + input: { value: "SGVsbG8=" }, + success: true, + }, + { + name: "base64: rejects invalid base64", + golden: "TestFormatValidators/format_only/v4.golden", + schema: "base64Schema", + input: { value: "not base64!!!" }, + success: false, + }, + + // --- uuid4Schema --- + { + name: "uuid4: accepts valid uuid v4", + golden: "TestFormatValidators/format_only/v4.golden", + schema: "uuid4Schema", + input: { value: "550e8400-e29b-41d4-a716-446655440000" }, + success: true, + }, + { + name: "uuid4: rejects invalid uuid", + golden: "TestFormatValidators/format_only/v4.golden", + schema: "uuid4Schema", + input: { value: "not-a-uuid" }, + success: false, + }, + + // --- md5Schema --- + { + name: "md5: accepts valid md5 hash", + golden: "TestFormatValidators/format_only/v4.golden", + schema: "md5Schema", + input: { value: "d41d8cd98f00b204e9800998ecf8427e" }, + success: true, + }, + { + name: "md5: rejects invalid md5", + golden: "TestFormatValidators/format_only/v4.golden", + schema: "md5Schema", + input: { value: "not-a-hash" }, + success: false, + }, + + // --------------------------------------------------------------------------- + // UNION V4 + // --------------------------------------------------------------------------- + + // --- ipSchema --- + { + name: "ip union: accepts valid ipv4", + golden: "TestFormatValidators/union_only/v4.golden", + schema: "ipSchema", + input: { value: "127.0.0.1" }, + success: true, + }, + { + name: "ip union: accepts valid ipv6", + golden: "TestFormatValidators/union_only/v4.golden", + schema: "ipSchema", + input: { value: "::1" }, + success: true, + }, + { + name: "ip union: rejects invalid ip", + golden: "TestFormatValidators/union_only/v4.golden", + schema: "ipSchema", + input: { value: "notanip" }, + success: false, + }, + + // --------------------------------------------------------------------------- + // SPECIAL + // --------------------------------------------------------------------------- + + // --- TestEverything --- + { + name: "everything: parses full valid object", + golden: "TestEverything.golden", + schema: "UserSchema", + input: { + Name: "John", + Nickname: null, + Age: 30, + Height: 5.9, + OldPostWithMetaData: { Title: "Hello", Post: { Title: "World" } }, + Tags: ["a", "b"], + TagsOptional: ["x"], + TagsOptionalNullable: null, + Favourites: [{ Name: "Alice" }], + Posts: [{ Title: "Post1" }], + Post: { Title: "Main" }, + PostOptional: { Title: "Optional" }, + PostOptionalNullable: null, + Metadata: { key: "val" }, + MetadataOptional: undefined, + MetadataOptionalNullable: null, + ExtendedProps: { any: "thing" }, + ExtendedPropsOptional: null, + ExtendedPropsNullable: null, + ExtendedPropsOptionalNullable: null, + ExtendedPropsVeryIndirect: null, + NewPostWithMetaData: { Title: "New", Post: { Title: "Inner" } }, + VeryNewPost: { Title: "VeryNew" }, + MapWithStruct: { k: { Title: "T", Post: { Title: "P" } } }, + }, + success: true, + }, + + // --- TestEverythingWithValidations --- + { + name: "everything with validations: parses full valid object", + golden: "TestEverythingWithValidations.golden", + schema: "UserSchema", + input: { + Name: "John", + Nickname: null, + Age: 18, + Height: 1.5, + OldPostWithMetaData: { Title: "Hello", Post: { Title: "World" } }, + Tags: ["a", "b"], + TagsOptional: ["a", "b"], + TagsOptionalNullable: ["a", "b"], + Favourites: null, + Posts: [{ Title: "Hello" }], + Post: { Title: "Hello" }, + PostOptional: { Title: "Hello" }, + PostOptionalNullable: { Title: "Hello" }, + Metadata: null, + MetadataLength: { Hello: "World" }, + MetadataOptional: undefined, + MetadataOptionalNullable: null, + ExtendedProps: null, + ExtendedPropsOptional: undefined, + ExtendedPropsNullable: null, + ExtendedPropsOptionalNullable: null, + ExtendedPropsVeryIndirect: null, + NewPostWithMetaData: { Title: "Hello", Post: { Title: "World" } }, + VeryNewPost: { Title: "Hello" }, + MapWithStruct: { + Hello: { Title: "World", Post: { Title: "Hello" } }, + }, + }, + success: true, + }, + + // --- TestGenerics --- + { + name: "generics: StringIntPairSchema", + golden: "TestGenerics.golden", + schema: "StringIntPairSchema", + input: { First: "hello", Second: 42 }, + success: true, + }, + { + name: "generics: GenericPairIntBoolSchema", + golden: "TestGenerics.golden", + schema: "GenericPairIntBoolSchema", + input: { First: 1, Second: true }, + success: true, + }, + { + name: "generics: PairMapStringIntBoolSchema", + golden: "TestGenerics.golden", + schema: "PairMapStringIntBoolSchema", + input: { items: { key: { First: 1, Second: false } } }, + success: true, + }, + + // --- TestInterfaceAny --- + { + name: "interface any: accepts any value for Metadata", + golden: "TestInterfaceAny.golden", + schema: "UserSchema", + input: { Name: "John", Metadata: { anything: [1, 2, 3] } }, + success: true, + }, + + // --- TestCustomTag/v4 --- + { + name: "custom tag v4: parses SortParamsSchema", + golden: "TestCustomTag/v4.golden", + schema: "SortParamsSchema", + input: { order: "asc", field: "name" }, + success: true, + }, + + // --- TestZodV4Defaults/enum_keyed_maps_become_partial_records --- + { + name: "v4 defaults: enum keyed maps become partial records", + golden: "TestZodV4Defaults/enum_keyed_maps_become_partial_records.golden", + schema: "PayloadSchema", + input: { Metadata: { draft: "some note" } }, + success: true, + }, + + // --- TestZodV4Defaults/ip_unions_inherit_generic_string_constraints --- + { + name: "v4 defaults: ip unions inherit generic string constraints", + golden: + "TestZodV4Defaults/ip_unions_inherit_generic_string_constraints.golden", + schema: "PayloadSchema", + input: { Address: "127.0.0.1" }, + success: true, + }, + + // --- TestZodV4Defaults/oneof_takes_precedence_over_ip_specialization --- + { + name: "v4 defaults: oneof takes precedence over ip specialization", + golden: + "TestZodV4Defaults/oneof_takes_precedence_over_ip_specialization.golden", + schema: "PayloadSchema", + input: { Address: "127.0.0.1" }, + success: true, + }, + + // --- TestZodV4Defaults/optional_format_with_nullable_pointer/v4 --- + { + name: "v4 defaults: optional format with nullable pointer accepts null", + golden: "TestZodV4Defaults/optional_format_with_nullable_pointer/v4.golden", + schema: "PayloadSchema", + input: { email: null }, + success: true, + }, + { + name: "v4 defaults: optional format with nullable pointer accepts valid email", + golden: "TestZodV4Defaults/optional_format_with_nullable_pointer/v4.golden", + schema: "PayloadSchema", + input: { email: "test@example.com" }, + success: true, + }, + + // --- TestZodV4Defaults/string_formats_use_zod_v4_builders --- + { + name: "v4 defaults: string formats use zod v4 builders", + golden: "TestZodV4Defaults/string_formats_use_zod_v4_builders.golden", + schema: "PayloadSchema", + input: { + Email: "test@example.com", + Link: "https://example.com", + Base64: "SGVsbG8=", + ID: "550e8400-e29b-41d4-a716-446655440000", + Checksum: "d41d8cd98f00b204e9800998ecf8427e", + }, + success: true, + }, +]; diff --git a/tests/golden.test.ts b/tests/golden.test.ts new file mode 100644 index 0000000..22ac45f --- /dev/null +++ b/tests/golden.test.ts @@ -0,0 +1,90 @@ +/** + * Runtime tests for golden file schemas. + * + * Dynamically imports schemas from golden files and tests them against + * the cases defined in cases.ts. Run inside Docker via docker-typecheck.sh. + * + * The ZOD_VERSION env var ("v3" or "v4") determines which zod version is active. + * Golden files with a @zod-version metadata that doesn't match are skipped. + */ +import { describe, expect, it } from "vitest"; +import { readFileSync } from "fs"; +import { cases } from "./cases"; + +// Golden files are copied to /test/golden/ as .ts files by the docker script. +const GOLDEN_DIR = "/test/golden"; + +// Which zod version we're testing under (set by docker script) +const currentZodVersion = process.env.ZOD_VERSION || "v4"; + +// Cache for imported golden modules +const moduleCache = new Map>(); + +// Cache for golden file zod version metadata +const versionCache = new Map(); + +function getGoldenZodVersion(golden: string): string | null { + if (!versionCache.has(golden)) { + const tsName = golden.replace(/\//g, "__").replace(/\.golden$/, ".ts"); + try { + const content = readFileSync(`${GOLDEN_DIR}/${tsName}`, "utf-8"); + // The docker script strips // @ comments but the version is in the original. + // Since prepare_ts strips metadata lines, we check the golden source directly. + // Actually, the golden source is at /golden/ (mounted read-only from testdata/). + const goldenSource = readFileSync( + `/golden/${golden}`, + "utf-8" + ); + const match = goldenSource.match(/^\/\/ @zod-version: (v\d+)/m); + versionCache.set(golden, match ? match[1] : null); + } catch { + versionCache.set(golden, null); + } + } + return versionCache.get(golden)!; +} + +function shouldSkip(golden: string): boolean { + const version = getGoldenZodVersion(golden); + // null means "both versions" — always run + if (version === null) return false; + // Skip if the golden file's version doesn't match the current zod version + return version !== currentZodVersion; +} + +async function getSchema(golden: string, schemaName: string) { + if (!moduleCache.has(golden)) { + const tsName = golden.replace(/\//g, "__").replace(/\.golden$/, ".ts"); + const mod = await import(`${GOLDEN_DIR}/${tsName}`); + moduleCache.set(golden, mod); + } + const mod = moduleCache.get(golden)!; + const schema = mod[schemaName]; + if (!schema || typeof (schema as any).safeParse !== "function") { + throw new Error( + `Schema "${schemaName}" not found or not a Zod schema in ${golden}` + ); + } + return schema as { safeParse: (input: unknown) => any }; +} + +describe(`Golden file runtime tests (zod@${currentZodVersion})`, () => { + for (const tc of cases) { + const skip = shouldSkip(tc.golden); + + const testFn = skip ? it.skip : it; + + testFn(tc.name, async () => { + const schema = await getSchema(tc.golden, tc.schema); + const result = schema.safeParse(tc.input); + + if (tc.success) { + expect(result.success).toBe(true); + const expected = tc.output !== undefined ? tc.output : tc.input; + expect(result.data).toEqual(expected); + } else { + expect(result.success).toBe(false); + } + }); + } +}); diff --git a/tests/zod.test.ts b/tests/zod.test.ts deleted file mode 100644 index 437be64..0000000 --- a/tests/zod.test.ts +++ /dev/null @@ -1,292 +0,0 @@ -import {z} from "zod" -import {describe, expect, it} from 'vitest' - -describe("Zod time tests", () => { - it('TestStructTime', () => { - const UserSchema = z.object({ - Name: z.string(), - When: z.coerce.date() - }) - - const user1 = UserSchema.parse({ - Name: "John", - When: "2021-01-01T00:00:00Z", - }) - expect(user1).toEqual({ - Name: "John", - When: new Date("2021-01-01T00:00:00Z"), - }) - - const user2 = UserSchema.parse({ - Name: "John", - When: 1609459200000, - }) - expect(user2).toEqual({ - Name: "John", - When: new Date("2021-01-01T00:00:00Z"), - }) - - const user3 = UserSchema.parse({ - Name: "John", - When: null, - }) - expect(user3).toEqual({ - Name: "John", - When: new Date(0), - }) - - const user4 = UserSchema.parse({ - Name: "John", - When: "0001-01-01T00:00:00Z" - }) - expect(user4).toEqual({ - Name: "John", - When: new Date("0001-01-01T00:00:00Z"), - }) - - const user5 = UserSchema.safeParse({ - Name: "John", - When: "", - }); - expect(user5.success).toBe(false) - }) - - it('TestTimeWithRequired', () => { - const UserSchema = z.object({ - Name: z.string(), - When: z.coerce.date().refine( - (val) => val.getTime() !== new Date('0001-01-01T00:00:00Z').getTime() && val.getTime() !== new Date(0).getTime(), - 'Invalid date' - ), - }) - - const user1 = UserSchema.parse({ - Name: "John", - When: "2021-01-01T00:00:00Z", - }) - expect(user1).toEqual({ - Name: "John", - When: new Date("2021-01-01T00:00:00Z"), - }) - - const user2 = UserSchema.parse({ - Name: "John", - When: 1609459200000, - }) - expect(user2).toEqual({ - Name: "John", - When: new Date("2021-01-01T00:00:00Z"), - }) - - const user3 = UserSchema.safeParse({ - Name: "John", - When: null, - }) - expect(user3.success).toBe(false) - - const user4 = UserSchema.safeParse({ - Name: "John", - When: "0001-01-01T00:00:00Z" - }) - expect(user4.success).toBe(false) - - const user5 = UserSchema.safeParse({ - Name: "John", - When: "", - }); - expect(user5.success).toBe(false) - }) -}) - -describe("Zod test everything validations", () => { - it('TestEverything', () => { - const PostSchema = z.object({ - Title: z.string().min(1), - }) - type Post = z.infer - - const PostWithMetaDataSchema = z.object({ - Title: z.string().min(1), - Post: PostSchema, - }) - type PostWithMetaData = z.infer - - const UserSchema = z.object({ - Name: z.string().min(1), - Nickname: z.string().nullable(), - Age: z.number().gte(18).refine((val) => val !== 0), - Height: z.number().gte(1.5).refine((val) => val !== 0), - OldPostWithMetaData: PostWithMetaDataSchema, - Tags: z.string().array().nonempty().min(1), - TagsOptional: z.string().array().optional(), - TagsOptionalNullable: z.string().array().optional().nullable(), - Favourites: z.object({ - Name: z.string().min(1), - }).array().nullable(), - Posts: PostSchema.array().nonempty(), - Post: PostSchema, - PostOptional: PostSchema.optional(), - PostOptionalNullable: PostSchema.optional().nullable(), - Metadata: z.record(z.string(), z.string()).nullable(), - MetadataLength: z.record(z.string(), z.string()).refine((val) => Object.keys(val).length > 0, 'Empty map').refine((val) => Object.keys(val).length >= 1, 'Map too small').refine((val) => Object.keys(val).length <= 10, 'Map too large'), - MetadataOptional: z.record(z.string(), z.string()).optional(), - MetadataOptionalNullable: z.record(z.string(), z.string()).optional().nullable(), - ExtendedProps: z.any(), - ExtendedPropsOptional: z.any(), - ExtendedPropsNullable: z.any(), - ExtendedPropsOptionalNullable: z.any(), - ExtendedPropsVeryIndirect: z.any(), - NewPostWithMetaData: PostWithMetaDataSchema, - VeryNewPost: PostSchema, - MapWithStruct: z.record(z.string(), PostWithMetaDataSchema).nullable(), - }) - type User = z.infer - - const user1 = UserSchema.parse({ - Name: "John", - Nickname: null, - Age: 18, - Height: 1.5, - OldPostWithMetaData: { - Title: "Hello", - Post: { - Title: "World", - }, - }, - Tags: ["a", "b"], - TagsOptional: ["a", "b"], - TagsOptionalNullable: ["a", "b"], - Favourites: null, - Posts: [ - { - Title: "Hello", - }, - ], - Post: { - Title: "Hello", - }, - PostOptional: { - Title: "Hello", - }, - PostOptionalNullable: { - Title: "Hello", - }, - Metadata: null, - MetadataLength: { - "Hello": "World", - }, - MetadataOptional: undefined, - MetadataOptionalNullable: null, - ExtendedProps: null, - ExtendedPropsOptional: undefined, - ExtendedPropsNullable: null, - ExtendedPropsOptionalNullable: null, - ExtendedPropsVeryIndirect: null, - NewPostWithMetaData: { - Title: "Hello", - Post: { - Title: "World", - }, - }, - VeryNewPost: { - Title: "Hello", - }, - MapWithStruct: { - "Hello": { - Title: "World", - Post: { - Title: "Hello", - }, - }, - }, - }) - expect(user1).toEqual({ - Name: "John", - Nickname: null, - Age: 18, - Height: 1.5, - OldPostWithMetaData: { - Title: "Hello", - Post: { - Title: "World", - }, - }, - Tags: ["a", "b"], - TagsOptional: ["a", "b"], - TagsOptionalNullable: ["a", "b"], - Favourites: null, - Posts: [ - { - Title: "Hello", - }, - ], - Post: { - Title: "Hello", - }, - PostOptional: { - Title: "Hello", - }, - PostOptionalNullable: { - Title: "Hello", - }, - Metadata: null, - MetadataLength: { - "Hello": "World", - }, - MetadataOptional: undefined, - MetadataOptionalNullable: null, - ExtendedProps: null, - ExtendedPropsOptional: undefined, - ExtendedPropsNullable: null, - ExtendedPropsOptionalNullable: null, - ExtendedPropsVeryIndirect: null, - NewPostWithMetaData: { - Title: "Hello", - Post: { - Title: "World", - }, - }, - VeryNewPost: { - Title: "Hello", - }, - MapWithStruct: { - "Hello": { - Title: "World", - Post: { - Title: "Hello", - }, - }, - }, - }) - }) -}) - -describe("Zod test enum", () => { - it('TestEnum1', () => { - const EnumSchema = z.enum(["a", "b"]) - type Enum = z.infer - - const enum1 = EnumSchema.parse("a") - testStringType(enum1) - // testConstType(enum1) - // Does not work with const - }) - - it('TestEnum2', () => { - const EnumSchema = z.enum(["abc", "def"] as const) - type Enum = z.infer - - const enum1 = EnumSchema.parse("abc") - testStringType(enum1) - testConstType(enum1) - // Works with both string and const - }) -}) - -function testStringType(x: string) { - console.log(x) -} - -function testConstType(x: "abc"|"def") { - console.log(x) -} From dee7a0e6fc12985a06b1303f784c04d08558107f Mon Sep 17 00:00:00 2001 From: Ramil Amparo Date: Fri, 3 Apr 2026 13:09:19 +0400 Subject: [PATCH 09/35] Fix PR review feedback and improve code quality Security: - Add escapeJSString() to escape backslashes and double quotes in generated JS string literals (contains, startswith, endswith, eq, ne, oneof). Prevents broken output from struct tag values with special chars. Refactors: - Replace lastFieldSelfRef side-channel flag with convertResult struct that explicitly returns {text, selfRef} from convertType(). Public ConvertType API unchanged via thin wrapper. - Extract renderChain() with shared cases from renderV3Chain/renderV4Chain, eliminating ~80 lines of duplication. - Simplify parseStringValidators: replace ~90 lines of switch cases with knownStringTags map lookup. - Extract regex rendering into maps (regexChainMap, unicodeRegexChainMap, v3FormatRegexMap) with renderRegex()/renderUnicodeRegex() helpers. Bug fixes: - Fix v4 embedded field ordering: spreads now come before named fields so named fields override embedded ones (last key wins in JS), matching Go's shadowing semantics. Previously spreads came after, causing embedded fields to override named fields incorrectly. - Panic on non-integer gt=/lt= arguments instead of silently using 0. Tests: - Add field shadowing test (named field overrides embedded field, v3+v4) - Add escapeJSString unit tests - Add gt/lt non-integer panic tests Co-Authored-By: Claude Opus 4.6 (1M context) --- testdata/TestCustomTag/v4.golden | 2 +- testdata/TestNestedStruct/v4.golden | 2 +- .../v4.golden | 2 +- .../v4.golden | 2 +- .../embedded_structs_use_shape_spreads.golden | 2 +- .../v3.golden | 13 + .../v4.golden | 14 + ...preads_to_override_embedded_fields.golden} | 4 +- zod.go | 486 +++++++----------- zod_test.go | 50 +- 10 files changed, 275 insertions(+), 302 deletions(-) create mode 100644 testdata/TestZodV4Defaults/named_field_shadows_embedded_field/v3.golden create mode 100644 testdata/TestZodV4Defaults/named_field_shadows_embedded_field/v4.golden rename testdata/TestZodV4Defaults/{recursive_embedded_shapes_keep_named_fields_before_spreads.golden => recursive_embedded_shapes_keep_named_fields_after_spreads_to_override_embedded_fields.golden} (91%) diff --git a/testdata/TestCustomTag/v4.golden b/testdata/TestCustomTag/v4.golden index 8f82532..78f43fd 100644 --- a/testdata/TestCustomTag/v4.golden +++ b/testdata/TestCustomTag/v4.golden @@ -7,12 +7,12 @@ export const SortParamsSchema = z.object({ export type SortParams = z.infer export const RequestSchema = z.object({ + ...SortParamsSchema.extend({field: z.enum(['title', 'address', 'age', 'dob'])}).shape, PaginationParams: z.object({ start: z.number().gt(0).optional(), end: z.number().gt(0).optional(), }).refine((val) => !val.start || !val.end || val.start < val.end, 'Start should be less than end'), search: z.string().refine((val) => !val || /^[a-z0-9_]*$/.test(val), 'Invalid search identifier').optional(), - ...SortParamsSchema.extend({field: z.enum(['title', 'address', 'age', 'dob'])}).shape, }) export type Request = z.infer diff --git a/testdata/TestNestedStruct/v4.golden b/testdata/TestNestedStruct/v4.golden index 2e0fc02..0605c86 100644 --- a/testdata/TestNestedStruct/v4.golden +++ b/testdata/TestNestedStruct/v4.golden @@ -11,9 +11,9 @@ export const HasNameSchema = z.object({ export type HasName = z.infer export const UserSchema = z.object({ - Tags: z.string().array().nullable(), ...HasIDSchema.shape, ...HasNameSchema.shape, + Tags: z.string().array().nullable(), }) export type User = z.infer diff --git a/testdata/TestRecursiveEmbeddedWithPointersAndDates/embedded_struct_with_pointer_to_self_and_date/v4.golden b/testdata/TestRecursiveEmbeddedWithPointersAndDates/embedded_struct_with_pointer_to_self_and_date/v4.golden index d9c859f..415b304 100644 --- a/testdata/TestRecursiveEmbeddedWithPointersAndDates/embedded_struct_with_pointer_to_self_and_date/v4.golden +++ b/testdata/TestRecursiveEmbeddedWithPointersAndDates/embedded_struct_with_pointer_to_self_and_date/v4.golden @@ -13,8 +13,8 @@ const CommentSchemaShape = { export const CommentSchema: z.ZodType = z.object(CommentSchemaShape) export const ArticleSchema = z.object({ - Title: z.string(), ...CommentSchemaShape, + Title: z.string(), }) export type Article = z.infer diff --git a/testdata/TestRecursiveEmbeddedWithPointersAndDates/recursive_struct_with_pointer_field_and_date/v4.golden b/testdata/TestRecursiveEmbeddedWithPointersAndDates/recursive_struct_with_pointer_field_and_date/v4.golden index 3661084..06a670d 100644 --- a/testdata/TestRecursiveEmbeddedWithPointersAndDates/recursive_struct_with_pointer_field_and_date/v4.golden +++ b/testdata/TestRecursiveEmbeddedWithPointersAndDates/recursive_struct_with_pointer_field_and_date/v4.golden @@ -13,8 +13,8 @@ const TreeNodeSchemaShape = { export const TreeNodeSchema: z.ZodType = z.object(TreeNodeSchemaShape) export const TreeSchema = z.object({ - UpdatedAt: z.coerce.date(), ...TreeNodeSchemaShape, + UpdatedAt: z.coerce.date(), }) export type Tree = z.infer diff --git a/testdata/TestZodV4Defaults/embedded_structs_use_shape_spreads.golden b/testdata/TestZodV4Defaults/embedded_structs_use_shape_spreads.golden index 2e0fc02..0605c86 100644 --- a/testdata/TestZodV4Defaults/embedded_structs_use_shape_spreads.golden +++ b/testdata/TestZodV4Defaults/embedded_structs_use_shape_spreads.golden @@ -11,9 +11,9 @@ export const HasNameSchema = z.object({ export type HasName = z.infer export const UserSchema = z.object({ - Tags: z.string().array().nullable(), ...HasIDSchema.shape, ...HasNameSchema.shape, + Tags: z.string().array().nullable(), }) export type User = z.infer diff --git a/testdata/TestZodV4Defaults/named_field_shadows_embedded_field/v3.golden b/testdata/TestZodV4Defaults/named_field_shadows_embedded_field/v3.golden new file mode 100644 index 0000000..7a572f0 --- /dev/null +++ b/testdata/TestZodV4Defaults/named_field_shadows_embedded_field/v3.golden @@ -0,0 +1,13 @@ +// @zod-version: v3 +// @typecheck +export const BaseSchema = z.object({ + id: z.string(), + name: z.string(), +}) +export type Base = z.infer + +export const ChildSchema = z.object({ + id: z.number(), +}).merge(BaseSchema) +export type Child = z.infer + diff --git a/testdata/TestZodV4Defaults/named_field_shadows_embedded_field/v4.golden b/testdata/TestZodV4Defaults/named_field_shadows_embedded_field/v4.golden new file mode 100644 index 0000000..6ebcc95 --- /dev/null +++ b/testdata/TestZodV4Defaults/named_field_shadows_embedded_field/v4.golden @@ -0,0 +1,14 @@ +// @zod-version: v4 +// @typecheck +export const BaseSchema = z.object({ + id: z.string(), + name: z.string(), +}) +export type Base = z.infer + +export const ChildSchema = z.object({ + ...BaseSchema.shape, + id: z.number(), +}) +export type Child = z.infer + diff --git a/testdata/TestZodV4Defaults/recursive_embedded_shapes_keep_named_fields_before_spreads.golden b/testdata/TestZodV4Defaults/recursive_embedded_shapes_keep_named_fields_after_spreads_to_override_embedded_fields.golden similarity index 91% rename from testdata/TestZodV4Defaults/recursive_embedded_shapes_keep_named_fields_before_spreads.golden rename to testdata/TestZodV4Defaults/recursive_embedded_shapes_keep_named_fields_after_spreads_to_override_embedded_fields.golden index 3661084..2ea46b2 100644 --- a/testdata/TestZodV4Defaults/recursive_embedded_shapes_keep_named_fields_before_spreads.golden +++ b/testdata/TestZodV4Defaults/recursive_embedded_shapes_keep_named_fields_after_spreads_to_override_embedded_fields.golden @@ -4,17 +4,19 @@ export type TreeNode = { Value: string, CreatedAt: Date, Children: TreeNode[] | null, + UpdatedAt: string, } const TreeNodeSchemaShape = { Value: z.string(), CreatedAt: z.coerce.date(), get Children() { return TreeNodeSchema.array().nullable(); }, + UpdatedAt: z.string(), } export const TreeNodeSchema: z.ZodType = z.object(TreeNodeSchemaShape) export const TreeSchema = z.object({ - UpdatedAt: z.coerce.date(), ...TreeNodeSchemaShape, + UpdatedAt: z.coerce.date(), }) export type Tree = z.infer diff --git a/zod.go b/zod.go index 29d6c4e..b1d3c36 100644 --- a/zod.go +++ b/zod.go @@ -180,15 +180,14 @@ type stringValidator struct { } type Converter struct { - prefix string - customTypes map[string]CustomFn - customTags map[string]CustomFn - ignoreTags []string - zodV3 bool - lastFieldSelfRef bool - structs int - outputs map[string]entry - stack []meta + prefix string + customTypes map[string]CustomFn + customTags map[string]CustomFn + ignoreTags []string + zodV3 bool + structs int + outputs map[string]entry + stack []meta } func (c *Converter) addSchema(name string, data string, selfRef bool) { @@ -330,6 +329,7 @@ func (c *Converter) convertStruct(input reflect.Type, indent int) string { merges := []string{} embeddedFields := []string{} + namedFields := []string{} fields := input.NumField() for i := 0; i < fields; i++ { @@ -349,15 +349,22 @@ func (c *Converter) convertStruct(input reflect.Type, indent int) string { embeddedFields = append(embeddedFields, c.convertEmbeddedFieldSpread(field, indent+1)) } } else { - output.WriteString(c.convertNamedField(field, indent+1, optional, nullable)) + namedFields = append(namedFields, c.convertNamedField(field, indent+1, optional, nullable)) } } + // In v4, embedded spreads are written before named fields so that named + // fields override embedded ones (last key wins in JS object literals). + // This matches Go's shadowing semantics where the outer struct's field + // takes precedence over the embedded struct's field. if !c.zodV3 { for _, line := range embeddedFields { output.WriteString(line) } } + for _, line := range namedFields { + output.WriteString(line) + } output.WriteString(indentation(indent)) output.WriteString(`})`) @@ -473,19 +480,28 @@ func (c *Converter) handleCustomType(t reflect.Type, validate string, indent int return "", false } +type convertResult struct { + text string + selfRef bool +} + // ConvertType should be called from custom converter functions. func (c *Converter) ConvertType(t reflect.Type, validate string, indent int) string { + return c.convertType(t, validate, indent).text +} + +func (c *Converter) convertType(t reflect.Type, validate string, indent int) convertResult { if t.Kind() == reflect.Ptr { inner := t.Elem() validate = strings.TrimPrefix(validate, "omitempty") validate = strings.TrimPrefix(validate, ",") - return c.ConvertType(inner, validate, indent) + return c.convertType(inner, validate, indent) } // Custom types should be handled before maps/slices, as we might have // custom types that are maps/slices. if custom, ok := c.handleCustomType(t, validate, indent); ok { - return custom + return convertResult{text: custom} } if t.Kind() == reflect.Slice || t.Kind() == reflect.Array { @@ -493,12 +509,13 @@ func (c *Converter) ConvertType(t reflect.Type, validate string, indent int) str } if t.Kind() == reflect.Map { - return c.convertMap(t, validate, indent) + return convertResult{text: c.convertMap(t, validate, indent)} } if t.Kind() == reflect.Struct { var validateStr strings.Builder var refines []string + var selfRef bool name := typeName(t) parts := strings.Split(validate, ",") @@ -514,14 +531,14 @@ func (c *Converter) ConvertType(t reflect.Type, validate string, indent int) str if c.zodV3 { validateStr.WriteString(fmt.Sprintf("z.lazy(() => %s)", schemaName(c.prefix, name))) } else { - c.lastFieldSelfRef = true + selfRef = true validateStr.WriteString(schemaName(c.prefix, name)) } } else { // throws panic if there is a cycle detectCycle(name, c.stack) - data, selfRef := c.convertStructTopLevel(t, name) - c.addSchema(name, data, selfRef) + data, sRef := c.convertStructTopLevel(t, name) + c.addSchema(name, data, sRef) validateStr.WriteString(schemaName(c.prefix, name)) } } @@ -547,7 +564,8 @@ func (c *Converter) ConvertType(t reflect.Type, validate string, indent int) str validateStr.WriteString(refine) } - return validateStr.String() + schema := validateStr.String() + return convertResult{text: schema, selfRef: selfRef} } // boolean, number, string, any @@ -560,13 +578,13 @@ func (c *Converter) ConvertType(t reflect.Type, validate string, indent int) str if validate != "" { switch zodType { case "string": - return c.validateString(validate) + return convertResult{text: c.validateString(validate)} case "number": validateStr = c.validateNumber(validate) } } - return fmt.Sprintf("z.%s()%s", zodType, validateStr) + return convertResult{text: fmt.Sprintf("z.%s()%s", zodType, validateStr)} } func (c *Converter) getType(t reflect.Type, indent int) string { @@ -627,16 +645,14 @@ func (c *Converter) convertNamedField(f reflect.StructField, indent int, optiona nullableCall = ".nullable()" } - t := c.ConvertType(f.Type, f.Tag.Get("validate"), indent) - isSelfRef := c.lastFieldSelfRef - c.lastFieldSelfRef = false + res := c.convertType(f.Type, f.Tag.Get("validate"), indent) - if isSelfRef && !c.zodV3 { + if res.selfRef && !c.zodV3 { return fmt.Sprintf( "%sget %s() { return %s%s%s; },\n", indentation(indent), name, - t, + res.text, optionalCall, nullableCall) } @@ -645,13 +661,13 @@ func (c *Converter) convertNamedField(f reflect.StructField, indent int, optiona "%s%s: %s%s%s,\n", indentation(indent), name, - t, + res.text, optionalCall, nullableCall) } func (c *Converter) convertEmbeddedFieldMerge(f reflect.StructField, indent int) (string, bool) { - t := c.ConvertType(f.Type, f.Tag.Get("validate"), indent) + t := c.convertType(f.Type, f.Tag.Get("validate"), indent).text typeName := typeName(f.Type) entry, ok := c.outputs[typeName] if ok && entry.selfRef { @@ -663,7 +679,7 @@ func (c *Converter) convertEmbeddedFieldMerge(f reflect.StructField, indent int) } func (c *Converter) convertEmbeddedFieldSpread(f reflect.StructField, indent int) string { - t := c.ConvertType(f.Type, f.Tag.Get("validate"), indent) + t := c.convertType(f.Type, f.Tag.Get("validate"), indent).text typeName := typeName(f.Type) entry, ok := c.outputs[typeName] if ok && entry.selfRef { @@ -712,7 +728,7 @@ func (c *Converter) getTypeField(f reflect.StructField, indent int, optional, nu return typeName(f.Type), true } -func (c *Converter) convertSliceAndArray(t reflect.Type, validate string, indent int) string { +func (c *Converter) convertSliceAndArray(t reflect.Type, validate string, indent int) convertResult { var validateStr strings.Builder var refines []string validateCurrent := getValidateCurrent(validate) @@ -783,9 +799,11 @@ forParts: validateStr.WriteString(refine) } - return fmt.Sprintf( - "%s.array()%s", - c.ConvertType(t.Elem(), getValidateAfterDive(validate), indent), validateStr.String()) + elemResult := c.convertType(t.Elem(), getValidateAfterDive(validate), indent) + return convertResult{ + text: fmt.Sprintf("%s.array()%s", elemResult.text, validateStr.String()), + selfRef: elemResult.selfRef, + } } func (c *Converter) getTypeSliceAndArray(t reflect.Type, indent int) string { @@ -1048,6 +1066,24 @@ func (c *Converter) validateString(validate string) string { return c.renderStringSchema(validators) } +var knownStringTags = map[string]bool{ + "required": true, "email": true, "url": true, "http_url": true, + "ipv4": true, "ip4_addr": true, "ipv6": true, "ip6_addr": true, + "ip": true, "ip_addr": true, + "url_encoded": true, "alpha": true, "alphanum": true, + "alphanumunicode": true, "alphaunicode": true, "ascii": true, + "lowercase": true, "number": true, "numeric": true, "uppercase": true, + "base64": true, "mongodb": true, "datetime": true, "hexadecimal": true, + "json": true, "jwt": true, "latitude": true, "longitude": true, + "uuid": true, "uuid3": true, "uuid3_rfc4122": true, + "uuid4": true, "uuid4_rfc4122": true, + "uuid5": true, "uuid5_rfc4122": true, "uuid_rfc4122": true, + "md4": true, "md5": true, "sha256": true, "sha384": true, "sha512": true, + "contains": true, "endswith": true, "startswith": true, + "eq": true, "ne": true, "len": true, "min": true, "max": true, + "gt": true, "gte": true, "lt": true, "lte": true, +} + func (c *Converter) parseStringValidators(validate string) []stringValidator { var validators []stringValidator parts := strings.Split(validate, ",") @@ -1064,129 +1100,23 @@ func (c *Converter) parseStringValidators(validate string) []stringValidator { continue } - if valValue != "" { - switch valName { - case "oneof": - vals := splitParamsRegex.FindAllString(rawPart[6:], -1) - for i := 0; i < len(vals); i++ { - vals[i] = strings.Replace(vals[i], "'", "", -1) - } - if len(vals) == 0 { - panic("oneof= must be followed by a list of values") - } - enumText := fmt.Sprintf("z.enum([\"%s\"] as const)", strings.Join(vals, "\", \"")) - validators = append(validators, stringValidator{tag: "oneof", arg: enumText}) - case "contains": - validators = append(validators, stringValidator{tag: "contains", arg: valValue}) - case "endswith": - validators = append(validators, stringValidator{tag: "endswith", arg: valValue}) - case "startswith": - validators = append(validators, stringValidator{tag: "startswith", arg: valValue}) - case "eq": - validators = append(validators, stringValidator{tag: "eq", arg: valValue}) - case "ne": - validators = append(validators, stringValidator{tag: "ne", arg: valValue}) - case "len": - validators = append(validators, stringValidator{tag: "len", arg: valValue}) - case "min": - validators = append(validators, stringValidator{tag: "min", arg: valValue}) - case "max": - validators = append(validators, stringValidator{tag: "max", arg: valValue}) - case "gt": - validators = append(validators, stringValidator{tag: "gt", arg: valValue}) - case "gte": - validators = append(validators, stringValidator{tag: "gte", arg: valValue}) - case "lt": - validators = append(validators, stringValidator{tag: "lt", arg: valValue}) - case "lte": - validators = append(validators, stringValidator{tag: "lte", arg: valValue}) - default: - panic(fmt.Sprintf("unknown validation: %s", rawPart)) - } - continue - } - - switch valName { - case "omitempty": + switch { + case valName == "omitempty": // skip - case "required": - validators = append(validators, stringValidator{tag: "required"}) - case "email": - validators = append(validators, stringValidator{tag: "email"}) - case "url": - validators = append(validators, stringValidator{tag: "url"}) - case "ipv4", "ip4_addr": - validators = append(validators, stringValidator{tag: valName}) - case "ipv6", "ip6_addr": - validators = append(validators, stringValidator{tag: valName}) - case "ip", "ip_addr": - validators = append(validators, stringValidator{tag: valName}) - case "http_url": - validators = append(validators, stringValidator{tag: "http_url"}) - case "url_encoded": - validators = append(validators, stringValidator{tag: "url_encoded"}) - case "alpha": - validators = append(validators, stringValidator{tag: "alpha"}) - case "alphanum": - validators = append(validators, stringValidator{tag: "alphanum"}) - case "alphanumunicode": - validators = append(validators, stringValidator{tag: "alphanumunicode"}) - case "alphaunicode": - validators = append(validators, stringValidator{tag: "alphaunicode"}) - case "ascii": - validators = append(validators, stringValidator{tag: "ascii"}) - case "boolean": + case valName == "oneof" && valValue != "": + vals := splitParamsRegex.FindAllString(rawPart[6:], -1) + for i := 0; i < len(vals); i++ { + vals[i] = escapeJSString(strings.Replace(vals[i], "'", "", -1)) + } + if len(vals) == 0 { + panic("oneof= must be followed by a list of values") + } + enumText := fmt.Sprintf("z.enum([\"%s\"] as const)", strings.Join(vals, "\", \"")) + validators = append(validators, stringValidator{tag: "oneof", arg: enumText}) + case valName == "boolean": validators = append(validators, stringValidator{tag: "boolean", arg: "z.enum(['true', 'false'])"}) - case "lowercase": - validators = append(validators, stringValidator{tag: "lowercase"}) - case "number": - validators = append(validators, stringValidator{tag: "number"}) - case "numeric": - validators = append(validators, stringValidator{tag: "numeric"}) - case "uppercase": - validators = append(validators, stringValidator{tag: "uppercase"}) - case "base64": - validators = append(validators, stringValidator{tag: "base64"}) - case "mongodb": - validators = append(validators, stringValidator{tag: "mongodb"}) - case "datetime": - validators = append(validators, stringValidator{tag: "datetime"}) - case "hexadecimal": - validators = append(validators, stringValidator{tag: "hexadecimal"}) - case "json": - validators = append(validators, stringValidator{tag: "json"}) - case "jwt": - validators = append(validators, stringValidator{tag: "jwt"}) - case "latitude": - validators = append(validators, stringValidator{tag: "latitude"}) - case "longitude": - validators = append(validators, stringValidator{tag: "longitude"}) - case "uuid": - validators = append(validators, stringValidator{tag: "uuid"}) - case "uuid3": - validators = append(validators, stringValidator{tag: "uuid3"}) - case "uuid3_rfc4122": - validators = append(validators, stringValidator{tag: "uuid3_rfc4122"}) - case "uuid4": - validators = append(validators, stringValidator{tag: "uuid4"}) - case "uuid4_rfc4122": - validators = append(validators, stringValidator{tag: "uuid4_rfc4122"}) - case "uuid5": - validators = append(validators, stringValidator{tag: "uuid5"}) - case "uuid5_rfc4122": - validators = append(validators, stringValidator{tag: "uuid5_rfc4122"}) - case "uuid_rfc4122": - validators = append(validators, stringValidator{tag: "uuid_rfc4122"}) - case "md4": - validators = append(validators, stringValidator{tag: "md4"}) - case "md5": - validators = append(validators, stringValidator{tag: "md5"}) - case "sha256": - validators = append(validators, stringValidator{tag: "sha256"}) - case "sha384": - validators = append(validators, stringValidator{tag: "sha384"}) - case "sha512": - validators = append(validators, stringValidator{tag: "sha512"}) + case knownStringTags[valName]: + validators = append(validators, stringValidator{tag: valName, arg: valValue}) default: panic(fmt.Sprintf("unknown validation: %s", rawPart)) } @@ -1337,88 +1267,67 @@ func (c *Converter) renderStringSchema(validators []stringValidator) string { return "z.string()" + chain } -func (c *Converter) renderV3Chain(v stringValidator) string { +// escapeJSString escapes backslashes and double quotes in a string so it can +// be safely interpolated into a JavaScript double-quoted string literal. +// Without this, struct tag values like `contains=foo"bar` would produce broken +// or injectable JS output such as `.includes("foo"bar")`. +func escapeJSString(s string) string { + s = strings.ReplaceAll(s, `\`, `\\`) + s = strings.ReplaceAll(s, `"`, `\"`) + return s +} + +// regexChainMap maps validator tags to their regex pattern strings. +// Used by renderChain and renderV3Chain to generate .regex() calls. +var regexChainMap = map[string]string{ + "url_encoded": uRLEncodedRegexString, + "alpha": alphaRegexString, + "alphanum": alphaNumericRegexString, + "ascii": aSCIIRegexString, + "number": numberRegexString, + "numeric": numericRegexString, + "mongodb": mongodbRegexString, + "latitude": latitudeRegexString, + "longitude": longitudeRegexString, + "md4": md4RegexString, +} + +func renderRegex(pattern string) string { + return fmt.Sprintf(".regex(/%s/)", pattern) +} + +// unicodeRegexChainMap is like regexChainMap but for patterns needing the /u flag. +var unicodeRegexChainMap = map[string]string{ + "alphanumunicode": alphaUnicodeNumericRegexString, + "alphaunicode": alphaUnicodeRegexString, +} + +func renderUnicodeRegex(pattern string) string { + return fmt.Sprintf(".regex(/%s/u)", pattern) +} + +func renderChain(v stringValidator) string { + // Regex-based validators + if pattern, ok := regexChainMap[v.tag]; ok { + return renderRegex(pattern) + } + if pattern, ok := unicodeRegexChainMap[v.tag]; ok { + return renderUnicodeRegex(pattern) + } + switch v.tag { case "required": return ".min(1)" - case "email": - return ".email()" - case "url": - return ".url()" - case "ip", "ip_addr": - return ".ip()" - case "ipv4", "ip4_addr": - return `.ip({ version: "v4" })` - case "ipv6", "ip6_addr": - return `.ip({ version: "v6" })` - case "http_url": - return ".url()" - case "base64": - return fmt.Sprintf(".regex(/%s/)", base64RegexString) - case "datetime": - return ".datetime()" - case "hexadecimal": - return fmt.Sprintf(".regex(/%s/)", hexadecimalRegexString) - case "jwt": - return fmt.Sprintf(".regex(/%s/)", jWTRegexString) - case "uuid": - return fmt.Sprintf(".regex(/%s/)", uUIDRegexString) - case "uuid3": - return fmt.Sprintf(".regex(/%s/)", uUID3RegexString) - case "uuid3_rfc4122": - return fmt.Sprintf(".regex(/%s/)", uUID3RFC4122RegexString) - case "uuid4": - return fmt.Sprintf(".regex(/%s/)", uUID4RegexString) - case "uuid4_rfc4122": - return fmt.Sprintf(".regex(/%s/)", uUID4RFC4122RegexString) - case "uuid5": - return fmt.Sprintf(".regex(/%s/)", uUID5RegexString) - case "uuid5_rfc4122": - return fmt.Sprintf(".regex(/%s/)", uUID5RFC4122RegexString) - case "uuid_rfc4122": - return fmt.Sprintf(".regex(/%s/)", uUIDRFC4122RegexString) - case "md4": - return fmt.Sprintf(".regex(/%s/)", md4RegexString) - case "md5": - return fmt.Sprintf(".regex(/%s/)", md5RegexString) - case "sha256": - return fmt.Sprintf(".regex(/%s/)", sha256RegexString) - case "sha384": - return fmt.Sprintf(".regex(/%s/)", sha384RegexString) - case "sha512": - return fmt.Sprintf(".regex(/%s/)", sha512RegexString) case "contains": - return fmt.Sprintf(`.includes("%s")`, v.arg) + return fmt.Sprintf(`.includes("%s")`, escapeJSString(v.arg)) case "startswith": - return fmt.Sprintf(`.startsWith("%s")`, v.arg) + return fmt.Sprintf(`.startsWith("%s")`, escapeJSString(v.arg)) case "endswith": - return fmt.Sprintf(`.endsWith("%s")`, v.arg) - case "url_encoded": - return fmt.Sprintf(".regex(/%s/)", uRLEncodedRegexString) - case "alpha": - return fmt.Sprintf(".regex(/%s/)", alphaRegexString) - case "alphanum": - return fmt.Sprintf(".regex(/%s/)", alphaNumericRegexString) - case "alphanumunicode": - return fmt.Sprintf(".regex(/%s/u)", alphaUnicodeNumericRegexString) - case "alphaunicode": - return fmt.Sprintf(".regex(/%s/u)", alphaUnicodeRegexString) - case "ascii": - return fmt.Sprintf(".regex(/%s/)", aSCIIRegexString) - case "number": - return fmt.Sprintf(".regex(/%s/)", numberRegexString) - case "numeric": - return fmt.Sprintf(".regex(/%s/)", numericRegexString) - case "mongodb": - return fmt.Sprintf(".regex(/%s/)", mongodbRegexString) - case "latitude": - return fmt.Sprintf(".regex(/%s/)", latitudeRegexString) - case "longitude": - return fmt.Sprintf(".regex(/%s/)", longitudeRegexString) + return fmt.Sprintf(`.endsWith("%s")`, escapeJSString(v.arg)) case "eq": - return fmt.Sprintf(`.refine((val) => val === "%s")`, v.arg) + return fmt.Sprintf(`.refine((val) => val === "%s")`, escapeJSString(v.arg)) case "ne": - return fmt.Sprintf(`.refine((val) => val !== "%s")`, v.arg) + return fmt.Sprintf(`.refine((val) => val !== "%s")`, escapeJSString(v.arg)) case "len": return fmt.Sprintf(".refine((val) => [...val].length === %s, 'String must contain %s character(s)')", v.arg, v.arg) case "min": @@ -1426,12 +1335,18 @@ func (c *Converter) renderV3Chain(v stringValidator) string { case "max": return fmt.Sprintf(".refine((val) => [...val].length <= %s, 'String must contain at most %s character(s)')", v.arg, v.arg) case "gt": - val, _ := strconv.Atoi(v.arg) + val, err := strconv.Atoi(v.arg) + if err != nil { + panic(fmt.Sprintf("gt= requires an integer argument, got: %s", v.arg)) + } return fmt.Sprintf(".refine((val) => [...val].length > %d, 'String must contain at least %d character(s)')", val, val+1) case "gte": return fmt.Sprintf(".refine((val) => [...val].length >= %s, 'String must contain at least %s character(s)')", v.arg, v.arg) case "lt": - val, _ := strconv.Atoi(v.arg) + val, err := strconv.Atoi(v.arg) + if err != nil { + panic(fmt.Sprintf("lt= requires an integer argument, got: %s", v.arg)) + } return fmt.Sprintf(".refine((val) => [...val].length < %d, 'String must contain at most %d character(s)')", val, val-1) case "lte": return fmt.Sprintf(".refine((val) => [...val].length <= %s, 'String must contain at most %s character(s)')", v.arg, v.arg) @@ -1448,6 +1363,56 @@ func (c *Converter) renderV3Chain(v stringValidator) string { } } +// v3FormatRegexMap maps format validator tags to their v3 regex pattern strings. +// These tags have v4 top-level builders but fall back to regex in v3. +var v3FormatRegexMap = map[string]string{ + "base64": base64RegexString, + "hexadecimal": hexadecimalRegexString, + "jwt": jWTRegexString, + "uuid": uUIDRegexString, + "uuid3": uUID3RegexString, + "uuid3_rfc4122": uUID3RFC4122RegexString, + "uuid4": uUID4RegexString, + "uuid4_rfc4122": uUID4RFC4122RegexString, + "uuid5": uUID5RegexString, + "uuid5_rfc4122": uUID5RFC4122RegexString, + "uuid_rfc4122": uUIDRFC4122RegexString, + "md5": md5RegexString, + "sha256": sha256RegexString, + "sha384": sha384RegexString, + "sha512": sha512RegexString, +} + +func (c *Converter) renderV3Chain(v stringValidator) string { + if s := renderChain(v); s != "" { + return s + } + + // v3 format regex fallbacks (these have v4 top-level builders but use regex in v3) + if pattern, ok := v3FormatRegexMap[v.tag]; ok { + return renderRegex(pattern) + } + + switch v.tag { + case "email": + return ".email()" + case "url": + return ".url()" + case "ip", "ip_addr": + return ".ip()" + case "ipv4", "ip4_addr": + return `.ip({ version: "v4" })` + case "ipv6", "ip6_addr": + return `.ip({ version: "v6" })` + case "http_url": + return ".url()" + case "datetime": + return ".datetime()" + default: + return "" + } +} + func (c *Converter) renderV4FormatBase(v stringValidator) string { switch v.tag { case "email": @@ -1492,70 +1457,7 @@ func (c *Converter) renderV4FormatBase(v stringValidator) string { } func (c *Converter) renderV4Chain(v stringValidator) string { - switch v.tag { - case "required": - return ".min(1)" - case "contains": - return fmt.Sprintf(`.includes("%s")`, v.arg) - case "startswith": - return fmt.Sprintf(`.startsWith("%s")`, v.arg) - case "endswith": - return fmt.Sprintf(`.endsWith("%s")`, v.arg) - case "url_encoded": - return fmt.Sprintf(".regex(/%s/)", uRLEncodedRegexString) - case "alpha": - return fmt.Sprintf(".regex(/%s/)", alphaRegexString) - case "alphanum": - return fmt.Sprintf(".regex(/%s/)", alphaNumericRegexString) - case "alphanumunicode": - return fmt.Sprintf(".regex(/%s/u)", alphaUnicodeNumericRegexString) - case "alphaunicode": - return fmt.Sprintf(".regex(/%s/u)", alphaUnicodeRegexString) - case "ascii": - return fmt.Sprintf(".regex(/%s/)", aSCIIRegexString) - case "number": - return fmt.Sprintf(".regex(/%s/)", numberRegexString) - case "numeric": - return fmt.Sprintf(".regex(/%s/)", numericRegexString) - case "mongodb": - return fmt.Sprintf(".regex(/%s/)", mongodbRegexString) - case "latitude": - return fmt.Sprintf(".regex(/%s/)", latitudeRegexString) - case "longitude": - return fmt.Sprintf(".regex(/%s/)", longitudeRegexString) - case "md4": - return fmt.Sprintf(".regex(/%s/)", md4RegexString) - case "eq": - return fmt.Sprintf(`.refine((val) => val === "%s")`, v.arg) - case "ne": - return fmt.Sprintf(`.refine((val) => val !== "%s")`, v.arg) - case "len": - return fmt.Sprintf(".refine((val) => [...val].length === %s, 'String must contain %s character(s)')", v.arg, v.arg) - case "min": - return fmt.Sprintf(".refine((val) => [...val].length >= %s, 'String must contain at least %s character(s)')", v.arg, v.arg) - case "max": - return fmt.Sprintf(".refine((val) => [...val].length <= %s, 'String must contain at most %s character(s)')", v.arg, v.arg) - case "gt": - val, _ := strconv.Atoi(v.arg) - return fmt.Sprintf(".refine((val) => [...val].length > %d, 'String must contain at least %d character(s)')", val, val+1) - case "gte": - return fmt.Sprintf(".refine((val) => [...val].length >= %s, 'String must contain at least %s character(s)')", v.arg, v.arg) - case "lt": - val, _ := strconv.Atoi(v.arg) - return fmt.Sprintf(".refine((val) => [...val].length < %d, 'String must contain at most %d character(s)')", val, val-1) - case "lte": - return fmt.Sprintf(".refine((val) => [...val].length <= %s, 'String must contain at most %s character(s)')", v.arg, v.arg) - case "lowercase": - return ".refine((val) => val === val.toLowerCase())" - case "uppercase": - return ".refine((val) => val === val.toUpperCase())" - case "json": - return ".refine((val) => { try { JSON.parse(val); return true } catch { return false } })" - case "_custom": - return v.arg - default: - return "" - } + return renderChain(v) } func isPartialRecordKeySchema(schema string) bool { diff --git a/zod_test.go b/zod_test.go index bf24a53..429354e 100644 --- a/zod_test.go +++ b/zod_test.go @@ -411,6 +411,27 @@ func TestStringValidations(t *testing.T) { } assert.Panics(t, func() { StructToZodSchema(Bad2{}) }) }) + + t.Run("gt with non-integer panics", func(t *testing.T) { + type Bad struct { + Name string `validate:"gt=abc"` + } + assert.Panics(t, func() { StructToZodSchema(Bad{}) }) + }) + + t.Run("lt with non-integer panics", func(t *testing.T) { + type Bad struct { + Name string `validate:"lt=abc"` + } + assert.Panics(t, func() { StructToZodSchema(Bad{}) }) + }) + + t.Run("escapeJSString escapes quotes and backslashes", func(t *testing.T) { + assert.Equal(t, `foo\"bar`, escapeJSString(`foo"bar`)) + assert.Equal(t, `foo\\bar`, escapeJSString(`foo\bar`)) + assert.Equal(t, `a\"b\\c`, escapeJSString(`a"b\c`)) + assert.Equal(t, `no change`, escapeJSString(`no change`)) + }) } func TestZodV4Defaults(t *testing.T) { @@ -505,6 +526,20 @@ func TestZodV4Defaults(t *testing.T) { assertSchema(t, Payload{}, "v4") }) + t.Run("named field shadows embedded field", func(t *testing.T) { + type Base struct { + ID string `json:"id"` + Name string `json:"name"` + } + + type Child struct { + Base + ID int `json:"id"` // shadows Base.ID, keeps Base.Name + } + + assertSchema(t, Child{}, "v3", "v4") + }) + t.Run("recursive embedded shapes preserve encounter order for duplicate keys", func(t *testing.T) { type Base struct { ID string `json:"id"` @@ -519,11 +554,12 @@ func TestZodV4Defaults(t *testing.T) { goldenAssert(t, []byte(StructToZodSchema(Node{})), withGoldenZodVersion("v4")) }) - t.Run("recursive embedded shapes keep named fields before spreads", func(t *testing.T) { + t.Run("recursive embedded shapes keep named fields after spreads to override embedded fields", func(t *testing.T) { type TreeNode struct { Value string CreatedAt time.Time Children *[]TreeNode + UpdatedAt string } type Tree struct { @@ -547,7 +583,9 @@ func TestNumberValidations(t *testing.T) { }) t.Run("bad tag panics", func(t *testing.T) { - type Bad struct{ Age int `validate:"bad=18"` } + type Bad struct { + Age int `validate:"bad=18"` + } assert.Panics(t, func() { StructToZodSchema(Bad{}) }) }) } @@ -634,7 +672,9 @@ func TestMapWithValidations(t *testing.T) { }) t.Run("bad tag panics", func(t *testing.T) { - type Bad struct{ Map map[string]string `validate:"bad=1"` } + type Bad struct { + Map map[string]string `validate:"bad=1"` + } assert.Panics(t, func() { StructToZodSchema(Bad{}) }) }) } @@ -911,7 +951,9 @@ func TestConvertSliceWithValidations(t *testing.T) { t.Run("oneof without dive panics", func(t *testing.T) { assert.Panics(t, func() { - type Bad struct{ Slice []string `validate:"oneof=a b c"` } + type Bad struct { + Slice []string `validate:"oneof=a b c"` + } StructToZodSchema(Bad{}) }) }) From 843a69a6c71ff32a856742f356c0b17192fe8e78 Mon Sep 17 00:00:00 2001 From: Ramil Amparo Date: Fri, 3 Apr 2026 13:18:37 +0400 Subject: [PATCH 10/35] Add escaped characters test --- ...in_tag_values_are_escaped_in_output.golden | 11 ++++++++++ zod_test.go | 20 +++++++++++++++++++ 2 files changed, 31 insertions(+) create mode 100644 testdata/TestStringValidations/special_chars_in_tag_values_are_escaped_in_output.golden diff --git a/testdata/TestStringValidations/special_chars_in_tag_values_are_escaped_in_output.golden b/testdata/TestStringValidations/special_chars_in_tag_values_are_escaped_in_output.golden new file mode 100644 index 0000000..6cc3d8b --- /dev/null +++ b/testdata/TestStringValidations/special_chars_in_tag_values_are_escaped_in_output.golden @@ -0,0 +1,11 @@ +// @typecheck +export const ContainsQuoteSchema = z.object({ + value: z.string().includes("foo\"bar"), +}) +export type ContainsQuote = z.infer + +export const EqBackslashSchema = z.object({ + value: z.string().refine((val) => val === "a\\b"), +}) +export type EqBackslash = z.infer + diff --git a/zod_test.go b/zod_test.go index 429354e..b0041cd 100644 --- a/zod_test.go +++ b/zod_test.go @@ -432,6 +432,26 @@ func TestStringValidations(t *testing.T) { assert.Equal(t, `a\"b\\c`, escapeJSString(`a"b\c`)) assert.Equal(t, `no change`, escapeJSString(`no change`)) }) + + t.Run("special chars in tag values are escaped in output", func(t *testing.T) { + // Go struct tag syntax can't contain raw quotes, but reflect.StructOf can. + // This tests that the generated JS output correctly escapes them. + c := NewConverterWithOpts() + + contains := reflect.StructOf([]reflect.StructField{{ + Name: "Value", Type: reflect.TypeOf(""), + Tag: reflect.StructTag(`validate:"contains=foo\"bar" json:"value"`), + }}) + c.AddTypeWithName(reflect.New(contains).Elem().Interface(), "ContainsQuote") + + eq := reflect.StructOf([]reflect.StructField{{ + Name: "Value", Type: reflect.TypeOf(""), + Tag: reflect.StructTag(`validate:"eq=a\\b" json:"value"`), + }}) + c.AddTypeWithName(reflect.New(eq).Elem().Interface(), "EqBackslash") + + goldenAssert(t, []byte(c.Export())) + }) } func TestZodV4Defaults(t *testing.T) { From b439c9b51eda3f111fbdec721d8467ccccf594aa Mon Sep 17 00:00:00 2001 From: Ramil Amparo Date: Fri, 3 Apr 2026 13:34:28 +0400 Subject: [PATCH 11/35] Address second round of PR review feedback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Use version-aware dispatch in enum branch (renderV3Chain for v3, renderChain for v4) instead of always calling renderV3Chain. Defensive change — no current behavioral difference. - Replace magic number rawPart[6:] with rawPart[len("oneof="):] - Use strings.ReplaceAll instead of strings.Replace with count -1 - Panic in renderV4FormatBase default case instead of returning "" to catch future omissions when adding format tags - Validate numeric arguments for len/min/max/gte/lte string validators using requireIntArg helper (same treatment as gt/lt) - Remove renderV4Chain wrapper — inline renderChain at all call sites Co-Authored-By: Claude Opus 4.6 (1M context) --- zod.go | 51 +++++++++++++++++++++++++++++++-------------------- 1 file changed, 31 insertions(+), 20 deletions(-) diff --git a/zod.go b/zod.go index b1d3c36..5dc4b32 100644 --- a/zod.go +++ b/zod.go @@ -1104,9 +1104,9 @@ func (c *Converter) parseStringValidators(validate string) []stringValidator { case valName == "omitempty": // skip case valName == "oneof" && valValue != "": - vals := splitParamsRegex.FindAllString(rawPart[6:], -1) + vals := splitParamsRegex.FindAllString(rawPart[len("oneof="):], -1) for i := 0; i < len(vals); i++ { - vals[i] = escapeJSString(strings.Replace(vals[i], "'", "", -1)) + vals[i] = escapeJSString(strings.ReplaceAll(vals[i], "'", "")) } if len(vals) == 0 { panic("oneof= must be followed by a list of values") @@ -1175,7 +1175,12 @@ func (c *Converter) renderStringSchema(validators []stringValidator) string { if v.tag == "oneof" || v.tag == "boolean" { continue } - rendered := c.renderV3Chain(v) + var rendered string + if c.zodV3 { + rendered = c.renderV3Chain(v) + } else { + rendered = renderChain(v) + } if strings.HasPrefix(rendered, ".refine") { chain += rendered } @@ -1207,7 +1212,7 @@ func (c *Converter) renderStringSchema(validators []stringValidator) string { if v.tag == "required" || unionTags[v.tag] { continue } - armChain += c.renderV4Chain(v) + armChain += renderChain(v) } return fmt.Sprintf("z.union([z.ipv4()%s, z.ipv6()%s])", armChain, armChain) } @@ -1237,7 +1242,7 @@ func (c *Converter) renderStringSchema(validators []stringValidator) string { if formatTags[v.tag] { chain += c.renderV3Chain(v) } else { - chain += c.renderV4Chain(v) + chain += renderChain(v) } } return "z.string()" + chain @@ -1251,7 +1256,7 @@ func (c *Converter) renderStringSchema(validators []stringValidator) string { if v.tag == "required" && !keepRequired { continue } - chain += c.renderV4Chain(v) + chain += renderChain(v) } if keepRequired { chain = ".min(1)" + chain @@ -1262,7 +1267,7 @@ func (c *Converter) renderStringSchema(validators []stringValidator) string { // Case 3: No format/union — plain string chain := "" for _, v := range validators { - chain += c.renderV4Chain(v) + chain += renderChain(v) } return "z.string()" + chain } @@ -1271,6 +1276,16 @@ func (c *Converter) renderStringSchema(validators []stringValidator) string { // be safely interpolated into a JavaScript double-quoted string literal. // Without this, struct tag values like `contains=foo"bar` would produce broken // or injectable JS output such as `.includes("foo"bar")`. +// requireIntArg validates that arg is a valid integer for the given tag name. +// Returns the parsed value. Panics if arg is not a valid integer. +func requireIntArg(tag, arg string) int { + val, err := strconv.Atoi(arg) + if err != nil { + panic(fmt.Sprintf("%s= requires an integer argument, got: %s", tag, arg)) + } + return val +} + func escapeJSString(s string) string { s = strings.ReplaceAll(s, `\`, `\\`) s = strings.ReplaceAll(s, `"`, `\"`) @@ -1306,6 +1321,7 @@ func renderUnicodeRegex(pattern string) string { return fmt.Sprintf(".regex(/%s/u)", pattern) } +// renderChain is used by both v3 and v4 rendering func renderChain(v stringValidator) string { // Regex-based validators if pattern, ok := regexChainMap[v.tag]; ok { @@ -1329,26 +1345,25 @@ func renderChain(v stringValidator) string { case "ne": return fmt.Sprintf(`.refine((val) => val !== "%s")`, escapeJSString(v.arg)) case "len": + requireIntArg("len", v.arg) return fmt.Sprintf(".refine((val) => [...val].length === %s, 'String must contain %s character(s)')", v.arg, v.arg) case "min": + requireIntArg("min", v.arg) return fmt.Sprintf(".refine((val) => [...val].length >= %s, 'String must contain at least %s character(s)')", v.arg, v.arg) case "max": + requireIntArg("max", v.arg) return fmt.Sprintf(".refine((val) => [...val].length <= %s, 'String must contain at most %s character(s)')", v.arg, v.arg) case "gt": - val, err := strconv.Atoi(v.arg) - if err != nil { - panic(fmt.Sprintf("gt= requires an integer argument, got: %s", v.arg)) - } + val := requireIntArg("gt", v.arg) return fmt.Sprintf(".refine((val) => [...val].length > %d, 'String must contain at least %d character(s)')", val, val+1) case "gte": + requireIntArg("gte", v.arg) return fmt.Sprintf(".refine((val) => [...val].length >= %s, 'String must contain at least %s character(s)')", v.arg, v.arg) case "lt": - val, err := strconv.Atoi(v.arg) - if err != nil { - panic(fmt.Sprintf("lt= requires an integer argument, got: %s", v.arg)) - } + val := requireIntArg("lt", v.arg) return fmt.Sprintf(".refine((val) => [...val].length < %d, 'String must contain at most %d character(s)')", val, val-1) case "lte": + requireIntArg("lte", v.arg) return fmt.Sprintf(".refine((val) => [...val].length <= %s, 'String must contain at most %s character(s)')", v.arg, v.arg) case "lowercase": return ".refine((val) => val === val.toLowerCase())" @@ -1452,14 +1467,10 @@ func (c *Converter) renderV4FormatBase(v stringValidator) string { case "sha512": return `z.hash("sha512")` default: - return "" + panic(fmt.Sprintf("renderV4FormatBase: unhandled format tag %q", v.tag)) } } -func (c *Converter) renderV4Chain(v stringValidator) string { - return renderChain(v) -} - func isPartialRecordKeySchema(schema string) bool { schema = strings.TrimSpace(schema) return strings.HasPrefix(schema, "z.enum(") || strings.HasPrefix(schema, "z.literal(") From dd8fdf78e44dff9daf7da468bea5eefddfc5a382 Mon Sep 17 00:00:00 2001 From: Ramil Amparo Date: Fri, 3 Apr 2026 13:49:02 +0400 Subject: [PATCH 12/35] Remove dead oneof guard and fix doc comment spacing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove unreachable len(vals) == 0 panic in oneof handler — the case guard already requires valValue != "" and FindAllString always matches - Separate escapeJSString and requireIntArg doc comments with blank line Co-Authored-By: Claude Opus 4.6 (1M context) --- zod.go | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/zod.go b/zod.go index 5dc4b32..b195b78 100644 --- a/zod.go +++ b/zod.go @@ -1108,9 +1108,6 @@ func (c *Converter) parseStringValidators(validate string) []stringValidator { for i := 0; i < len(vals); i++ { vals[i] = escapeJSString(strings.ReplaceAll(vals[i], "'", "")) } - if len(vals) == 0 { - panic("oneof= must be followed by a list of values") - } enumText := fmt.Sprintf("z.enum([\"%s\"] as const)", strings.Join(vals, "\", \"")) validators = append(validators, stringValidator{tag: "oneof", arg: enumText}) case valName == "boolean": @@ -1276,6 +1273,12 @@ func (c *Converter) renderStringSchema(validators []stringValidator) string { // be safely interpolated into a JavaScript double-quoted string literal. // Without this, struct tag values like `contains=foo"bar` would produce broken // or injectable JS output such as `.includes("foo"bar")`. +func escapeJSString(s string) string { + s = strings.ReplaceAll(s, `\`, `\\`) + s = strings.ReplaceAll(s, `"`, `\"`) + return s +} + // requireIntArg validates that arg is a valid integer for the given tag name. // Returns the parsed value. Panics if arg is not a valid integer. func requireIntArg(tag, arg string) int { @@ -1286,12 +1289,6 @@ func requireIntArg(tag, arg string) int { return val } -func escapeJSString(s string) string { - s = strings.ReplaceAll(s, `\`, `\\`) - s = strings.ReplaceAll(s, `"`, `\"`) - return s -} - // regexChainMap maps validator tags to their regex pattern strings. // Used by renderChain and renderV3Chain to generate .regex() calls. var regexChainMap = map[string]string{ From cc02003aeb0aa980f4a88e9ea1d74a925a18b7e2 Mon Sep 17 00:00:00 2001 From: Ramil Amparo Date: Fri, 3 Apr 2026 13:53:41 +0400 Subject: [PATCH 13/35] Add partialRecords test --- tests/cases.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/cases.ts b/tests/cases.ts index 2eae563..461016c 100644 --- a/tests/cases.ts +++ b/tests/cases.ts @@ -1548,6 +1548,13 @@ export const cases: TestCase[] = [ input: { Metadata: { draft: "some note" } }, success: true, }, + { + name: "v4 defaults: enum keyed maps for partial records reject invalid keys", + golden: "TestZodV4Defaults/enum_keyed_maps_become_partial_records.golden", + schema: "PayloadSchema", + input: { Metadata: { invalid: "some note" } }, + success: false, + }, // --- TestZodV4Defaults/ip_unions_inherit_generic_string_constraints --- { From 3c547b5b7712bb0c7ef62db1c4cbfb27e2fa59b5 Mon Sep 17 00:00:00 2001 From: Ramil Amparo Date: Fri, 3 Apr 2026 14:02:44 +0400 Subject: [PATCH 14/35] Address third round of PR review feedback - Use json.Marshal in escapeJSString for complete JS string escaping (newlines, control characters, not just quotes and backslashes) - Pass reflect.TypeOf("") instead of reflect.TypeOf(0) to custom tag handlers in string validation context - Replace string concatenation with strings.Builder in renderStringSchema Co-Authored-By: Claude Opus 4.6 (1M context) --- zod.go | 64 ++++++++++++++++++++++++++++++---------------------------- 1 file changed, 33 insertions(+), 31 deletions(-) diff --git a/zod.go b/zod.go index b195b78..0655dd3 100644 --- a/zod.go +++ b/zod.go @@ -1,6 +1,7 @@ package zen import ( + "encoding/json" "fmt" "reflect" "regexp" @@ -1095,7 +1096,7 @@ func (c *Converter) parseStringValidators(validate string) []stringValidator { } if h, ok := c.customTags[valName]; ok { - v := h(c, reflect.TypeOf(0), valValue, 0) + v := h(c, reflect.TypeOf(""), valValue, 0) validators = append(validators, stringValidator{tag: "_custom", arg: v}) continue } @@ -1161,7 +1162,7 @@ func (c *Converter) renderStringSchema(validators []stringValidator) string { // Phase 3: Handle enum — return early if hasEnum { base := "" - chain := "" + var chain strings.Builder for _, v := range validators { if v.tag == "oneof" || v.tag == "boolean" { base = v.arg @@ -1179,10 +1180,10 @@ func (c *Converter) renderStringSchema(validators []stringValidator) string { rendered = renderChain(v) } if strings.HasPrefix(rendered, ".refine") { - chain += rendered + chain.WriteString(rendered) } } - return base + chain + return base + chain.String() } // Phase 4: Render v3 @@ -1190,28 +1191,29 @@ func (c *Converter) renderStringSchema(validators []stringValidator) string { // Skip required when a format or union is present — format validators // already reject empty strings in both v3 and v4. skipRequired := hasFormat || hasUnion - chain := "" + var chain strings.Builder for _, v := range validators { if v.tag == "required" && skipRequired { continue } - chain += c.renderV3Chain(v) + chain.WriteString(c.renderV3Chain(v)) } - return "z.string()" + chain + return "z.string()" + chain.String() } // Phase 5: Render v4 // Case 1: Union (ip/ip_addr) if hasUnion { - armChain := "" + var armChain strings.Builder for _, v := range validators { if v.tag == "required" || unionTags[v.tag] { continue } - armChain += renderChain(v) + armChain.WriteString(renderChain(v)) } - return fmt.Sprintf("z.union([z.ipv4()%s, z.ipv6()%s])", armChain, armChain) + ac := armChain.String() + return fmt.Sprintf("z.union([z.ipv4()%s, z.ipv6()%s])", ac, ac) } // Case 2: Format present @@ -1231,52 +1233,52 @@ func (c *Converter) renderStringSchema(validators []stringValidator) string { if hasTransformBefore { // Fall back to z.string() + chains (format becomes a chain method via v3 form) - chain := "" + var chain strings.Builder for _, v := range validators { if v.tag == "required" && !keepRequired { continue } if formatTags[v.tag] { - chain += c.renderV3Chain(v) + chain.WriteString(c.renderV3Chain(v)) } else { - chain += renderChain(v) + chain.WriteString(renderChain(v)) } } - return "z.string()" + chain + return "z.string()" + chain.String() } // Format as base base := c.renderV4FormatBase(validators[formatIdx]) - chain := "" + var chain strings.Builder + if keepRequired { + chain.WriteString(".min(1)") + } for i := formatIdx + 1; i < len(validators); i++ { v := validators[i] if v.tag == "required" && !keepRequired { continue } - chain += renderChain(v) - } - if keepRequired { - chain = ".min(1)" + chain + chain.WriteString(renderChain(v)) } - return base + chain + return base + chain.String() } // Case 3: No format/union — plain string - chain := "" + var chain strings.Builder for _, v := range validators { - chain += renderChain(v) + chain.WriteString(renderChain(v)) } - return "z.string()" + chain + return "z.string()" + chain.String() } -// escapeJSString escapes backslashes and double quotes in a string so it can -// be safely interpolated into a JavaScript double-quoted string literal. -// Without this, struct tag values like `contains=foo"bar` would produce broken -// or injectable JS output such as `.includes("foo"bar")`. +// escapeJSString escapes a string so it can be safely interpolated into a +// JavaScript double-quoted string literal. Uses json.Marshal for complete +// handling of quotes, backslashes, newlines, and control characters, then +// strips the outer quotes. func escapeJSString(s string) string { - s = strings.ReplaceAll(s, `\`, `\\`) - s = strings.ReplaceAll(s, `"`, `\"`) - return s + b, _ := json.Marshal(s) + // json.Marshal wraps in quotes: "foo" → strip them + return string(b[1 : len(b)-1]) } // requireIntArg validates that arg is a valid integer for the given tag name. @@ -1507,7 +1509,7 @@ func (c *Converter) preprocessValidationTagPart(part string, refines *[]string, } if h, ok := c.customTags[valName]; ok { - v := h(c, reflect.TypeOf(0), valValue, 0) + v := h(c, reflect.TypeOf(""), valValue, 0) if strings.HasPrefix(v, ".refine") { *refines = append(*refines, v) } else { From f165d5e12d19714307ce624c24ba7079b557d141 Mon Sep 17 00:00:00 2001 From: Ramil Amparo Date: Fri, 3 Apr 2026 14:15:05 +0400 Subject: [PATCH 15/35] Add more custom tests --- .../custom_type_mapped_to_string.golden} | 0 ...om_type_resolves_inner_generic_type.golden | 14 +++ .../custom_type_with_nullable_control.golden | 7 ++ tests/cases.ts | 40 +++++++++ zod_test.go | 90 ++++++++++++++----- 5 files changed, 131 insertions(+), 20 deletions(-) rename testdata/{TestCustom.golden => TestCustomTypes/custom_type_mapped_to_string.golden} (100%) create mode 100644 testdata/TestCustomTypes/custom_type_resolves_inner_generic_type.golden create mode 100644 testdata/TestCustomTypes/custom_type_with_nullable_control.golden diff --git a/testdata/TestCustom.golden b/testdata/TestCustomTypes/custom_type_mapped_to_string.golden similarity index 100% rename from testdata/TestCustom.golden rename to testdata/TestCustomTypes/custom_type_mapped_to_string.golden diff --git a/testdata/TestCustomTypes/custom_type_resolves_inner_generic_type.golden b/testdata/TestCustomTypes/custom_type_resolves_inner_generic_type.golden new file mode 100644 index 0000000..63d618b --- /dev/null +++ b/testdata/TestCustomTypes/custom_type_resolves_inner_generic_type.golden @@ -0,0 +1,14 @@ +// @typecheck +export const ProfileSchema = z.object({ + Bio: z.string(), +}) +export type Profile = z.infer + +export const UserSchema = z.object({ + MaybeName: z.string().optional().nullish(), + MaybeAge: z.number().optional().nullish(), + MaybeHeight: z.number().optional().nullish(), + MaybeProfile: ProfileSchema.optional().nullish(), +}) +export type User = z.infer + diff --git a/testdata/TestCustomTypes/custom_type_with_nullable_control.golden b/testdata/TestCustomTypes/custom_type_with_nullable_control.golden new file mode 100644 index 0000000..029d662 --- /dev/null +++ b/testdata/TestCustomTypes/custom_type_with_nullable_control.golden @@ -0,0 +1,7 @@ +// @typecheck +export const UserSchema = z.object({ + Name: z.string(), + Email: z.string().optional().nullable(), +}) +export type User = z.infer + diff --git a/tests/cases.ts b/tests/cases.ts index 461016c..6f3d4ff 100644 --- a/tests/cases.ts +++ b/tests/cases.ts @@ -1606,4 +1606,44 @@ export const cases: TestCase[] = [ }, success: true, }, + + // --- Custom types --- + { + name: "custom type: mapped to string", + golden: "TestCustomTypes/custom_type_mapped_to_string.golden", + schema: "UserSchema", + input: { Name: "John", Money: "123.45" }, + success: true, + }, + { + name: "custom type: resolves inner generic type", + golden: "TestCustomTypes/custom_type_resolves_inner_generic_type.golden", + schema: "UserSchema", + input: { + MaybeName: "John", + MaybeAge: 30, + MaybeHeight: 1.8, + MaybeProfile: { Bio: "Hello" }, + }, + success: true, + }, + { + name: "custom type: resolves inner generic with nullish", + golden: "TestCustomTypes/custom_type_resolves_inner_generic_type.golden", + schema: "UserSchema", + input: { + MaybeName: null, + MaybeAge: undefined, + MaybeHeight: null, + MaybeProfile: undefined, + }, + success: true, + }, + { + name: "custom type: nullable pointer with custom handler", + golden: "TestCustomTypes/custom_type_with_nullable_control.golden", + schema: "UserSchema", + input: { Name: "John", Email: null }, + success: true, + }, ]; diff --git a/zod_test.go b/zod_test.go index b0041cd..eedf064 100644 --- a/zod_test.go +++ b/zod_test.go @@ -789,29 +789,79 @@ func TestDuration(t *testing.T) { assertSchema(t, User{}) } -func TestCustom(t *testing.T) { - type Decimal struct { - Value int - Exponent int - } +// Wrapper mimics a generic optional type like 4d63.com/optional.Optional[T]. +// The custom handler resolves the inner type via ConvertType(t.Elem(), ...). +type Wrapper[T any] struct{ Value T } + +func TestCustomTypes(t *testing.T) { + t.Run("custom type mapped to string", func(t *testing.T) { + type Decimal struct { + Value int + Exponent int + } + type User struct { + Name string + Money Decimal + } - type User struct { - Name string - Money Decimal - } + customTypes := map[string]CustomFn{ + "github.com/hypersequent/zen.Decimal": func(c *Converter, t reflect.Type, validate string, i int) string { + return "z.string()" + }, + } - customTypes := map[string]CustomFn{ - "github.com/hypersequent/zen.Decimal": func(c *Converter, t reflect.Type, validate string, i int) string { - return "z.string()" - }, - } + v3c := NewConverterWithOpts(WithCustomTypes(customTypes), WithZodV3()) + v4c := NewConverterWithOpts(WithCustomTypes(customTypes)) + v3out := v3c.Convert(User{}) + v4out := v4c.Convert(User{}) + assert.Equal(t, v3out, v4out) + goldenAssert(t, []byte(v4out)) + }) - v3c := NewConverterWithOpts(WithCustomTypes(customTypes), WithZodV3()) - v4c := NewConverterWithOpts(WithCustomTypes(customTypes)) - v3out := v3c.Convert(User{}) - v4out := v4c.Convert(User{}) - assert.Equal(t, v3out, v4out) - goldenAssert(t, []byte(v4out)) + t.Run("custom type resolves inner generic type", func(t *testing.T) { + type Profile struct { + Bio string + } + type User struct { + MaybeName Wrapper[string] + MaybeAge Wrapper[int] + MaybeHeight Wrapper[float64] + MaybeProfile Wrapper[Profile] + } + + customTypes := map[string]CustomFn{ + "github.com/hypersequent/zen.Wrapper": func(c *Converter, t reflect.Type, validate string, i int) string { + return fmt.Sprintf("%s.optional().nullish()", c.ConvertType(t.Field(0).Type, validate, i)) + }, + } + + v3c := NewConverterWithOpts(WithCustomTypes(customTypes), WithZodV3()) + v4c := NewConverterWithOpts(WithCustomTypes(customTypes)) + v3out := v3c.Convert(User{}) + v4out := v4c.Convert(User{}) + assert.Equal(t, v3out, v4out) + goldenAssert(t, []byte(v4out)) + }) + + t.Run("custom type with nullable control", func(t *testing.T) { + type User struct { + Name string + Email *Wrapper[string] + } + + customTypes := map[string]CustomFn{ + "github.com/hypersequent/zen.Wrapper": func(c *Converter, t reflect.Type, validate string, i int) string { + return fmt.Sprintf("%s.optional()", c.ConvertType(t.Field(0).Type, validate, i)) + }, + } + + v3c := NewConverterWithOpts(WithCustomTypes(customTypes), WithZodV3()) + v4c := NewConverterWithOpts(WithCustomTypes(customTypes)) + v3out := v3c.Convert(User{}) + v4out := v4c.Convert(User{}) + assert.Equal(t, v3out, v4out) + goldenAssert(t, []byte(v4out)) + }) } func TestEverything(t *testing.T) { From c7da97f6f8f1636b9fb80c779bcc1b9fcd36c1b5 Mon Sep 17 00:00:00 2001 From: Ramil Amparo Date: Fri, 3 Apr 2026 14:17:38 +0400 Subject: [PATCH 16/35] Add test for ignore tags --- .../ignores_specified_tag.golden | 6 ++++++ zod_test.go | 17 +++++++++++++++++ 2 files changed, 23 insertions(+) create mode 100644 testdata/TestWithIgnoreTags/ignores_specified_tag.golden diff --git a/testdata/TestWithIgnoreTags/ignores_specified_tag.golden b/testdata/TestWithIgnoreTags/ignores_specified_tag.golden new file mode 100644 index 0000000..d012f70 --- /dev/null +++ b/testdata/TestWithIgnoreTags/ignores_specified_tag.golden @@ -0,0 +1,6 @@ +// @typecheck +export const UserSchema = z.object({ + Name: z.string().min(1), +}) +export type User = z.infer + diff --git a/zod_test.go b/zod_test.go index eedf064..7417ee6 100644 --- a/zod_test.go +++ b/zod_test.go @@ -864,6 +864,23 @@ func TestCustomTypes(t *testing.T) { }) } +func TestWithIgnoreTags(t *testing.T) { + type User struct { + Name string `validate:"required,customtag=value"` + } + + t.Run("panics on unknown tag", func(t *testing.T) { + assert.Panics(t, func() { StructToZodSchema(User{}) }) + }) + + t.Run("ignores specified tag", func(t *testing.T) { + assert.NotPanics(t, func() { + StructToZodSchema(User{}, WithIgnoreTags("customtag")) + }) + goldenAssert(t, []byte(StructToZodSchema(User{}, WithIgnoreTags("customtag")))) + }) +} + func TestEverything(t *testing.T) { // The order matters PostWithMetaData needs to be declared after post otherwise it will raise a // `Block-scoped variable 'Post' used before its declaration.` typescript error. From c4282c73d8bb0b26289de9ab04af757f822de0df Mon Sep 17 00:00:00 2001 From: Ramil Amparo Date: Fri, 3 Apr 2026 14:32:26 +0400 Subject: [PATCH 17/35] Add custom type tests, WithIgnoreTags test, remove dead z.literal branch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add TestCustomTypes: custom type mapped to string, generic inner type resolution via ConvertType, and nullable pointer with custom handler - Add TestWithIgnoreTags: verifies unknown tags panic and ignored tags are silently skipped - Remove dead z.literal() branch from isPartialRecordKeySchema — no code path produces literal key schemas - Add comments for custom tag handler type parameter and isPartialRecordKeySchema - Add runtime test cases for custom types in tests/cases.ts Co-Authored-By: Claude Opus 4.6 (1M context) --- zod.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/zod.go b/zod.go index 0655dd3..c218fde 100644 --- a/zod.go +++ b/zod.go @@ -1096,6 +1096,8 @@ func (c *Converter) parseStringValidators(validate string) []stringValidator { } if h, ok := c.customTags[valName]; ok { + // The type parameter is string since this is a string validation context. + // Custom tag handlers may inspect it to vary their output by field type. v := h(c, reflect.TypeOf(""), valValue, 0) validators = append(validators, stringValidator{tag: "_custom", arg: v}) continue @@ -1470,9 +1472,11 @@ func (c *Converter) renderV4FormatBase(v stringValidator) string { } } +// isPartialRecordKeySchema returns true if the key schema represents a finite +// set of keys (enum), meaning z.partialRecord should be used instead of z.record. func isPartialRecordKeySchema(schema string) bool { schema = strings.TrimSpace(schema) - return strings.HasPrefix(schema, "z.enum(") || strings.HasPrefix(schema, "z.literal(") + return strings.HasPrefix(schema, "z.enum(") } func (c *Converter) parseValidationTagPart(part string) (string, string, bool) { @@ -1509,6 +1513,8 @@ func (c *Converter) preprocessValidationTagPart(part string, refines *[]string, } if h, ok := c.customTags[valName]; ok { + // The type parameter is string since this is a string validation context. + // Custom tag handlers may inspect it to vary their output by field type. v := h(c, reflect.TypeOf(""), valValue, 0) if strings.HasPrefix(v, ".refine") { *refines = append(*refines, v) From 98dcbe37d6dcf1be1e2d52375d7b57baae60b885 Mon Sep 17 00:00:00 2001 From: Ramil Amparo Date: Fri, 3 Apr 2026 14:36:43 +0400 Subject: [PATCH 18/35] Move enum-keyed map test out of TestZodV4Defaults Test now covers both v3 (z.record) and v4 (z.partialRecord), so it no longer belongs in v4-only test group. Renamed to TestMapWithEnumKey and updated golden file paths in runtime test cases. Co-Authored-By: Claude Opus 4.6 (1M context) --- testdata/TestMapWithEnumKey/v3.golden | 7 +++++++ .../v4.golden} | 0 tests/cases.ts | 4 ++-- zod_test.go | 16 ++++++++-------- 4 files changed, 17 insertions(+), 10 deletions(-) create mode 100644 testdata/TestMapWithEnumKey/v3.golden rename testdata/{TestZodV4Defaults/enum_keyed_maps_become_partial_records.golden => TestMapWithEnumKey/v4.golden} (100%) diff --git a/testdata/TestMapWithEnumKey/v3.golden b/testdata/TestMapWithEnumKey/v3.golden new file mode 100644 index 0000000..8d55639 --- /dev/null +++ b/testdata/TestMapWithEnumKey/v3.golden @@ -0,0 +1,7 @@ +// @zod-version: v3 +// @typecheck +export const PayloadSchema = z.object({ + Metadata: z.record(z.enum(["draft", "published"] as const), z.string()).nullable(), +}) +export type Payload = z.infer + diff --git a/testdata/TestZodV4Defaults/enum_keyed_maps_become_partial_records.golden b/testdata/TestMapWithEnumKey/v4.golden similarity index 100% rename from testdata/TestZodV4Defaults/enum_keyed_maps_become_partial_records.golden rename to testdata/TestMapWithEnumKey/v4.golden diff --git a/tests/cases.ts b/tests/cases.ts index 6f3d4ff..297a7fe 100644 --- a/tests/cases.ts +++ b/tests/cases.ts @@ -1543,14 +1543,14 @@ export const cases: TestCase[] = [ // --- TestZodV4Defaults/enum_keyed_maps_become_partial_records --- { name: "v4 defaults: enum keyed maps become partial records", - golden: "TestZodV4Defaults/enum_keyed_maps_become_partial_records.golden", + golden: "TestMapWithEnumKey/v4.golden", schema: "PayloadSchema", input: { Metadata: { draft: "some note" } }, success: true, }, { name: "v4 defaults: enum keyed maps for partial records reject invalid keys", - golden: "TestZodV4Defaults/enum_keyed_maps_become_partial_records.golden", + golden: "TestMapWithEnumKey/v4.golden", schema: "PayloadSchema", input: { Metadata: { invalid: "some note" } }, success: false, diff --git a/zod_test.go b/zod_test.go index 7417ee6..e349597 100644 --- a/zod_test.go +++ b/zod_test.go @@ -538,14 +538,6 @@ func TestZodV4Defaults(t *testing.T) { assertSchema(t, Payload{}, "v3", "v4") }) - t.Run("enum keyed maps become partial records", func(t *testing.T) { - type Payload struct { - Metadata map[string]string `validate:"dive,keys,oneof=draft published,endkeys"` - } - - assertSchema(t, Payload{}, "v4") - }) - t.Run("named field shadows embedded field", func(t *testing.T) { type Base struct { ID string `json:"id"` @@ -728,6 +720,14 @@ func TestMapWithNonStringKey(t *testing.T) { }) } +func TestMapWithEnumKey(t *testing.T) { + type Payload struct { + Metadata map[string]string `validate:"dive,keys,oneof=draft published,endkeys"` + } + + assertSchema(t, Payload{}, "v3", "v4") +} + func TestGetValidateKeys(t *testing.T) { assert.Equal(t, "min=3", getValidateKeys("dive,keys,min=3,endkeys,max=4")) assert.Equal(t, "min=3,max=5", getValidateKeys("dive,keys,min=3,max=5,endkeys,max=4")) From 45d3b50b92fe5f35added984dab669698fafc0fa Mon Sep 17 00:00:00 2001 From: Ramil Amparo Date: Fri, 3 Apr 2026 14:58:01 +0400 Subject: [PATCH 19/35] Fix runtime tests --- tests/cases.ts | 8 -------- 1 file changed, 8 deletions(-) diff --git a/tests/cases.ts b/tests/cases.ts index 297a7fe..010d770 100644 --- a/tests/cases.ts +++ b/tests/cases.ts @@ -201,14 +201,6 @@ export const cases: TestCase[] = [ success: false, }, - // --- TestCustom --- - { - name: "custom type: parses valid object", - golden: "TestCustom.golden", - schema: "UserSchema", - input: { Name: "John", Money: "100.00" }, - success: true, - }, // --------------------------------------------------------------------------- // ARRAYS From 4b9756663427fe86e56127d9381517211beda51e Mon Sep 17 00:00:00 2001 From: Ramil Amparo Date: Fri, 3 Apr 2026 15:19:21 +0400 Subject: [PATCH 20/35] Panic on invalid tags and add more tests --- tests/cases.ts | 41 +++++++++++++++++++++++++++++-- zod.go | 43 +++++++++++++++++++++++++++++---- zod_test.go | 65 ++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 142 insertions(+), 7 deletions(-) diff --git a/tests/cases.ts b/tests/cases.ts index 010d770..6e36b61 100644 --- a/tests/cases.ts +++ b/tests/cases.ts @@ -1293,14 +1293,28 @@ export const cases: TestCase[] = [ success: false, }, - // --- urlSchema --- + // --- urlSchema (z.url() accepts any scheme) --- { - name: "url: accepts valid url", + name: "url: accepts https", golden: "TestFormatValidators/format_only/v4.golden", schema: "urlSchema", input: { value: "https://example.com" }, success: true, }, + { + name: "url: accepts ftp", + golden: "TestFormatValidators/format_only/v4.golden", + schema: "urlSchema", + input: { value: "ftp://files.example.com/file.txt" }, + success: true, + }, + { + name: "url: accepts mailto", + golden: "TestFormatValidators/format_only/v4.golden", + schema: "urlSchema", + input: { value: "mailto:user@example.com" }, + success: true, + }, { name: "url: rejects invalid url", golden: "TestFormatValidators/format_only/v4.golden", @@ -1309,6 +1323,29 @@ export const cases: TestCase[] = [ success: false, }, + // --- http_urlSchema (z.httpUrl() only accepts http/https) --- + { + name: "http_url: accepts https", + golden: "TestFormatValidators/format_only/v4.golden", + schema: "http_urlSchema", + input: { value: "https://example.com" }, + success: true, + }, + { + name: "http_url: rejects ftp", + golden: "TestFormatValidators/format_only/v4.golden", + schema: "http_urlSchema", + input: { value: "ftp://files.example.com/file.txt" }, + success: false, + }, + { + name: "http_url: rejects mailto", + golden: "TestFormatValidators/format_only/v4.golden", + schema: "http_urlSchema", + input: { value: "mailto:user@example.com" }, + success: false, + }, + // --- ipv4Schema --- { name: "ipv4: accepts valid ipv4", diff --git a/zod.go b/zod.go index c218fde..c1cdd2c 100644 --- a/zod.go +++ b/zod.go @@ -749,30 +749,37 @@ forParts: if valValue != "" { switch valName { case "min": + requireIntArg("min", valValue) validateStr.WriteString(fmt.Sprintf(".min(%s)", valValue)) case "max": + requireIntArg("max", valValue) validateStr.WriteString(fmt.Sprintf(".max(%s)", valValue)) case "len": + requireIntArg("len", valValue) validateStr.WriteString(fmt.Sprintf(".length(%s)", valValue)) case "eq": + requireIntArg("eq", valValue) validateStr.WriteString(fmt.Sprintf(".length(%s)", valValue)) case "ne": + requireIntArg("ne", valValue) refines = append(refines, fmt.Sprintf(".refine((val) => val.length !== %s)", valValue)) case "gt": - val, err := strconv.Atoi(valValue) - if err != nil || val < 0 { + val := requireIntArg("gt", valValue) + if val < 0 { panic(fmt.Sprintf("invalid gt value: %s", valValue)) } validateStr.WriteString(fmt.Sprintf(".min(%d)", val+1)) case "gte": + requireIntArg("gte", valValue) validateStr.WriteString(fmt.Sprintf(".min(%s)", valValue)) case "lt": - val, err := strconv.Atoi(valValue) - if err != nil || val <= 0 { + val := requireIntArg("lt", valValue) + if val <= 0 { panic(fmt.Sprintf("invalid lt value: %s", valValue)) } validateStr.WriteString(fmt.Sprintf(".max(%d)", val-1)) case "lte": + requireIntArg("lte", valValue) validateStr.WriteString(fmt.Sprintf(".max(%s)", valValue)) default: @@ -859,22 +866,31 @@ forParts: if valValue != "" { switch valName { case "min": + requireIntArg("min", valValue) refines = append(refines, fmt.Sprintf(".refine((val) => Object.keys(val).length >= %s, 'Map too small')", valValue)) case "max": + requireIntArg("max", valValue) refines = append(refines, fmt.Sprintf(".refine((val) => Object.keys(val).length <= %s, 'Map too large')", valValue)) case "len": + requireIntArg("len", valValue) refines = append(refines, fmt.Sprintf(".refine((val) => Object.keys(val).length === %s, 'Map wrong size')", valValue)) case "eq": + requireIntArg("eq", valValue) refines = append(refines, fmt.Sprintf(".refine((val) => Object.keys(val).length === %s, 'Map wrong size')", valValue)) case "ne": + requireIntArg("ne", valValue) refines = append(refines, fmt.Sprintf(".refine((val) => Object.keys(val).length !== %s, 'Map wrong size')", valValue)) case "gt": + requireIntArg("gt", valValue) refines = append(refines, fmt.Sprintf(".refine((val) => Object.keys(val).length > %s, 'Map too small')", valValue)) case "gte": + requireIntArg("gte", valValue) refines = append(refines, fmt.Sprintf(".refine((val) => Object.keys(val).length >= %s, 'Map too small')", valValue)) case "lt": + requireIntArg("lt", valValue) refines = append(refines, fmt.Sprintf(".refine((val) => Object.keys(val).length < %s, 'Map too large')", valValue)) case "lte": + requireIntArg("lte", valValue) refines = append(refines, fmt.Sprintf(".refine((val) => Object.keys(val).length <= %s, 'Map too large')", valValue)) default: @@ -1000,22 +1016,31 @@ func (c *Converter) validateNumber(validate string) string { if valValue != "" { switch valName { case "gt": + requireNumericArg("gt", valValue) validateStr.WriteString(fmt.Sprintf(".gt(%s)", valValue)) case "gte", "min": + requireNumericArg(valName, valValue) validateStr.WriteString(fmt.Sprintf(".gte(%s)", valValue)) case "lt": + requireNumericArg("lt", valValue) validateStr.WriteString(fmt.Sprintf(".lt(%s)", valValue)) case "lte", "max": + requireNumericArg(valName, valValue) validateStr.WriteString(fmt.Sprintf(".lte(%s)", valValue)) case "eq", "len": + requireNumericArg(valName, valValue) refines = append(refines, fmt.Sprintf(".refine((val) => val === %s)", valValue)) case "ne": + requireNumericArg("ne", valValue) refines = append(refines, fmt.Sprintf(".refine((val) => val !== %s)", valValue)) case "oneof": vals := strings.Fields(valValue) if len(vals) == 0 { panic(fmt.Sprintf("invalid oneof validation: %s", part)) } + for _, v := range vals { + requireNumericArg("oneof", v) + } refines = append(refines, fmt.Sprintf(".refine((val) => [%s].includes(val))", strings.Join(vals, ", "))) default: @@ -1293,6 +1318,14 @@ func requireIntArg(tag, arg string) int { return val } +// requireNumericArg validates that arg is a valid number (integer or float). +// Panics if arg is not a valid number. +func requireNumericArg(tag, arg string) { + if _, err := strconv.ParseFloat(arg, 64); err != nil { + panic(fmt.Sprintf("%s= requires a numeric argument, got: %s", tag, arg)) + } +} + // regexChainMap maps validator tags to their regex pattern strings. // Used by renderChain and renderV3Chain to generate .regex() calls. var regexChainMap = map[string]string{ @@ -1425,7 +1458,7 @@ func (c *Converter) renderV3Chain(v stringValidator) string { case "datetime": return ".datetime()" default: - return "" + panic(fmt.Sprintf("renderV3Chain: unhandled tag %q", v.tag)) } } diff --git a/zod_test.go b/zod_test.go index e349597..aa9e949 100644 --- a/zod_test.go +++ b/zod_test.go @@ -600,6 +600,39 @@ func TestNumberValidations(t *testing.T) { } assert.Panics(t, func() { StructToZodSchema(Bad{}) }) }) + + t.Run("non-numeric arg panics", func(t *testing.T) { + tags := []string{"gt", "gte", "lt", "lte", "min", "max", "eq", "ne", "len"} + for _, tag := range tags { + t.Run(tag, func(t *testing.T) { + assert.Panics(t, func() { + st := reflect.StructOf([]reflect.StructField{{ + Name: "V", + Type: reflect.TypeOf(0), + Tag: reflect.StructTag(fmt.Sprintf(`validate:"%s=abc" json:"v"`, tag)), + }}) + StructToZodSchema(reflect.New(st).Elem().Interface()) + }) + }) + } + t.Run("oneof", func(t *testing.T) { + assert.Panics(t, func() { + st := reflect.StructOf([]reflect.StructField{{ + Name: "V", + Type: reflect.TypeOf(0), + Tag: reflect.StructTag(`validate:"oneof=1 abc 3" json:"v"`), + }}) + StructToZodSchema(reflect.New(st).Elem().Interface()) + }) + }) + }) + + t.Run("float args are accepted", func(t *testing.T) { + type S struct { + V float64 `validate:"gt=1.5,lt=9.9"` + } + assert.NotPanics(t, func() { StructToZodSchema(S{}) }) + }) } func TestInterfaceAny(t *testing.T) { @@ -689,6 +722,22 @@ func TestMapWithValidations(t *testing.T) { } assert.Panics(t, func() { StructToZodSchema(Bad{}) }) }) + + t.Run("non-integer args panic", func(t *testing.T) { + tags := []string{"min", "max", "len", "eq", "ne", "gt", "gte", "lt", "lte"} + for _, tag := range tags { + t.Run(tag, func(t *testing.T) { + assert.Panics(t, func() { + st := reflect.StructOf([]reflect.StructField{{ + Name: "M", + Type: reflect.TypeOf(map[string]string{}), + Tag: reflect.StructTag(fmt.Sprintf(`validate:"%s=abc" json:"m"`, tag)), + }}) + StructToZodSchema(reflect.New(st).Elem().Interface()) + }) + }) + } + }) } func TestMapWithNonStringKey(t *testing.T) { @@ -1044,6 +1093,22 @@ func TestConvertSliceWithValidations(t *testing.T) { StructToZodSchema(Bad{}) }) }) + + t.Run("non-integer args panic", func(t *testing.T) { + tags := []string{"min", "max", "len", "eq", "ne", "gt", "gte", "lt", "lte"} + for _, tag := range tags { + t.Run(tag, func(t *testing.T) { + assert.Panics(t, func() { + st := reflect.StructOf([]reflect.StructField{{ + Name: "V", + Type: reflect.TypeOf([]string{}), + Tag: reflect.StructTag(fmt.Sprintf(`validate:"%s=abc" json:"v"`, tag)), + }}) + StructToZodSchema(reflect.New(st).Elem().Interface()) + }) + }) + } + }) } func TestRecursive1(t *testing.T) { From e470ca926e9df834efa3dd53294d7df44416cf13 Mon Sep 17 00:00:00 2001 From: Ramil Amparo Date: Fri, 3 Apr 2026 15:34:14 +0400 Subject: [PATCH 21/35] Panic on unknown tag --- zod.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zod.go b/zod.go index c1cdd2c..fc45a91 100644 --- a/zod.go +++ b/zod.go @@ -1408,7 +1408,7 @@ func renderChain(v stringValidator) string { case "_custom": return v.arg default: - return "" + panic(fmt.Sprintf("renderChain: unhandled format tag %q", v.tag)) } } From 1beae0bfa95aa9df334f693f4ca5ed938cf80a73 Mon Sep 17 00:00:00 2001 From: Ramil Amparo Date: Fri, 3 Apr 2026 15:39:14 +0400 Subject: [PATCH 22/35] Fix panicing --- zod.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/zod.go b/zod.go index fc45a91..25cfb3a 100644 --- a/zod.go +++ b/zod.go @@ -1408,7 +1408,12 @@ func renderChain(v stringValidator) string { case "_custom": return v.arg default: - panic(fmt.Sprintf("renderChain: unhandled format tag %q", v.tag)) + // Format/union tags (email, url, ip, etc.) are handled by + // renderV3Chain or renderV4FormatBase, not here. + if !formatTags[v.tag] && !unionTags[v.tag] { + panic(fmt.Sprintf("renderChain: unhandled tag %q", v.tag)) + } + return "" } } From a71118d3283b6f20a129e1c3d39d6c7bdc243cd5 Mon Sep 17 00:00:00 2001 From: Ramil Amparo Date: Fri, 3 Apr 2026 15:57:35 +0400 Subject: [PATCH 23/35] Fix shadowed variable --- zod.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/zod.go b/zod.go index 25cfb3a..a8d2fb3 100644 --- a/zod.go +++ b/zod.go @@ -669,11 +669,11 @@ func (c *Converter) convertNamedField(f reflect.StructField, indent int, optiona func (c *Converter) convertEmbeddedFieldMerge(f reflect.StructField, indent int) (string, bool) { t := c.convertType(f.Type, f.Tag.Get("validate"), indent).text - typeName := typeName(f.Type) - entry, ok := c.outputs[typeName] + name := typeName(f.Type) + entry, ok := c.outputs[name] if ok && entry.selfRef { // Since we are spreading shape, we won't be able to support any validation tags on the embedded field - return fmt.Sprintf("%s...%s,\n", indentation(indent), shapeName(c.prefix, typeName)), false + return fmt.Sprintf("%s...%s,\n", indentation(indent), shapeName(c.prefix, name)), false } return fmt.Sprintf(".merge(%s)", t), true From 36d03d0ce5c097333e1e05f8dcda369ef59dffd4 Mon Sep 17 00:00:00 2001 From: Ramil Amparo Date: Fri, 3 Apr 2026 16:14:26 +0400 Subject: [PATCH 24/35] Fix boolean schema --- testdata/TestStringValidations.golden | Bin 5266 -> 5275 bytes zod.go | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/testdata/TestStringValidations.golden b/testdata/TestStringValidations.golden index 689ba05b18795d9a8ca49164c506b360cd2b3443..8d4d20bb919cb42795cc773ce7c3e00f306997e9 100644 GIT binary patch delta 38 tcmbQFIa_l>I-jIcNl|I4l8%B>T4GLds#2^%VzEMUeqM3O=5ju1HURXS3{C(5 delta 29 kcmbQOIZ1OvI-h`gNl|I4x{iW+T4GLds(S3^20m#v0FfIBCjbBd diff --git a/zod.go b/zod.go index a8d2fb3..5e68472 100644 --- a/zod.go +++ b/zod.go @@ -1139,7 +1139,7 @@ func (c *Converter) parseStringValidators(validate string) []stringValidator { enumText := fmt.Sprintf("z.enum([\"%s\"] as const)", strings.Join(vals, "\", \"")) validators = append(validators, stringValidator{tag: "oneof", arg: enumText}) case valName == "boolean": - validators = append(validators, stringValidator{tag: "boolean", arg: "z.enum(['true', 'false'])"}) + validators = append(validators, stringValidator{tag: "boolean", arg: `z.enum(["true", "false"] as const)`}) case knownStringTags[valName]: validators = append(validators, stringValidator{tag: valName, arg: valValue}) default: From e17befb06b750859f412e343a43c29e3bcefd38a Mon Sep 17 00:00:00 2001 From: Ramil Amparo Date: Fri, 3 Apr 2026 17:27:56 +0400 Subject: [PATCH 25/35] Simplify enum processing --- testdata/TestOneofRequired.golden | 8 ++ .../enum_ignores_other_validators.golden | 26 ++++++ ...s_precedence_over_ip_specialization.golden | 7 -- tests/cases.ts | 84 ++++++++++++++++--- zod.go | 22 +---- zod_test.go | 40 +++++++-- 6 files changed, 142 insertions(+), 45 deletions(-) create mode 100644 testdata/TestOneofRequired.golden create mode 100644 testdata/TestStringValidations/enum_ignores_other_validators.golden delete mode 100644 testdata/TestZodV4Defaults/oneof_takes_precedence_over_ip_specialization.golden diff --git a/testdata/TestOneofRequired.golden b/testdata/TestOneofRequired.golden new file mode 100644 index 0000000..a18d223 --- /dev/null +++ b/testdata/TestOneofRequired.golden @@ -0,0 +1,8 @@ +// @typecheck +export const PayloadSchema = z.object({ + status: z.enum(["active", "inactive"] as const), + statusImplicitRequired: z.enum(["active", "inactive"] as const), + channel: z.enum(["email", "sms"] as const).optional(), +}) +export type Payload = z.infer + diff --git a/testdata/TestStringValidations/enum_ignores_other_validators.golden b/testdata/TestStringValidations/enum_ignores_other_validators.golden new file mode 100644 index 0000000..a22d98d --- /dev/null +++ b/testdata/TestStringValidations/enum_ignores_other_validators.golden @@ -0,0 +1,26 @@ +// @typecheck +export const RequiredOneofSchema = z.object({ + v: z.enum(["a", "b"] as const), +}) +export type RequiredOneof = z.infer + +export const OneofContainsSchema = z.object({ + v: z.enum(["a", "b"] as const), +}) +export type OneofContains = z.infer + +export const OneofStartswithSchema = z.object({ + v: z.enum(["a", "b"] as const), +}) +export type OneofStartswith = z.infer + +export const OneofEndswithSchema = z.object({ + v: z.enum(["a", "b"] as const), +}) +export type OneofEndswith = z.infer + +export const OneofIpSchema = z.object({ + v: z.enum(["127.0.0.1", "::1"] as const), +}) +export type OneofIp = z.infer + diff --git a/testdata/TestZodV4Defaults/oneof_takes_precedence_over_ip_specialization.golden b/testdata/TestZodV4Defaults/oneof_takes_precedence_over_ip_specialization.golden deleted file mode 100644 index 9263d61..0000000 --- a/testdata/TestZodV4Defaults/oneof_takes_precedence_over_ip_specialization.golden +++ /dev/null @@ -1,7 +0,0 @@ -// @zod-version: v4 -// @typecheck -export const PayloadSchema = z.object({ - Address: z.enum(["127.0.0.1", "::1"] as const), -}) -export type Payload = z.infer - diff --git a/tests/cases.ts b/tests/cases.ts index 6e36b61..032133b 100644 --- a/tests/cases.ts +++ b/tests/cases.ts @@ -942,6 +942,80 @@ export const cases: TestCase[] = [ success: false, }, + // --- TestOneofRequired --- + { + name: "oneof required: accepts valid status", + golden: "TestOneofRequired.golden", + schema: "PayloadSchema", + input: { status: "active" }, + success: true, + }, + { + name: "oneof required: rejects empty status", + golden: "TestOneofRequired.golden", + schema: "PayloadSchema", + input: { status: "" }, + success: false, + }, + { + name: "oneof required: rejects invalid status", + golden: "TestOneofRequired.golden", + schema: "PayloadSchema", + input: { status: "deleted" }, + success: false, + }, + { + name: "oneof optional: accepts with channel present", + golden: "TestOneofRequired.golden", + schema: "PayloadSchema", + input: { status: "active", channel: "email" }, + success: true, + }, + { + name: "oneof optional: accepts without channel (optional)", + golden: "TestOneofRequired.golden", + schema: "PayloadSchema", + input: { status: "active" }, + success: true, + }, + { + name: "oneof optional: rejects invalid channel", + golden: "TestOneofRequired.golden", + schema: "PayloadSchema", + input: { status: "active", channel: "phone" }, + success: false, + }, + + // --- OneofIpSchema (enum ignores ip validator) --- + { + name: "oneof+ip: accepts value in oneof list", + golden: "TestStringValidations/enum_ignores_other_validators.golden", + schema: "OneofIpSchema", + input: { v: "127.0.0.1" }, + success: true, + }, + { + name: "oneof+ip: accepts second value in oneof list", + golden: "TestStringValidations/enum_ignores_other_validators.golden", + schema: "OneofIpSchema", + input: { v: "::1" }, + success: true, + }, + { + name: "oneof+ip: rejects valid ip not in oneof list", + golden: "TestStringValidations/enum_ignores_other_validators.golden", + schema: "OneofIpSchema", + input: { v: "192.168.1.1" }, + success: false, + }, + { + name: "oneof+ip: rejects non-ip string", + golden: "TestStringValidations/enum_ignores_other_validators.golden", + schema: "OneofIpSchema", + input: { v: "hello" }, + success: false, + }, + // --- lenSchema --- { name: "string len: accepts string of length 5", @@ -1595,16 +1669,6 @@ export const cases: TestCase[] = [ success: true, }, - // --- TestZodV4Defaults/oneof_takes_precedence_over_ip_specialization --- - { - name: "v4 defaults: oneof takes precedence over ip specialization", - golden: - "TestZodV4Defaults/oneof_takes_precedence_over_ip_specialization.golden", - schema: "PayloadSchema", - input: { Address: "127.0.0.1" }, - success: true, - }, - // --- TestZodV4Defaults/optional_format_with_nullable_pointer/v4 --- { name: "v4 defaults: optional format with nullable pointer accepts null", diff --git a/zod.go b/zod.go index 5e68472..b5aae0c 100644 --- a/zod.go +++ b/zod.go @@ -1186,31 +1186,13 @@ func (c *Converter) renderStringSchema(validators []stringValidator) string { panic("cannot combine multiple format validators (e.g. email + url)") } - // Phase 3: Handle enum — return early + // Phase 3: Handle enum — return early, other validators are redundant if hasEnum { - base := "" - var chain strings.Builder - for _, v := range validators { - if v.tag == "oneof" || v.tag == "boolean" { - base = v.arg - break - } - } for _, v := range validators { if v.tag == "oneof" || v.tag == "boolean" { - continue - } - var rendered string - if c.zodV3 { - rendered = c.renderV3Chain(v) - } else { - rendered = renderChain(v) - } - if strings.HasPrefix(rendered, ".refine") { - chain.WriteString(rendered) + return v.arg } } - return base + chain.String() } // Phase 4: Render v3 diff --git a/zod_test.go b/zod_test.go index aa9e949..68cf617 100644 --- a/zod_test.go +++ b/zod_test.go @@ -452,6 +452,38 @@ func TestStringValidations(t *testing.T) { goldenAssert(t, []byte(c.Export())) }) + + t.Run("enum ignores other validators", func(t *testing.T) { + c := NewConverterWithOpts() + c.AddTypeWithName(struct { + V string `validate:"required,oneof=a b" json:"v"` + }{}, "RequiredOneof") + c.AddTypeWithName(struct { + V string `validate:"oneof=a b,contains=x" json:"v"` + }{}, "OneofContains") + c.AddTypeWithName(struct { + V string `validate:"oneof=a b,startswith=a" json:"v"` + }{}, "OneofStartswith") + c.AddTypeWithName(struct { + V string `validate:"oneof=a b,endswith=z" json:"v"` + }{}, "OneofEndswith") + c.AddTypeWithName(struct { + V string `validate:"oneof='127.0.0.1' '::1',ip" json:"v"` + }{}, "OneofIp") + goldenAssert(t, []byte(c.Export())) + }) +} + +func TestOneofRequired(t *testing.T) { + type Payload struct { + Status string `json:"status" validate:"required,oneof=active inactive"` + // Would generate the same schema as the above. This doesn't mirror go validator exactly as it allows empty values. + // For now let's assume that empty strings are not valid enum values, but we can revisit if there's demand for that. + StatusImplicitRequired string `json:"statusImplicitRequired" validate:"oneof=active inactive"` + Channel *string `json:"channel,omitempty" validate:"omitempty,oneof=email sms"` + } + + assertSchema(t, Payload{}) } func TestZodV4Defaults(t *testing.T) { @@ -506,14 +538,6 @@ func TestZodV4Defaults(t *testing.T) { assertSchema(t, Payload{}, "v4") }) - t.Run("oneof takes precedence over ip specialization", func(t *testing.T) { - type Payload struct { - Address string `validate:"oneof='127.0.0.1' '::1',ip"` - } - - assertSchema(t, Payload{}, "v4") - }) - t.Run("format combined with union panics", func(t *testing.T) { type Payload struct { Address string `validate:"email,ip"` From 0df6343efe02b9a5bfd505246350770d4e0ea64a Mon Sep 17 00:00:00 2001 From: Ramil Amparo Date: Fri, 3 Apr 2026 17:51:27 +0400 Subject: [PATCH 26/35] Fix custom type handlers --- zod.go | 20 +++++++++----------- zod_test.go | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 11 deletions(-) diff --git a/zod.go b/zod.go index b5aae0c..359cc6b 100644 --- a/zod.go +++ b/zod.go @@ -545,7 +545,7 @@ func (c *Converter) convertType(t reflect.Type, validate string, indent int) con } for _, part := range parts { - valName, _, done := c.preprocessValidationTagPart(part, &refines, &validateStr) + valName, _, done := c.preprocessValidationTagPart(part, &refines, &validateStr, t) if done { continue } @@ -581,7 +581,7 @@ func (c *Converter) convertType(t reflect.Type, validate string, indent int) con case "string": return convertResult{text: c.validateString(validate)} case "number": - validateStr = c.validateNumber(validate) + validateStr = c.validateNumber(validate, t) } } @@ -738,7 +738,7 @@ func (c *Converter) convertSliceAndArray(t reflect.Type, validate string, indent forParts: for _, part := range parts { - valName, valValue, done := c.preprocessValidationTagPart(part, &refines, &validateStr) + valName, valValue, done := c.preprocessValidationTagPart(part, &refines, &validateStr, t) if done { continue } @@ -838,7 +838,7 @@ func (c *Converter) convertKeyType(t reflect.Type, validate string) string { case "string": return c.validateString(validate) case "number": - validateStr = c.validateNumber(validate) + validateStr = c.validateNumber(validate, t) } } @@ -858,7 +858,7 @@ func (c *Converter) convertMap(t reflect.Type, validate string, indent int) stri forParts: for _, part := range parts { - valName, valValue, done := c.preprocessValidationTagPart(part, &refines, &validateStr) + valName, valValue, done := c.preprocessValidationTagPart(part, &refines, &validateStr, t) if done { continue } @@ -1002,13 +1002,13 @@ func (c *Converter) checkIsIgnored(part string) bool { // not implementing omitempty for numbers and strings // could support unusual cases like `validate:"omitempty,min=3,max=5"` -func (c *Converter) validateNumber(validate string) string { +func (c *Converter) validateNumber(validate string, t reflect.Type) string { var validateStr strings.Builder var refines []string parts := strings.Split(validate, ",") for _, part := range parts { - valName, valValue, done := c.preprocessValidationTagPart(part, &refines, &validateStr) + valName, valValue, done := c.preprocessValidationTagPart(part, &refines, &validateStr, t) if done { continue } @@ -1526,16 +1526,14 @@ func (c *Converter) parseValidationTagPart(part string) (string, string, bool) { return valName, valValue, false } -func (c *Converter) preprocessValidationTagPart(part string, refines *[]string, validateStr *strings.Builder) (string, string, bool) { +func (c *Converter) preprocessValidationTagPart(part string, refines *[]string, validateStr *strings.Builder, t reflect.Type) (string, string, bool) { valName, valValue, done := c.parseValidationTagPart(part) if done { return "", "", true } if h, ok := c.customTags[valName]; ok { - // The type parameter is string since this is a string validation context. - // Custom tag handlers may inspect it to vary their output by field type. - v := h(c, reflect.TypeOf(""), valValue, 0) + v := h(c, t, valValue, 0) if strings.HasPrefix(v, ".refine") { *refines = append(*refines, v) } else { diff --git a/zod_test.go b/zod_test.go index 68cf617..0e90b60 100644 --- a/zod_test.go +++ b/zod_test.go @@ -1256,6 +1256,41 @@ func TestCustomTag(t *testing.T) { }) } +func TestCustomTagReceivesCorrectType(t *testing.T) { + // A "nonzero" custom tag that emits different validation depending on the + // field type: strings check for non-empty, numbers check for non-zero, + // time.Time checks for non-zero date. + handler := map[string]CustomFn{ + "nonzero": func(c *Converter, t reflect.Type, validate string, i int) string { + switch t.Kind() { + case reflect.String: + return `.refine((val) => val !== "", "must not be empty")` + case reflect.Int, reflect.Float64: + return ".refine((val) => val !== 0, \"must not be zero\")" + case reflect.Struct: + if t.Name() == "Time" { + return ".refine((val) => val.getTime() !== 0, \"must not be zero time\")" + } + return ".refine((val) => true)" + default: + return ".refine((val) => true)" + } + }, + } + + type Payload struct { + Name string `json:"name" validate:"nonzero"` + Age int `json:"age" validate:"nonzero"` + When time.Time `json:"when" validate:"nonzero"` + } + + output := NewConverterWithOpts(WithCustomTags(handler)).Convert(Payload{}) + + assert.Contains(t, output, `val !== ""`, "string field should get string-specific check") + assert.Contains(t, output, `val !== 0, "must not be zero"`, "number field should get number-specific check") + assert.Contains(t, output, `val.getTime() !== 0`, "time field should get time-specific check") +} + func TestRecursiveEmbeddedStruct(t *testing.T) { type ItemA struct { Name string From 5a3d986b3d8664df7060edfda1e95e70dc8505bd Mon Sep 17 00:00:00 2001 From: Ramil Amparo Date: Fri, 3 Apr 2026 18:05:21 +0400 Subject: [PATCH 27/35] Fixes --- zod.go | 8 ++++++-- zod_test.go | 4 ++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/zod.go b/zod.go index 359cc6b..64c9057 100644 --- a/zod.go +++ b/zod.go @@ -971,7 +971,11 @@ func getValidateValues(validate string) string { var validateValues string if strings.Contains(validate, "dive,keys") { - removedPrefix := strings.SplitN(validate, ",endkeys", 2)[1] + parts := strings.SplitN(validate, ",endkeys", 2) + if len(parts) < 2 { + panic("malformed validation: 'dive,keys' without matching 'endkeys'") + } + removedPrefix := parts[1] if strings.Contains(removedPrefix, ",dive") { validateValues = strings.SplitN(removedPrefix, ",dive", 2)[0] @@ -1661,7 +1665,7 @@ func getTypeNameWithGenerics(name string) string { typeArgs := strings.Split(name[typeArgsIdx+1:len(name)-1], ",") for _, arg := range typeArgs { - sb.WriteString(strings.ToTitle(arg[:1])) // Capitalize first letter + sb.WriteString(strings.ToUpper(arg[:1])) // Capitalize first letter sb.WriteString(arg[1:]) } diff --git a/zod_test.go b/zod_test.go index 0e90b60..60a03b4 100644 --- a/zod_test.go +++ b/zod_test.go @@ -832,6 +832,10 @@ func TestGetValidateValues(t *testing.T) { assert.Equal(t, "min=3", getValidateValues("min=2,dive,min=3")) assert.Equal(t, "min=3,max=4", getValidateValues("dive,min=3,max=4,dive,min=4,max=5")) assert.Equal(t, "max=4", getValidateValues("min=2,dive,keys,min=3,endkeys,max=4")) + + t.Run("dive keys without endkeys panics", func(t *testing.T) { + assert.Panics(t, func() { getValidateValues("dive,keys,min=3") }) + }) } func TestGetValidateCurrent(t *testing.T) { From 9e8a9255b71122f0cf21120a4ee5d5adc3dd56b8 Mon Sep 17 00:00:00 2001 From: Ramil Amparo Date: Fri, 3 Apr 2026 18:12:36 +0400 Subject: [PATCH 28/35] Fix tests --- tests/cases.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/cases.ts b/tests/cases.ts index 032133b..3d917bb 100644 --- a/tests/cases.ts +++ b/tests/cases.ts @@ -947,42 +947,42 @@ export const cases: TestCase[] = [ name: "oneof required: accepts valid status", golden: "TestOneofRequired.golden", schema: "PayloadSchema", - input: { status: "active" }, + input: { status: "active", statusImplicitRequired: "active" }, success: true, }, { name: "oneof required: rejects empty status", golden: "TestOneofRequired.golden", schema: "PayloadSchema", - input: { status: "" }, + input: { status: "", statusImplicitRequired: "active" }, success: false, }, { name: "oneof required: rejects invalid status", golden: "TestOneofRequired.golden", schema: "PayloadSchema", - input: { status: "deleted" }, + input: { status: "deleted", statusImplicitRequired: "active" }, success: false, }, { name: "oneof optional: accepts with channel present", golden: "TestOneofRequired.golden", schema: "PayloadSchema", - input: { status: "active", channel: "email" }, + input: { status: "active", statusImplicitRequired: "active", channel: "email" }, success: true, }, { name: "oneof optional: accepts without channel (optional)", golden: "TestOneofRequired.golden", schema: "PayloadSchema", - input: { status: "active" }, + input: { status: "active", statusImplicitRequired: "active" }, success: true, }, { name: "oneof optional: rejects invalid channel", golden: "TestOneofRequired.golden", schema: "PayloadSchema", - input: { status: "active", channel: "phone" }, + input: { status: "active", statusImplicitRequired: "active", channel: "phone" }, success: false, }, From d5cd2eead4a616bf8c91b772222fae2b59648dcc Mon Sep 17 00:00:00 2001 From: Ramil Amparo Date: Fri, 3 Apr 2026 18:29:10 +0400 Subject: [PATCH 29/35] Fix bare dive panic --- zod.go | 6 +++++- zod_test.go | 4 ++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/zod.go b/zod.go index 64c9057..7aae616 100644 --- a/zod.go +++ b/zod.go @@ -984,7 +984,11 @@ func getValidateValues(validate string) string { } validateValues = strings.TrimPrefix(validateValues, ",") } else if strings.Contains(validate, "dive") { - removedPrefix := strings.SplitN(validate, "dive,", 2)[1] + parts := strings.SplitN(validate, "dive,", 2) + if len(parts) < 2 { + return "" + } + removedPrefix := parts[1] if strings.Contains(removedPrefix, ",dive") { validateValues = strings.SplitN(removedPrefix, ",dive", 2)[0] } else { diff --git a/zod_test.go b/zod_test.go index 60a03b4..ad69b9c 100644 --- a/zod_test.go +++ b/zod_test.go @@ -836,6 +836,10 @@ func TestGetValidateValues(t *testing.T) { t.Run("dive keys without endkeys panics", func(t *testing.T) { assert.Panics(t, func() { getValidateValues("dive,keys,min=3") }) }) + + t.Run("bare dive returns empty", func(t *testing.T) { + assert.Equal(t, "", getValidateValues("dive")) + }) } func TestGetValidateCurrent(t *testing.T) { From eb3bd886428540611958b00409f54de9fe279487 Mon Sep 17 00:00:00 2001 From: Ramil Amparo Date: Fri, 3 Apr 2026 18:54:24 +0400 Subject: [PATCH 30/35] Improve tests --- testdata/TestOmitZero.golden | 8 +++ ...re_required_base64_preserves_min(1).golden | 8 +++ tests/cases.ts | 58 +++++++++++++++++++ zod_test.go | 24 ++++++++ 4 files changed, 98 insertions(+) create mode 100644 testdata/TestOmitZero.golden create mode 100644 testdata/TestZodV4Defaults/custom_tag_before_required_base64_preserves_min(1).golden diff --git a/testdata/TestOmitZero.golden b/testdata/TestOmitZero.golden new file mode 100644 index 0000000..602cd0f --- /dev/null +++ b/testdata/TestOmitZero.golden @@ -0,0 +1,8 @@ +// @typecheck +export const PayloadSchema = z.object({ + Name: z.string(), + Nickname: z.string().optional(), + Email: z.string().optional(), +}) +export type Payload = z.infer + diff --git a/testdata/TestZodV4Defaults/custom_tag_before_required_base64_preserves_min(1).golden b/testdata/TestZodV4Defaults/custom_tag_before_required_base64_preserves_min(1).golden new file mode 100644 index 0000000..347aed8 --- /dev/null +++ b/testdata/TestZodV4Defaults/custom_tag_before_required_base64_preserves_min(1).golden @@ -0,0 +1,8 @@ +// @zod-version: v4 +// @typecheck +export const PayloadSchema = z.object({ + Data: z.string().trim().min(1).regex(/^(?:[A-Za-z0-9+\/]{4})*(?:[A-Za-z0-9+\/]{2}==|[A-Za-z0-9+\/]{3}=|[A-Za-z0-9+\/]{4})$/), + Hex: z.string().trim().min(1).regex(/^(0[xX])?[0-9a-fA-F]+$/), +}) +export type Payload = z.infer + diff --git a/tests/cases.ts b/tests/cases.ts index 3d917bb..56a782b 100644 --- a/tests/cases.ts +++ b/tests/cases.ts @@ -1016,6 +1016,64 @@ export const cases: TestCase[] = [ success: false, }, + // --- TestOmitZero --- + { + name: "omitzero: accepts all fields present", + golden: "TestOmitZero.golden", + schema: "PayloadSchema", + input: { Name: "alice", Nickname: "ally", Email: "a@b.com" }, + success: true, + }, + { + name: "omitzero: accepts omitzero fields missing", + golden: "TestOmitZero.golden", + schema: "PayloadSchema", + input: { Name: "alice" }, + success: true, + }, + { + name: "omitzero: rejects required field missing", + golden: "TestOmitZero.golden", + schema: "PayloadSchema", + input: { Nickname: "ally" }, + success: false, + }, + + // --- TestZodV4Defaults/custom_tag_before_required_base64_preserves_min(1) --- + { + name: "base64 with trim and required: accepts valid base64", + golden: + "TestZodV4Defaults/custom_tag_before_required_base64_preserves_min(1).golden", + schema: "PayloadSchema", + input: { Data: "aGVsbG8=", Hex: "deadbeef" }, + success: true, + }, + { + name: "base64 with trim: trims whitespace before validating", + golden: + "TestZodV4Defaults/custom_tag_before_required_base64_preserves_min(1).golden", + schema: "PayloadSchema", + input: { Data: " aGVsbG8= ", Hex: " deadbeef " }, + output: { Data: "aGVsbG8=", Hex: "deadbeef" }, + success: true, + }, + { + name: "base64 with trim and required: rejects empty string", + golden: + "TestZodV4Defaults/custom_tag_before_required_base64_preserves_min(1).golden", + schema: "PayloadSchema", + input: { Data: "", Hex: "deadbeef" }, + success: false, + }, + { + name: "hex with trim and required: rejects empty string", + golden: + "TestZodV4Defaults/custom_tag_before_required_base64_preserves_min(1).golden", + schema: "PayloadSchema", + input: { Data: "aGVsbG8=", Hex: "" }, + success: false, + }, + // --- lenSchema --- { name: "string len: accepts string of length 5", diff --git a/zod_test.go b/zod_test.go index ad69b9c..eead07d 100644 --- a/zod_test.go +++ b/zod_test.go @@ -296,6 +296,15 @@ func TestStringOptionalNullable(t *testing.T) { assertSchema(t, User{}) } +func TestOmitZero(t *testing.T) { + type Payload struct { + Name string + Nickname string `json:",omitzero"` + Email *string `json:",omitzero"` + } + assertSchema(t, Payload{}) +} + func TestStringArrayNullable(t *testing.T) { type User struct { Name string @@ -530,6 +539,21 @@ func TestZodV4Defaults(t *testing.T) { goldenAssert(t, []byte(NewConverterWithOpts(WithCustomTags(customTagHandlers)).Convert(Payload{})), withGoldenZodVersion("v4")) }) + t.Run("custom tag before required base64 preserves min(1)", func(t *testing.T) { + type Payload struct { + Data string `validate:"trim,required,base64"` + Hex string `validate:"trim,required,hexadecimal"` + } + + customTagHandlers := map[string]CustomFn{ + "trim": func(c *Converter, t reflect.Type, validate string, i int) string { + return ".trim()" + }, + } + + goldenAssert(t, []byte(NewConverterWithOpts(WithCustomTags(customTagHandlers)).Convert(Payload{})), withGoldenZodVersion("v4")) + }) + t.Run("ip unions inherit generic string constraints", func(t *testing.T) { type Payload struct { Address string `validate:"ip,required,max=45"` From 72a9fcc3bae560ed659df7de0854d8177856f6fc Mon Sep 17 00:00:00 2001 From: Ramil Amparo Date: Mon, 6 Apr 2026 14:28:35 +0400 Subject: [PATCH 31/35] Run go work sync --- custom/decimal/go.mod | 2 +- custom/decimal/go.sum | 4 ++-- custom/optional/go.mod | 2 +- custom/optional/go.sum | 4 ++-- go.mod | 6 ++++-- go.sum | 2 -- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/custom/decimal/go.mod b/custom/decimal/go.mod index ce2cba2..91d7861 100644 --- a/custom/decimal/go.mod +++ b/custom/decimal/go.mod @@ -7,7 +7,7 @@ replace github.com/hypersequent/zen => ../.. require ( github.com/hypersequent/zen v0.0.0-00010101000000-000000000000 github.com/shopspring/decimal v1.3.1 - github.com/stretchr/testify v1.8.3 + github.com/stretchr/testify v1.9.0 ) require ( diff --git a/custom/decimal/go.sum b/custom/decimal/go.sum index 7dfe67b..3686241 100644 --- a/custom/decimal/go.sum +++ b/custom/decimal/go.sum @@ -4,8 +4,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= -github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= -github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/xorcare/golden v0.8.3 h1:0sFBpM6/ju8YzhN2akrsPTgm6YEuIwuh0JaeAk5Ne3g= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/custom/optional/go.mod b/custom/optional/go.mod index 7a3e146..8072fe1 100644 --- a/custom/optional/go.mod +++ b/custom/optional/go.mod @@ -7,7 +7,7 @@ replace github.com/hypersequent/zen => ../.. require ( 4d63.com/optional v0.2.0 github.com/hypersequent/zen v0.0.0-00010101000000-000000000000 - github.com/stretchr/testify v1.8.3 + github.com/stretchr/testify v1.9.0 ) require ( diff --git a/custom/optional/go.sum b/custom/optional/go.sum index 6caa326..553b289 100644 --- a/custom/optional/go.sum +++ b/custom/optional/go.sum @@ -4,8 +4,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= -github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/xorcare/golden v0.8.3 h1:0sFBpM6/ju8YzhN2akrsPTgm6YEuIwuh0JaeAk5Ne3g= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/go.mod b/go.mod index f8564ec..000d54e 100644 --- a/go.mod +++ b/go.mod @@ -2,11 +2,13 @@ module github.com/hypersequent/zen go 1.23 -require github.com/stretchr/testify v1.9.0 +require ( + github.com/stretchr/testify v1.9.0 + github.com/xorcare/golden v0.8.3 +) require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/xorcare/golden v0.8.3 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index f7850cd..6fbca3d 100644 --- a/go.sum +++ b/go.sum @@ -9,8 +9,6 @@ github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpE github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= -github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= From 1816be76557177cabf327cbcaaf6063ea2f986f8 Mon Sep 17 00:00:00 2001 From: Ramil Amparo Date: Mon, 6 Apr 2026 15:40:02 +0400 Subject: [PATCH 32/35] Use .check() for format validators and always emit .min(1) for required Format validators (email, url, uuid, etc.) now use z.string().check(z.email()) instead of z.email() as the schema base. This fixes ordering bugs where z.email().trim() would validate before trimming, rejecting valid spaced input. With .check(), the z.string() base is preserved so transforms like .trim() chain correctly in any position. The required tag now always emits .min(1) in both v3 and v4, including when combined with format or union validators. Previously it was skipped because format validators reject empty strings, but explicit .min(1) is more correct and consistent. IP unions updated to z.union([z.string().check(z.ipv4()), ...]) for consistency with the .check() approach. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../format_only/v4.golden | 46 +++---- .../format_with_required/v3.golden | 46 +++---- .../format_with_required/v4.golden | 46 +++---- .../TestFormatValidators/union_only/v4.golden | 4 +- .../union_with_required/v3.golden | 4 +- .../union_with_required/v4.golden | 4 +- ...re_required_base64_preserves_min(1).golden | 4 +- ..._inherit_generic_string_constraints.golden | 2 +- .../v4.golden | 2 +- .../string_formats_use_zod_v4_builders.golden | 10 +- ..._preserved_around_v4_format_helpers.golden | 4 +- tests/cases.ts | 42 +++++++ zod.go | 116 +++++------------- zod_test.go | 7 +- 14 files changed, 165 insertions(+), 172 deletions(-) diff --git a/testdata/TestFormatValidators/format_only/v4.golden b/testdata/TestFormatValidators/format_only/v4.golden index 01127bb..96e56b5 100644 --- a/testdata/TestFormatValidators/format_only/v4.golden +++ b/testdata/TestFormatValidators/format_only/v4.golden @@ -1,117 +1,117 @@ // @zod-version: v4 // @typecheck export const emailSchema = z.object({ - value: z.email(), + value: z.string().check(z.email()), }) export type email = z.infer export const urlSchema = z.object({ - value: z.url(), + value: z.string().check(z.url()), }) export type url = z.infer export const http_urlSchema = z.object({ - value: z.httpUrl(), + value: z.string().check(z.httpUrl()), }) export type http_url = z.infer export const ipv4Schema = z.object({ - value: z.ipv4(), + value: z.string().check(z.ipv4()), }) export type ipv4 = z.infer export const ip4_addrSchema = z.object({ - value: z.ipv4(), + value: z.string().check(z.ipv4()), }) export type ip4_addr = z.infer export const ipv6Schema = z.object({ - value: z.ipv6(), + value: z.string().check(z.ipv6()), }) export type ipv6 = z.infer export const ip6_addrSchema = z.object({ - value: z.ipv6(), + value: z.string().check(z.ipv6()), }) export type ip6_addr = z.infer export const base64Schema = z.object({ - value: z.base64(), + value: z.string().check(z.base64()), }) export type base64 = z.infer export const datetimeSchema = z.object({ - value: z.iso.datetime(), + value: z.string().check(z.iso.datetime()), }) export type datetime = z.infer export const hexadecimalSchema = z.object({ - value: z.hex(), + value: z.string().check(z.hex()), }) export type hexadecimal = z.infer export const jwtSchema = z.object({ - value: z.jwt(), + value: z.string().check(z.jwt()), }) export type jwt = z.infer export const uuidSchema = z.object({ - value: z.uuid(), + value: z.string().check(z.uuid()), }) export type uuid = z.infer export const uuid3Schema = z.object({ - value: z.uuid({ version: "v3" }), + value: z.string().check(z.uuid({ version: "v3" })), }) export type uuid3 = z.infer export const uuid3_rfc4122Schema = z.object({ - value: z.uuid({ version: "v3" }), + value: z.string().check(z.uuid({ version: "v3" })), }) export type uuid3_rfc4122 = z.infer export const uuid4Schema = z.object({ - value: z.uuid({ version: "v4" }), + value: z.string().check(z.uuid({ version: "v4" })), }) export type uuid4 = z.infer export const uuid4_rfc4122Schema = z.object({ - value: z.uuid({ version: "v4" }), + value: z.string().check(z.uuid({ version: "v4" })), }) export type uuid4_rfc4122 = z.infer export const uuid5Schema = z.object({ - value: z.uuid({ version: "v5" }), + value: z.string().check(z.uuid({ version: "v5" })), }) export type uuid5 = z.infer export const uuid5_rfc4122Schema = z.object({ - value: z.uuid({ version: "v5" }), + value: z.string().check(z.uuid({ version: "v5" })), }) export type uuid5_rfc4122 = z.infer export const uuid_rfc4122Schema = z.object({ - value: z.uuid(), + value: z.string().check(z.uuid()), }) export type uuid_rfc4122 = z.infer export const md5Schema = z.object({ - value: z.hash("md5"), + value: z.string().check(z.hash("md5")), }) export type md5 = z.infer export const sha256Schema = z.object({ - value: z.hash("sha256"), + value: z.string().check(z.hash("sha256")), }) export type sha256 = z.infer export const sha384Schema = z.object({ - value: z.hash("sha384"), + value: z.string().check(z.hash("sha384")), }) export type sha384 = z.infer export const sha512Schema = z.object({ - value: z.hash("sha512"), + value: z.string().check(z.hash("sha512")), }) export type sha512 = z.infer diff --git a/testdata/TestFormatValidators/format_with_required/v3.golden b/testdata/TestFormatValidators/format_with_required/v3.golden index 6c3af71..8aceef1 100644 --- a/testdata/TestFormatValidators/format_with_required/v3.golden +++ b/testdata/TestFormatValidators/format_with_required/v3.golden @@ -1,117 +1,117 @@ // @zod-version: v3 // @typecheck export const emailSchema = z.object({ - value: z.string().email(), + value: z.string().min(1).email(), }) export type email = z.infer export const urlSchema = z.object({ - value: z.string().url(), + value: z.string().min(1).url(), }) export type url = z.infer export const http_urlSchema = z.object({ - value: z.string().url(), + value: z.string().min(1).url(), }) export type http_url = z.infer export const ipv4Schema = z.object({ - value: z.string().ip({ version: "v4" }), + value: z.string().min(1).ip({ version: "v4" }), }) export type ipv4 = z.infer export const ip4_addrSchema = z.object({ - value: z.string().ip({ version: "v4" }), + value: z.string().min(1).ip({ version: "v4" }), }) export type ip4_addr = z.infer export const ipv6Schema = z.object({ - value: z.string().ip({ version: "v6" }), + value: z.string().min(1).ip({ version: "v6" }), }) export type ipv6 = z.infer export const ip6_addrSchema = z.object({ - value: z.string().ip({ version: "v6" }), + value: z.string().min(1).ip({ version: "v6" }), }) export type ip6_addr = z.infer export const base64Schema = z.object({ - value: z.string().regex(/^(?:[A-Za-z0-9+\/]{4})*(?:[A-Za-z0-9+\/]{2}==|[A-Za-z0-9+\/]{3}=|[A-Za-z0-9+\/]{4})$/), + value: z.string().min(1).regex(/^(?:[A-Za-z0-9+\/]{4})*(?:[A-Za-z0-9+\/]{2}==|[A-Za-z0-9+\/]{3}=|[A-Za-z0-9+\/]{4})$/), }) export type base64 = z.infer export const datetimeSchema = z.object({ - value: z.string().datetime(), + value: z.string().min(1).datetime(), }) export type datetime = z.infer export const hexadecimalSchema = z.object({ - value: z.string().regex(/^(0[xX])?[0-9a-fA-F]+$/), + value: z.string().min(1).regex(/^(0[xX])?[0-9a-fA-F]+$/), }) export type hexadecimal = z.infer export const jwtSchema = z.object({ - value: z.string().regex(/^[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]*$/), + value: z.string().min(1).regex(/^[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]*$/), }) export type jwt = z.infer export const uuidSchema = z.object({ - value: z.string().regex(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/), + value: z.string().min(1).regex(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/), }) export type uuid = z.infer export const uuid3Schema = z.object({ - value: z.string().regex(/^[0-9a-f]{8}-[0-9a-f]{4}-3[0-9a-f]{3}-[0-9a-f]{4}-[0-9a-f]{12}$/), + value: z.string().min(1).regex(/^[0-9a-f]{8}-[0-9a-f]{4}-3[0-9a-f]{3}-[0-9a-f]{4}-[0-9a-f]{12}$/), }) export type uuid3 = z.infer export const uuid3_rfc4122Schema = z.object({ - value: z.string().regex(/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-3[0-9a-fA-F]{3}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/), + value: z.string().min(1).regex(/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-3[0-9a-fA-F]{3}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/), }) export type uuid3_rfc4122 = z.infer export const uuid4Schema = z.object({ - value: z.string().regex(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/), + value: z.string().min(1).regex(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/), }) export type uuid4 = z.infer export const uuid4_rfc4122Schema = z.object({ - value: z.string().regex(/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$/), + value: z.string().min(1).regex(/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$/), }) export type uuid4_rfc4122 = z.infer export const uuid5Schema = z.object({ - value: z.string().regex(/^[0-9a-f]{8}-[0-9a-f]{4}-5[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/), + value: z.string().min(1).regex(/^[0-9a-f]{8}-[0-9a-f]{4}-5[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/), }) export type uuid5 = z.infer export const uuid5_rfc4122Schema = z.object({ - value: z.string().regex(/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-5[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$/), + value: z.string().min(1).regex(/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-5[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$/), }) export type uuid5_rfc4122 = z.infer export const uuid_rfc4122Schema = z.object({ - value: z.string().regex(/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/), + value: z.string().min(1).regex(/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/), }) export type uuid_rfc4122 = z.infer export const md5Schema = z.object({ - value: z.string().regex(/^[0-9a-f]{32}$/), + value: z.string().min(1).regex(/^[0-9a-f]{32}$/), }) export type md5 = z.infer export const sha256Schema = z.object({ - value: z.string().regex(/^[0-9a-f]{64}$/), + value: z.string().min(1).regex(/^[0-9a-f]{64}$/), }) export type sha256 = z.infer export const sha384Schema = z.object({ - value: z.string().regex(/^[0-9a-f]{96}$/), + value: z.string().min(1).regex(/^[0-9a-f]{96}$/), }) export type sha384 = z.infer export const sha512Schema = z.object({ - value: z.string().regex(/^[0-9a-f]{128}$/), + value: z.string().min(1).regex(/^[0-9a-f]{128}$/), }) export type sha512 = z.infer diff --git a/testdata/TestFormatValidators/format_with_required/v4.golden b/testdata/TestFormatValidators/format_with_required/v4.golden index a2cdf46..7cf7050 100644 --- a/testdata/TestFormatValidators/format_with_required/v4.golden +++ b/testdata/TestFormatValidators/format_with_required/v4.golden @@ -1,117 +1,117 @@ // @zod-version: v4 // @typecheck export const emailSchema = z.object({ - value: z.email(), + value: z.string().min(1).check(z.email()), }) export type email = z.infer export const urlSchema = z.object({ - value: z.url(), + value: z.string().min(1).check(z.url()), }) export type url = z.infer export const http_urlSchema = z.object({ - value: z.httpUrl(), + value: z.string().min(1).check(z.httpUrl()), }) export type http_url = z.infer export const ipv4Schema = z.object({ - value: z.ipv4(), + value: z.string().min(1).check(z.ipv4()), }) export type ipv4 = z.infer export const ip4_addrSchema = z.object({ - value: z.ipv4(), + value: z.string().min(1).check(z.ipv4()), }) export type ip4_addr = z.infer export const ipv6Schema = z.object({ - value: z.ipv6(), + value: z.string().min(1).check(z.ipv6()), }) export type ipv6 = z.infer export const ip6_addrSchema = z.object({ - value: z.ipv6(), + value: z.string().min(1).check(z.ipv6()), }) export type ip6_addr = z.infer export const base64Schema = z.object({ - value: z.base64().min(1), + value: z.string().min(1).check(z.base64()), }) export type base64 = z.infer export const datetimeSchema = z.object({ - value: z.iso.datetime(), + value: z.string().min(1).check(z.iso.datetime()), }) export type datetime = z.infer export const hexadecimalSchema = z.object({ - value: z.hex().min(1), + value: z.string().min(1).check(z.hex()), }) export type hexadecimal = z.infer export const jwtSchema = z.object({ - value: z.jwt(), + value: z.string().min(1).check(z.jwt()), }) export type jwt = z.infer export const uuidSchema = z.object({ - value: z.uuid(), + value: z.string().min(1).check(z.uuid()), }) export type uuid = z.infer export const uuid3Schema = z.object({ - value: z.uuid({ version: "v3" }), + value: z.string().min(1).check(z.uuid({ version: "v3" })), }) export type uuid3 = z.infer export const uuid3_rfc4122Schema = z.object({ - value: z.uuid({ version: "v3" }), + value: z.string().min(1).check(z.uuid({ version: "v3" })), }) export type uuid3_rfc4122 = z.infer export const uuid4Schema = z.object({ - value: z.uuid({ version: "v4" }), + value: z.string().min(1).check(z.uuid({ version: "v4" })), }) export type uuid4 = z.infer export const uuid4_rfc4122Schema = z.object({ - value: z.uuid({ version: "v4" }), + value: z.string().min(1).check(z.uuid({ version: "v4" })), }) export type uuid4_rfc4122 = z.infer export const uuid5Schema = z.object({ - value: z.uuid({ version: "v5" }), + value: z.string().min(1).check(z.uuid({ version: "v5" })), }) export type uuid5 = z.infer export const uuid5_rfc4122Schema = z.object({ - value: z.uuid({ version: "v5" }), + value: z.string().min(1).check(z.uuid({ version: "v5" })), }) export type uuid5_rfc4122 = z.infer export const uuid_rfc4122Schema = z.object({ - value: z.uuid(), + value: z.string().min(1).check(z.uuid()), }) export type uuid_rfc4122 = z.infer export const md5Schema = z.object({ - value: z.hash("md5"), + value: z.string().min(1).check(z.hash("md5")), }) export type md5 = z.infer export const sha256Schema = z.object({ - value: z.hash("sha256"), + value: z.string().min(1).check(z.hash("sha256")), }) export type sha256 = z.infer export const sha384Schema = z.object({ - value: z.hash("sha384"), + value: z.string().min(1).check(z.hash("sha384")), }) export type sha384 = z.infer export const sha512Schema = z.object({ - value: z.hash("sha512"), + value: z.string().min(1).check(z.hash("sha512")), }) export type sha512 = z.infer diff --git a/testdata/TestFormatValidators/union_only/v4.golden b/testdata/TestFormatValidators/union_only/v4.golden index 47d8ae1..83dc5b7 100644 --- a/testdata/TestFormatValidators/union_only/v4.golden +++ b/testdata/TestFormatValidators/union_only/v4.golden @@ -1,12 +1,12 @@ // @zod-version: v4 // @typecheck export const ipSchema = z.object({ - value: z.union([z.ipv4(), z.ipv6()]), + value: z.union([z.string().check(z.ipv4()), z.string().check(z.ipv6())]), }) export type ip = z.infer export const ip_addrSchema = z.object({ - value: z.union([z.ipv4(), z.ipv6()]), + value: z.union([z.string().check(z.ipv4()), z.string().check(z.ipv6())]), }) export type ip_addr = z.infer diff --git a/testdata/TestFormatValidators/union_with_required/v3.golden b/testdata/TestFormatValidators/union_with_required/v3.golden index 0d4bbef..de44142 100644 --- a/testdata/TestFormatValidators/union_with_required/v3.golden +++ b/testdata/TestFormatValidators/union_with_required/v3.golden @@ -1,12 +1,12 @@ // @zod-version: v3 // @typecheck export const ipSchema = z.object({ - value: z.string().ip(), + value: z.string().min(1).ip(), }) export type ip = z.infer export const ip_addrSchema = z.object({ - value: z.string().ip(), + value: z.string().min(1).ip(), }) export type ip_addr = z.infer diff --git a/testdata/TestFormatValidators/union_with_required/v4.golden b/testdata/TestFormatValidators/union_with_required/v4.golden index 47d8ae1..df29b72 100644 --- a/testdata/TestFormatValidators/union_with_required/v4.golden +++ b/testdata/TestFormatValidators/union_with_required/v4.golden @@ -1,12 +1,12 @@ // @zod-version: v4 // @typecheck export const ipSchema = z.object({ - value: z.union([z.ipv4(), z.ipv6()]), + value: z.union([z.string().check(z.ipv4()).min(1), z.string().check(z.ipv6()).min(1)]), }) export type ip = z.infer export const ip_addrSchema = z.object({ - value: z.union([z.ipv4(), z.ipv6()]), + value: z.union([z.string().check(z.ipv4()).min(1), z.string().check(z.ipv6()).min(1)]), }) export type ip_addr = z.infer diff --git a/testdata/TestZodV4Defaults/custom_tag_before_required_base64_preserves_min(1).golden b/testdata/TestZodV4Defaults/custom_tag_before_required_base64_preserves_min(1).golden index 347aed8..a203d82 100644 --- a/testdata/TestZodV4Defaults/custom_tag_before_required_base64_preserves_min(1).golden +++ b/testdata/TestZodV4Defaults/custom_tag_before_required_base64_preserves_min(1).golden @@ -1,8 +1,8 @@ // @zod-version: v4 // @typecheck export const PayloadSchema = z.object({ - Data: z.string().trim().min(1).regex(/^(?:[A-Za-z0-9+\/]{4})*(?:[A-Za-z0-9+\/]{2}==|[A-Za-z0-9+\/]{3}=|[A-Za-z0-9+\/]{4})$/), - Hex: z.string().trim().min(1).regex(/^(0[xX])?[0-9a-fA-F]+$/), + Data: z.string().trim().min(1).check(z.base64()), + Hex: z.string().trim().min(1).check(z.hex()), }) export type Payload = z.infer diff --git a/testdata/TestZodV4Defaults/ip_unions_inherit_generic_string_constraints.golden b/testdata/TestZodV4Defaults/ip_unions_inherit_generic_string_constraints.golden index 90fda32..a714864 100644 --- a/testdata/TestZodV4Defaults/ip_unions_inherit_generic_string_constraints.golden +++ b/testdata/TestZodV4Defaults/ip_unions_inherit_generic_string_constraints.golden @@ -1,7 +1,7 @@ // @zod-version: v4 // @typecheck export const PayloadSchema = z.object({ - Address: z.union([z.ipv4().refine((val) => [...val].length <= 45, 'String must contain at most 45 character(s)'), z.ipv6().refine((val) => [...val].length <= 45, 'String must contain at most 45 character(s)')]), + Address: z.union([z.string().check(z.ipv4()).min(1).refine((val) => [...val].length <= 45, 'String must contain at most 45 character(s)'), z.string().check(z.ipv6()).min(1).refine((val) => [...val].length <= 45, 'String must contain at most 45 character(s)')]), }) export type Payload = z.infer diff --git a/testdata/TestZodV4Defaults/optional_format_with_nullable_pointer/v4.golden b/testdata/TestZodV4Defaults/optional_format_with_nullable_pointer/v4.golden index c5154ba..ed5ed5a 100644 --- a/testdata/TestZodV4Defaults/optional_format_with_nullable_pointer/v4.golden +++ b/testdata/TestZodV4Defaults/optional_format_with_nullable_pointer/v4.golden @@ -1,7 +1,7 @@ // @zod-version: v4 // @typecheck export const PayloadSchema = z.object({ - email: z.email().nullable(), + email: z.string().check(z.email()).nullable(), }) export type Payload = z.infer diff --git a/testdata/TestZodV4Defaults/string_formats_use_zod_v4_builders.golden b/testdata/TestZodV4Defaults/string_formats_use_zod_v4_builders.golden index f3f08b6..f3bbb37 100644 --- a/testdata/TestZodV4Defaults/string_formats_use_zod_v4_builders.golden +++ b/testdata/TestZodV4Defaults/string_formats_use_zod_v4_builders.golden @@ -1,11 +1,11 @@ // @zod-version: v4 // @typecheck export const PayloadSchema = z.object({ - Email: z.email(), - Link: z.httpUrl(), - Base64: z.base64(), - ID: z.uuid({ version: "v4" }), - Checksum: z.hash("md5"), + Email: z.string().check(z.email()), + Link: z.string().check(z.httpUrl()), + Base64: z.string().check(z.base64()), + ID: z.string().check(z.uuid({ version: "v4" })), + Checksum: z.string().check(z.hash("md5")), }) export type Payload = z.infer diff --git a/testdata/TestZodV4Defaults/string_tag_order_is_preserved_around_v4_format_helpers.golden b/testdata/TestZodV4Defaults/string_tag_order_is_preserved_around_v4_format_helpers.golden index 291cf68..3e5ad3f 100644 --- a/testdata/TestZodV4Defaults/string_tag_order_is_preserved_around_v4_format_helpers.golden +++ b/testdata/TestZodV4Defaults/string_tag_order_is_preserved_around_v4_format_helpers.golden @@ -1,8 +1,8 @@ // @zod-version: v4 // @typecheck export const PayloadSchema = z.object({ - TrimmedThenEmail: z.string().trim().email(), - EmailThenTrimmed: z.email().trim(), + TrimmedThenEmail: z.string().trim().check(z.email()), + EmailThenTrimmed: z.string().check(z.email()).trim(), }) export type Payload = z.infer diff --git a/tests/cases.ts b/tests/cases.ts index 56a782b..efe5292 100644 --- a/tests/cases.ts +++ b/tests/cases.ts @@ -1797,4 +1797,46 @@ export const cases: TestCase[] = [ input: { Name: "John", Email: null }, success: true, }, + + // --------------------------------------------------------------------------- + // STRING TAG ORDER WITH FORMAT HELPERS (trim + email) + // --------------------------------------------------------------------------- + + // --- TestZodV4Defaults/string_tag_order_is_preserved_around_v4_format_helpers --- + { + name: "trim then email: valid email passes", + golden: "TestZodV4Defaults/string_tag_order_is_preserved_around_v4_format_helpers.golden", + schema: "PayloadSchema", + input: { TrimmedThenEmail: "user@example.com", EmailThenTrimmed: "user@example.com" }, + success: true, + }, + { + name: "trim then email: invalid email rejects", + golden: "TestZodV4Defaults/string_tag_order_is_preserved_around_v4_format_helpers.golden", + schema: "PayloadSchema", + input: { TrimmedThenEmail: "not-an-email", EmailThenTrimmed: "user@example.com" }, + success: false, + }, + { + name: "email then trimmed: invalid email rejects", + golden: "TestZodV4Defaults/string_tag_order_is_preserved_around_v4_format_helpers.golden", + schema: "PayloadSchema", + input: { TrimmedThenEmail: "user@example.com", EmailThenTrimmed: "not-an-email" }, + success: false, + }, + { + name: "trim then email: spaces trimmed before validation passes", + golden: "TestZodV4Defaults/string_tag_order_is_preserved_around_v4_format_helpers.golden", + schema: "PayloadSchema", + input: { TrimmedThenEmail: " user@example.com ", EmailThenTrimmed: "user@example.com" }, + success: true, + output: { TrimmedThenEmail: "user@example.com", EmailThenTrimmed: "user@example.com" }, + }, + { + name: "email then trimmed: spaces cause check to fail before trim runs", + golden: "TestZodV4Defaults/string_tag_order_is_preserved_around_v4_format_helpers.golden", + schema: "PayloadSchema", + input: { TrimmedThenEmail: "user@example.com", EmailThenTrimmed: " user@example.com " }, + success: false, + }, ]; diff --git a/zod.go b/zod.go index 7aae616..d285a4e 100644 --- a/zod.go +++ b/zod.go @@ -1089,12 +1089,6 @@ var unionTags = map[string]bool{ "ip": true, "ip_addr": true, } -// Tags where generated Zod schemas accepts an empty string -// unless `.min(1)` is added. -var v4AcceptsEmpty = map[string]bool{ - "base64": true, "hexadecimal": true, -} - func (c *Converter) validateString(validate string) string { validators := c.parseStringValidators(validate) return c.renderStringSchema(validators) @@ -1162,25 +1156,17 @@ func (c *Converter) renderStringSchema(validators []stringValidator) string { // Phase 1: Classify validators hasFormat := false hasUnion := false - hasRequired := false hasEnum := false - formatIdx := -1 formatCount := 0 - for i, v := range validators { + for _, v := range validators { if formatTags[v.tag] { hasFormat = true formatCount++ - if formatIdx == -1 { - formatIdx = i - } } if unionTags[v.tag] { hasUnion = true } - if v.tag == "required" { - hasRequired = true - } if v.tag == "oneof" || v.tag == "boolean" { hasEnum = true } @@ -1205,14 +1191,8 @@ func (c *Converter) renderStringSchema(validators []stringValidator) string { // Phase 4: Render v3 if c.zodV3 { - // Skip required when a format or union is present — format validators - // already reject empty strings in both v3 and v4. - skipRequired := hasFormat || hasUnion var chain strings.Builder for _, v := range validators { - if v.tag == "required" && skipRequired { - continue - } chain.WriteString(c.renderV3Chain(v)) } return "z.string()" + chain.String() @@ -1224,60 +1204,26 @@ func (c *Converter) renderStringSchema(validators []stringValidator) string { if hasUnion { var armChain strings.Builder for _, v := range validators { - if v.tag == "required" || unionTags[v.tag] { + if unionTags[v.tag] { continue } armChain.WriteString(renderChain(v)) } ac := armChain.String() - return fmt.Sprintf("z.union([z.ipv4()%s, z.ipv6()%s])", ac, ac) + return fmt.Sprintf("z.union([z.string().check(z.ipv4())%s, z.string().check(z.ipv6())%s])", ac, ac) } - // Case 2: Format present + // Case 2: Format present — use z.string() base with .check() for format if hasFormat { - // Check if anything (non-required, non-omitempty) precedes the format - hasTransformBefore := false - for i := 0; i < formatIdx; i++ { - v := validators[i] - if v.tag != "required" && v.tag != "omitempty" { - hasTransformBefore = true - break - } - } - - // Determine if required should be kept (base64/hex accept empty in v4) - keepRequired := hasRequired && v4AcceptsEmpty[validators[formatIdx].tag] - - if hasTransformBefore { - // Fall back to z.string() + chains (format becomes a chain method via v3 form) - var chain strings.Builder - for _, v := range validators { - if v.tag == "required" && !keepRequired { - continue - } - if formatTags[v.tag] { - chain.WriteString(c.renderV3Chain(v)) - } else { - chain.WriteString(renderChain(v)) - } - } - return "z.string()" + chain.String() - } - - // Format as base - base := c.renderV4FormatBase(validators[formatIdx]) var chain strings.Builder - if keepRequired { - chain.WriteString(".min(1)") - } - for i := formatIdx + 1; i < len(validators); i++ { - v := validators[i] - if v.tag == "required" && !keepRequired { - continue + for _, v := range validators { + if formatTags[v.tag] { + chain.WriteString(c.renderV4FormatCheck(v)) + } else { + chain.WriteString(renderChain(v)) } - chain.WriteString(renderChain(v)) } - return base + chain.String() + return "z.string()" + chain.String() } // Case 3: No format/union — plain string @@ -1399,7 +1345,7 @@ func renderChain(v stringValidator) string { return v.arg default: // Format/union tags (email, url, ip, etc.) are handled by - // renderV3Chain or renderV4FormatBase, not here. + // renderV3Chain or renderV4FormatCheck, not here. if !formatTags[v.tag] && !unionTags[v.tag] { panic(fmt.Sprintf("renderChain: unhandled tag %q", v.tag)) } @@ -1457,46 +1403,46 @@ func (c *Converter) renderV3Chain(v stringValidator) string { } } -func (c *Converter) renderV4FormatBase(v stringValidator) string { +func (c *Converter) renderV4FormatCheck(v stringValidator) string { switch v.tag { case "email": - return "z.email()" + return ".check(z.email())" case "url": - return "z.url()" + return ".check(z.url())" case "http_url": - return "z.httpUrl()" + return ".check(z.httpUrl())" case "ipv4", "ip4_addr": - return "z.ipv4()" + return ".check(z.ipv4())" case "ipv6", "ip6_addr": - return "z.ipv6()" + return ".check(z.ipv6())" case "base64": - return "z.base64()" + return ".check(z.base64())" case "datetime": - return "z.iso.datetime()" + return ".check(z.iso.datetime())" case "hexadecimal": - return "z.hex()" + return ".check(z.hex())" case "jwt": - return "z.jwt()" + return ".check(z.jwt())" case "uuid": - return "z.uuid()" + return ".check(z.uuid())" case "uuid3", "uuid3_rfc4122": - return `z.uuid({ version: "v3" })` + return `.check(z.uuid({ version: "v3" }))` case "uuid4", "uuid4_rfc4122": - return `z.uuid({ version: "v4" })` + return `.check(z.uuid({ version: "v4" }))` case "uuid5", "uuid5_rfc4122": - return `z.uuid({ version: "v5" })` + return `.check(z.uuid({ version: "v5" }))` case "uuid_rfc4122": - return "z.uuid()" + return ".check(z.uuid())" case "md5": - return `z.hash("md5")` + return `.check(z.hash("md5"))` case "sha256": - return `z.hash("sha256")` + return `.check(z.hash("sha256"))` case "sha384": - return `z.hash("sha384")` + return `.check(z.hash("sha384"))` case "sha512": - return `z.hash("sha512")` + return `.check(z.hash("sha512"))` default: - panic(fmt.Sprintf("renderV4FormatBase: unhandled format tag %q", v.tag)) + panic(fmt.Sprintf("renderV4FormatCheck: unhandled format tag %q", v.tag)) } } diff --git a/zod_test.go b/zod_test.go index eead07d..8401363 100644 --- a/zod_test.go +++ b/zod_test.go @@ -536,7 +536,12 @@ func TestZodV4Defaults(t *testing.T) { }, } - goldenAssert(t, []byte(NewConverterWithOpts(WithCustomTags(customTagHandlers)).Convert(Payload{})), withGoldenZodVersion("v4")) + goldenAssert( + t, + []byte(NewConverterWithOpts(WithCustomTags(customTagHandlers)).Convert(Payload{})), + withGoldenZodVersion("v3"), + withGoldenZodVersion("v4"), + ) }) t.Run("custom tag before required base64 preserves min(1)", func(t *testing.T) { From 6a39cb1c47a1fe8a2a3f12ebd33b3ded4026ffcc Mon Sep 17 00:00:00 2001 From: Ramil Amparo Date: Mon, 6 Apr 2026 16:03:16 +0400 Subject: [PATCH 33/35] Run go work sync --- custom/decimal/go.mod | 3 ++- custom/decimal/go.sum | 7 +++++-- custom/optional/go.mod | 3 ++- custom/optional/go.sum | 7 +++++-- go.mod | 5 ++++- go.sum | 7 +++++-- go.work.sum | 12 +++++++++++- 7 files changed, 34 insertions(+), 10 deletions(-) diff --git a/custom/decimal/go.mod b/custom/decimal/go.mod index 91d7861..f2d3fc6 100644 --- a/custom/decimal/go.mod +++ b/custom/decimal/go.mod @@ -7,11 +7,12 @@ replace github.com/hypersequent/zen => ../.. require ( github.com/hypersequent/zen v0.0.0-00010101000000-000000000000 github.com/shopspring/decimal v1.3.1 - github.com/stretchr/testify v1.9.0 + github.com/stretchr/testify v1.11.1 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect + github.com/kr/text v0.2.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/custom/decimal/go.sum b/custom/decimal/go.sum index 3686241..241d742 100644 --- a/custom/decimal/go.sum +++ b/custom/decimal/go.sum @@ -1,12 +1,15 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/xorcare/golden v0.8.3 h1:0sFBpM6/ju8YzhN2akrsPTgm6YEuIwuh0JaeAk5Ne3g= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/custom/optional/go.mod b/custom/optional/go.mod index 8072fe1..1cc3b96 100644 --- a/custom/optional/go.mod +++ b/custom/optional/go.mod @@ -7,11 +7,12 @@ replace github.com/hypersequent/zen => ../.. require ( 4d63.com/optional v0.2.0 github.com/hypersequent/zen v0.0.0-00010101000000-000000000000 - github.com/stretchr/testify v1.9.0 + github.com/stretchr/testify v1.11.1 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect + github.com/kr/text v0.2.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/custom/optional/go.sum b/custom/optional/go.sum index 553b289..c2aa4cc 100644 --- a/custom/optional/go.sum +++ b/custom/optional/go.sum @@ -2,11 +2,14 @@ 4d63.com/optional v0.2.0/go.mod h1:DBA8tAdkYkYbvRq1lK3FyDBBzioAJzZzQPC6Vj+a3jk= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/xorcare/golden v0.8.3 h1:0sFBpM6/ju8YzhN2akrsPTgm6YEuIwuh0JaeAk5Ne3g= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/go.mod b/go.mod index 000d54e..5990e81 100644 --- a/go.mod +++ b/go.mod @@ -3,12 +3,15 @@ module github.com/hypersequent/zen go 1.23 require ( - github.com/stretchr/testify v1.9.0 + github.com/stretchr/testify v1.11.1 github.com/xorcare/golden v0.8.3 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect + github.com/kr/pretty v0.3.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/rogpeppe/go-internal v1.14.1 // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 6fbca3d..9a44efd 100644 --- a/go.sum +++ b/go.sum @@ -1,8 +1,11 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -10,12 +13,12 @@ github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/xorcare/golden v0.8.3 h1:0sFBpM6/ju8YzhN2akrsPTgm6YEuIwuh0JaeAk5Ne3g= github.com/xorcare/golden v0.8.3/go.mod h1:lRw6LV+0Pp37EBDMR4sXIz4Y7r75dDZ6bYm0ILDpIHY= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/go.work.sum b/go.work.sum index 6e91cd0..1721b10 100644 --- a/go.work.sum +++ b/go.work.sum @@ -1,2 +1,12 @@ +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= From a5cf02037bd845875b25007f3c24c3e6f258c2e1 Mon Sep 17 00:00:00 2001 From: Ramil Amparo Date: Mon, 6 Apr 2026 16:23:40 +0400 Subject: [PATCH 34/35] Run go work sync --- go.work.sum | 3 +++ 1 file changed, 3 insertions(+) diff --git a/go.work.sum b/go.work.sum index 1721b10..3895189 100644 --- a/go.work.sum +++ b/go.work.sum @@ -9,4 +9,7 @@ github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/f github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= From b5c8f8575b73fecb8cdecc3156f229e7269ad8cc Mon Sep 17 00:00:00 2001 From: Ramil Amparo Date: Mon, 6 Apr 2026 17:14:51 +0400 Subject: [PATCH 35/35] More edge case tests --- .../v3.golden | 8 + .../v4.golden} | 0 tests/cases.ts | 140 ++++++++++++------ tests/golden.test.ts | 55 ++++--- zod_test.go | 116 ++++++--------- 5 files changed, 185 insertions(+), 134 deletions(-) create mode 100644 testdata/TestStringValidations/string_tag_order_is_preserved_around_v4_format_helpers/v3.golden rename testdata/{TestZodV4Defaults/string_tag_order_is_preserved_around_v4_format_helpers.golden => TestStringValidations/string_tag_order_is_preserved_around_v4_format_helpers/v4.golden} (100%) diff --git a/testdata/TestStringValidations/string_tag_order_is_preserved_around_v4_format_helpers/v3.golden b/testdata/TestStringValidations/string_tag_order_is_preserved_around_v4_format_helpers/v3.golden new file mode 100644 index 0000000..b3600f8 --- /dev/null +++ b/testdata/TestStringValidations/string_tag_order_is_preserved_around_v4_format_helpers/v3.golden @@ -0,0 +1,8 @@ +// @zod-version: v3 +// @typecheck +export const PayloadSchema = z.object({ + TrimmedThenEmail: z.string().trim().email(), + EmailThenTrimmed: z.string().email().trim(), +}) +export type Payload = z.infer + diff --git a/testdata/TestZodV4Defaults/string_tag_order_is_preserved_around_v4_format_helpers.golden b/testdata/TestStringValidations/string_tag_order_is_preserved_around_v4_format_helpers/v4.golden similarity index 100% rename from testdata/TestZodV4Defaults/string_tag_order_is_preserved_around_v4_format_helpers.golden rename to testdata/TestStringValidations/string_tag_order_is_preserved_around_v4_format_helpers/v4.golden diff --git a/tests/cases.ts b/tests/cases.ts index efe5292..c24b782 100644 --- a/tests/cases.ts +++ b/tests/cases.ts @@ -11,7 +11,7 @@ export interface TestCase { /** Description of what this test verifies */ name: string; - /** Path to golden file relative to testdata/ */ + /** Path to golden file relative to testdata/, or a directory containing v3.golden */ golden: string; /** Name of the exported schema to test (e.g. "UserSchema") */ schema: string; @@ -201,7 +201,6 @@ export const cases: TestCase[] = [ success: false, }, - // --------------------------------------------------------------------------- // ARRAYS // --------------------------------------------------------------------------- @@ -765,7 +764,7 @@ export const cases: TestCase[] = [ // --- TestNestedStruct/v4 --- { name: "nested struct v4: parses valid object with spread shapes", - golden: "TestNestedStruct/v4.golden", + golden: "TestNestedStruct", schema: "UserSchema", input: { Tags: ["a", "b"], ID: "123", name: "John" }, success: true, @@ -774,7 +773,7 @@ export const cases: TestCase[] = [ // --- TestRecursive1/v4 --- { name: "recursive1 v4: parses nested children", - golden: "TestRecursive1/v4.golden", + golden: "TestRecursive1", schema: "NestedItemSchema", input: { id: 1, @@ -799,7 +798,7 @@ export const cases: TestCase[] = [ // --- TestRecursive2/v4 --- { name: "recursive2 v4: parses ParentSchema with nested next", - golden: "TestRecursive2/v4.golden", + golden: "TestRecursive2", schema: "ParentSchema", input: { child: { @@ -816,7 +815,7 @@ export const cases: TestCase[] = [ // --- TestRecursiveEmbeddedStruct/v4 --- { name: "recursive embedded v4: parses ItemBSchema", - golden: "TestRecursiveEmbeddedStruct/v4.golden", + golden: "TestRecursiveEmbeddedStruct", schema: "ItemBSchema", input: { Name: "root", @@ -832,7 +831,7 @@ export const cases: TestCase[] = [ { name: "recursive with dates v4: parses TreeSchema", golden: - "TestRecursiveEmbeddedWithPointersAndDates/recursive_struct_with_pointer_field_and_date/v4.golden", + "TestRecursiveEmbeddedWithPointersAndDates/recursive_struct_with_pointer_field_and_date", schema: "TreeSchema", input: { UpdatedAt: "2021-01-01T00:00:00Z", @@ -865,7 +864,7 @@ export const cases: TestCase[] = [ { name: "embedded self-pointer with dates v4: parses ArticleSchema", golden: - "TestRecursiveEmbeddedWithPointersAndDates/embedded_struct_with_pointer_to_self_and_date/v4.golden", + "TestRecursiveEmbeddedWithPointersAndDates/embedded_struct_with_pointer_to_self_and_date", schema: "ArticleSchema", input: { Title: "Article", @@ -968,7 +967,11 @@ export const cases: TestCase[] = [ name: "oneof optional: accepts with channel present", golden: "TestOneofRequired.golden", schema: "PayloadSchema", - input: { status: "active", statusImplicitRequired: "active", channel: "email" }, + input: { + status: "active", + statusImplicitRequired: "active", + channel: "email", + }, success: true, }, { @@ -982,7 +985,11 @@ export const cases: TestCase[] = [ name: "oneof optional: rejects invalid channel", golden: "TestOneofRequired.golden", schema: "PayloadSchema", - input: { status: "active", statusImplicitRequired: "active", channel: "phone" }, + input: { + status: "active", + statusImplicitRequired: "active", + channel: "phone", + }, success: false, }, @@ -1412,14 +1419,14 @@ export const cases: TestCase[] = [ // --- emailSchema --- { name: "email: accepts valid email", - golden: "TestFormatValidators/format_only/v4.golden", + golden: "TestFormatValidators/format_only", schema: "emailSchema", input: { value: "test@example.com" }, success: true, }, { name: "email: rejects invalid email", - golden: "TestFormatValidators/format_only/v4.golden", + golden: "TestFormatValidators/format_only", schema: "emailSchema", input: { value: "notanemail" }, success: false, @@ -1428,41 +1435,55 @@ export const cases: TestCase[] = [ // --- urlSchema (z.url() accepts any scheme) --- { name: "url: accepts https", - golden: "TestFormatValidators/format_only/v4.golden", + golden: "TestFormatValidators/format_only", schema: "urlSchema", input: { value: "https://example.com" }, success: true, }, { name: "url: accepts ftp", - golden: "TestFormatValidators/format_only/v4.golden", + golden: "TestFormatValidators/format_only", schema: "urlSchema", input: { value: "ftp://files.example.com/file.txt" }, success: true, }, { name: "url: accepts mailto", - golden: "TestFormatValidators/format_only/v4.golden", + golden: "TestFormatValidators/format_only", schema: "urlSchema", input: { value: "mailto:user@example.com" }, success: true, }, { name: "url: rejects invalid url", - golden: "TestFormatValidators/format_only/v4.golden", + golden: "TestFormatValidators/format_only", schema: "urlSchema", input: { value: "not a url" }, success: false, }, - // --- http_urlSchema (z.httpUrl() only accepts http/https) --- + // --- http_urlSchema (z.httpUrl() only accepts http/https) zod 3 accepts any URL --- { name: "http_url: accepts https", - golden: "TestFormatValidators/format_only/v4.golden", + golden: "TestFormatValidators/format_only", schema: "http_urlSchema", input: { value: "https://example.com" }, success: true, }, + { + name: "http_url: rejects invalid url", + golden: "TestFormatValidators/format_only", + schema: "http_urlSchema", + input: { value: "invalid_url" }, + success: false, + }, + { + name: "http_url: v3 accepts non-http url", + golden: "TestFormatValidators/format_only/v3.golden", + schema: "http_urlSchema", + input: { value: "ftp://files.example.com/file.txt" }, + success: true, + }, { name: "http_url: rejects ftp", golden: "TestFormatValidators/format_only/v4.golden", @@ -1481,14 +1502,14 @@ export const cases: TestCase[] = [ // --- ipv4Schema --- { name: "ipv4: accepts valid ipv4", - golden: "TestFormatValidators/format_only/v4.golden", + golden: "TestFormatValidators/format_only", schema: "ipv4Schema", input: { value: "127.0.0.1" }, success: true, }, { name: "ipv4: rejects invalid ipv4", - golden: "TestFormatValidators/format_only/v4.golden", + golden: "TestFormatValidators/format_only", schema: "ipv4Schema", input: { value: "999.999.999.999" }, success: false, @@ -1497,14 +1518,14 @@ export const cases: TestCase[] = [ // --- ipv6Schema --- { name: "ipv6: accepts valid ipv6", - golden: "TestFormatValidators/format_only/v4.golden", + golden: "TestFormatValidators/format_only", schema: "ipv6Schema", input: { value: "::1" }, success: true, }, { name: "ipv6: rejects invalid ipv6", - golden: "TestFormatValidators/format_only/v4.golden", + golden: "TestFormatValidators/format_only", schema: "ipv6Schema", input: { value: "not-ipv6" }, success: false, @@ -1513,14 +1534,14 @@ export const cases: TestCase[] = [ // --- base64Schema --- { name: "base64: accepts valid base64", - golden: "TestFormatValidators/format_only/v4.golden", + golden: "TestFormatValidators/format_only", schema: "base64Schema", input: { value: "SGVsbG8=" }, success: true, }, { name: "base64: rejects invalid base64", - golden: "TestFormatValidators/format_only/v4.golden", + golden: "TestFormatValidators/format_only", schema: "base64Schema", input: { value: "not base64!!!" }, success: false, @@ -1529,14 +1550,14 @@ export const cases: TestCase[] = [ // --- uuid4Schema --- { name: "uuid4: accepts valid uuid v4", - golden: "TestFormatValidators/format_only/v4.golden", + golden: "TestFormatValidators/format_only", schema: "uuid4Schema", input: { value: "550e8400-e29b-41d4-a716-446655440000" }, success: true, }, { name: "uuid4: rejects invalid uuid", - golden: "TestFormatValidators/format_only/v4.golden", + golden: "TestFormatValidators/format_only", schema: "uuid4Schema", input: { value: "not-a-uuid" }, success: false, @@ -1545,14 +1566,14 @@ export const cases: TestCase[] = [ // --- md5Schema --- { name: "md5: accepts valid md5 hash", - golden: "TestFormatValidators/format_only/v4.golden", + golden: "TestFormatValidators/format_only", schema: "md5Schema", input: { value: "d41d8cd98f00b204e9800998ecf8427e" }, success: true, }, { name: "md5: rejects invalid md5", - golden: "TestFormatValidators/format_only/v4.golden", + golden: "TestFormatValidators/format_only", schema: "md5Schema", input: { value: "not-a-hash" }, success: false, @@ -1565,21 +1586,21 @@ export const cases: TestCase[] = [ // --- ipSchema --- { name: "ip union: accepts valid ipv4", - golden: "TestFormatValidators/union_only/v4.golden", + golden: "TestFormatValidators/union_only", schema: "ipSchema", input: { value: "127.0.0.1" }, success: true, }, { name: "ip union: accepts valid ipv6", - golden: "TestFormatValidators/union_only/v4.golden", + golden: "TestFormatValidators/union_only", schema: "ipSchema", input: { value: "::1" }, success: true, }, { name: "ip union: rejects invalid ip", - golden: "TestFormatValidators/union_only/v4.golden", + golden: "TestFormatValidators/union_only", schema: "ipSchema", input: { value: "notanip" }, success: false, @@ -1695,7 +1716,7 @@ export const cases: TestCase[] = [ // --- TestCustomTag/v4 --- { name: "custom tag v4: parses SortParamsSchema", - golden: "TestCustomTag/v4.golden", + golden: "TestCustomTag", schema: "SortParamsSchema", input: { order: "asc", field: "name" }, success: true, @@ -1704,14 +1725,14 @@ export const cases: TestCase[] = [ // --- TestZodV4Defaults/enum_keyed_maps_become_partial_records --- { name: "v4 defaults: enum keyed maps become partial records", - golden: "TestMapWithEnumKey/v4.golden", + golden: "TestMapWithEnumKey", schema: "PayloadSchema", input: { Metadata: { draft: "some note" } }, success: true, }, { name: "v4 defaults: enum keyed maps for partial records reject invalid keys", - golden: "TestMapWithEnumKey/v4.golden", + golden: "TestMapWithEnumKey", schema: "PayloadSchema", input: { Metadata: { invalid: "some note" } }, success: false, @@ -1730,14 +1751,14 @@ export const cases: TestCase[] = [ // --- TestZodV4Defaults/optional_format_with_nullable_pointer/v4 --- { name: "v4 defaults: optional format with nullable pointer accepts null", - golden: "TestZodV4Defaults/optional_format_with_nullable_pointer/v4.golden", + golden: "TestZodV4Defaults/optional_format_with_nullable_pointer", schema: "PayloadSchema", input: { email: null }, success: true, }, { name: "v4 defaults: optional format with nullable pointer accepts valid email", - golden: "TestZodV4Defaults/optional_format_with_nullable_pointer/v4.golden", + golden: "TestZodV4Defaults/optional_format_with_nullable_pointer", schema: "PayloadSchema", input: { email: "test@example.com" }, success: true, @@ -1802,41 +1823,64 @@ export const cases: TestCase[] = [ // STRING TAG ORDER WITH FORMAT HELPERS (trim + email) // --------------------------------------------------------------------------- - // --- TestZodV4Defaults/string_tag_order_is_preserved_around_v4_format_helpers --- + // --- TestStringValidations/string_tag_order_is_preserved_around_v4_format_helpers --- { name: "trim then email: valid email passes", - golden: "TestZodV4Defaults/string_tag_order_is_preserved_around_v4_format_helpers.golden", + golden: + "TestStringValidations/string_tag_order_is_preserved_around_v4_format_helpers", schema: "PayloadSchema", - input: { TrimmedThenEmail: "user@example.com", EmailThenTrimmed: "user@example.com" }, + input: { + TrimmedThenEmail: "user@example.com", + EmailThenTrimmed: "user@example.com", + }, success: true, }, { name: "trim then email: invalid email rejects", - golden: "TestZodV4Defaults/string_tag_order_is_preserved_around_v4_format_helpers.golden", + golden: + "TestStringValidations/string_tag_order_is_preserved_around_v4_format_helpers", schema: "PayloadSchema", - input: { TrimmedThenEmail: "not-an-email", EmailThenTrimmed: "user@example.com" }, + input: { + TrimmedThenEmail: "not-an-email", + EmailThenTrimmed: "user@example.com", + }, success: false, }, { name: "email then trimmed: invalid email rejects", - golden: "TestZodV4Defaults/string_tag_order_is_preserved_around_v4_format_helpers.golden", + golden: + "TestStringValidations/string_tag_order_is_preserved_around_v4_format_helpers", schema: "PayloadSchema", - input: { TrimmedThenEmail: "user@example.com", EmailThenTrimmed: "not-an-email" }, + input: { + TrimmedThenEmail: "user@example.com", + EmailThenTrimmed: "not-an-email", + }, success: false, }, { name: "trim then email: spaces trimmed before validation passes", - golden: "TestZodV4Defaults/string_tag_order_is_preserved_around_v4_format_helpers.golden", + golden: + "TestStringValidations/string_tag_order_is_preserved_around_v4_format_helpers", schema: "PayloadSchema", - input: { TrimmedThenEmail: " user@example.com ", EmailThenTrimmed: "user@example.com" }, + input: { + TrimmedThenEmail: " user@example.com ", + EmailThenTrimmed: "user@example.com", + }, success: true, - output: { TrimmedThenEmail: "user@example.com", EmailThenTrimmed: "user@example.com" }, + output: { + TrimmedThenEmail: "user@example.com", + EmailThenTrimmed: "user@example.com", + }, }, { name: "email then trimmed: spaces cause check to fail before trim runs", - golden: "TestZodV4Defaults/string_tag_order_is_preserved_around_v4_format_helpers.golden", + golden: + "TestStringValidations/string_tag_order_is_preserved_around_v4_format_helpers", schema: "PayloadSchema", - input: { TrimmedThenEmail: "user@example.com", EmailThenTrimmed: " user@example.com " }, + input: { + TrimmedThenEmail: "user@example.com", + EmailThenTrimmed: " user@example.com ", + }, success: false, }, ]; diff --git a/tests/golden.test.ts b/tests/golden.test.ts index 22ac45f..b7da4e7 100644 --- a/tests/golden.test.ts +++ b/tests/golden.test.ts @@ -8,7 +8,7 @@ * Golden files with a @zod-version metadata that doesn't match are skipped. */ import { describe, expect, it } from "vitest"; -import { readFileSync } from "fs"; +import { existsSync, readFileSync } from "fs"; import { cases } from "./cases"; // Golden files are copied to /test/golden/ as .ts files by the docker script. @@ -23,25 +23,41 @@ const moduleCache = new Map>(); // Cache for golden file zod version metadata const versionCache = new Map(); +/** + * Resolves a golden path to a concrete .golden file path. + * + * If the path ends with ".golden", it is used as-is. + * Otherwise it is treated as a directory containing v3.golden / v4.golden, + * and the file matching currentZodVersion is returned. + */ +function resolveGolden(golden: string): string { + if (golden.endsWith(".golden")) { + return golden; + } + // Directory path — check that at least one version file exists + const dir = golden.endsWith("/") ? golden : golden + "/"; + const hasV3 = existsSync(`/golden/${dir}v3.golden`); + const hasV4 = existsSync(`/golden/${dir}v4.golden`); + if (!hasV3 && !hasV4) { + throw new Error( + `No golden files found in directory "${golden}" — expected v3.golden or v4.golden` + ); + } + return dir + currentZodVersion + ".golden"; +} + function getGoldenZodVersion(golden: string): string | null { - if (!versionCache.has(golden)) { - const tsName = golden.replace(/\//g, "__").replace(/\.golden$/, ".ts"); + const resolved = resolveGolden(golden); + if (!versionCache.has(resolved)) { try { - const content = readFileSync(`${GOLDEN_DIR}/${tsName}`, "utf-8"); - // The docker script strips // @ comments but the version is in the original. - // Since prepare_ts strips metadata lines, we check the golden source directly. - // Actually, the golden source is at /golden/ (mounted read-only from testdata/). - const goldenSource = readFileSync( - `/golden/${golden}`, - "utf-8" - ); + const goldenSource = readFileSync(`/golden/${resolved}`, "utf-8"); const match = goldenSource.match(/^\/\/ @zod-version: (v\d+)/m); - versionCache.set(golden, match ? match[1] : null); + versionCache.set(resolved, match ? match[1] : null); } catch { - versionCache.set(golden, null); + versionCache.set(resolved, null); } } - return versionCache.get(golden)!; + return versionCache.get(resolved)!; } function shouldSkip(golden: string): boolean { @@ -53,16 +69,17 @@ function shouldSkip(golden: string): boolean { } async function getSchema(golden: string, schemaName: string) { - if (!moduleCache.has(golden)) { - const tsName = golden.replace(/\//g, "__").replace(/\.golden$/, ".ts"); + const resolved = resolveGolden(golden); + if (!moduleCache.has(resolved)) { + const tsName = resolved.replace(/\//g, "__").replace(/\.golden$/, ".ts"); const mod = await import(`${GOLDEN_DIR}/${tsName}`); - moduleCache.set(golden, mod); + moduleCache.set(resolved, mod); } - const mod = moduleCache.get(golden)!; + const mod = moduleCache.get(resolved)!; const schema = mod[schemaName]; if (!schema || typeof (schema as any).safeParse !== "function") { throw new Error( - `Schema "${schemaName}" not found or not a Zod schema in ${golden}` + `Schema "${schemaName}" not found or not a Zod schema in ${resolved}` ); } return schema as { safeParse: (input: unknown) => any }; diff --git a/zod_test.go b/zod_test.go index 8401363..8757818 100644 --- a/zod_test.go +++ b/zod_test.go @@ -11,41 +11,20 @@ import ( "github.com/xorcare/golden" ) -// goldenMeta holds metadata written as comments at the top of golden files. -type goldenMeta struct { - zodVersion string // "v3", "v4", or "" (works with all versions) - noTypecheck bool // opt out of docker type-check tests -} - -type goldenOpt func(*goldenMeta) - -func withGoldenZodVersion(v string) goldenOpt { - return func(m *goldenMeta) { m.zodVersion = v } -} - // goldenAssert wraps golden.Assert, prepending metadata comments to the file. // The metadata is used by the docker type-check script to determine which zod // version to install and whether to include the file in type checking. // // All golden files are type-checked by default. -func goldenAssert(t *testing.T, data []byte, opts ...goldenOpt) { +func goldenAssert(t *testing.T, data string, version string) { t.Helper() - var meta goldenMeta - for _, o := range opts { - o(&meta) - } var lines []string - if meta.zodVersion != "" { - lines = append(lines, "// @zod-version: "+meta.zodVersion) + if version != "" { + lines = append(lines, "// @zod-version: "+version) } - if !meta.noTypecheck { - lines = append(lines, "// @typecheck") - } - if len(lines) > 0 { - header := strings.Join(lines, "\n") + "\n" - data = append([]byte(header), data...) - } - golden.Assert(t, data) + lines = append(lines, "// @typecheck") + header := strings.Join(lines, "\n") + "\n" + golden.Assert(t, []byte(header+data)) } // assertSchema is a golden file test helper for Zod schema output. @@ -73,13 +52,13 @@ func assertSchema(t *testing.T, schema any, versions ...string) { v3out := StructToZodSchema(schema, WithZodV3()) v4out := StructToZodSchema(schema) assert.Equal(t, v3out, v4out) - goldenAssert(t, []byte(v4out)) + goldenAssert(t, v4out, "") case 1: - goldenAssert(t, []byte(StructToZodSchema(schema, optsFor(versions[0])...)), withGoldenZodVersion(versions[0])) + goldenAssert(t, StructToZodSchema(schema, optsFor(versions[0])...), versions[0]) default: for _, ver := range versions { t.Run(ver, func(t *testing.T) { - goldenAssert(t, []byte(StructToZodSchema(schema, optsFor(ver)...)), withGoldenZodVersion(ver)) + goldenAssert(t, StructToZodSchema(schema, optsFor(ver)...), ver) }) } } @@ -111,7 +90,7 @@ func assertValidators(t *testing.T, fieldType reflect.Type, validators []struct{ v3 := buildValidatorConverter(fieldType, validators, WithZodV3()) v4 := buildValidatorConverter(fieldType, validators) assert.Equal(t, v3.Export(), v4.Export()) - goldenAssert(t, []byte(v4.Export())) + goldenAssert(t, v4.Export(), "") default: for _, ver := range versions { t.Run(ver, func(t *testing.T) { @@ -120,7 +99,7 @@ func assertValidators(t *testing.T, fieldType reflect.Type, validators []struct{ opts = append(opts, WithZodV3()) } c := buildValidatorConverter(fieldType, validators, opts...) - goldenAssert(t, []byte(c.Export()), withGoldenZodVersion(ver)) + goldenAssert(t, c.Export(), ver) }) } } @@ -204,7 +183,7 @@ func TestStructSimplePrefix(t *testing.T) { v3out := StructToZodSchema(User{}, WithPrefix("Bot"), WithZodV3()) v4out := StructToZodSchema(User{}, WithPrefix("Bot")) assert.Equal(t, v3out, v4out) - goldenAssert(t, []byte(v4out)) + goldenAssert(t, v4out, "") } func TestNestedStruct(t *testing.T) { @@ -459,7 +438,7 @@ func TestStringValidations(t *testing.T) { }}) c.AddTypeWithName(reflect.New(eq).Elem().Interface(), "EqBackslash") - goldenAssert(t, []byte(c.Export())) + goldenAssert(t, c.Export(), "") }) t.Run("enum ignores other validators", func(t *testing.T) { @@ -479,7 +458,30 @@ func TestStringValidations(t *testing.T) { c.AddTypeWithName(struct { V string `validate:"oneof='127.0.0.1' '::1',ip" json:"v"` }{}, "OneofIp") - goldenAssert(t, []byte(c.Export())) + goldenAssert(t, c.Export(), "") + }) + + t.Run("string tag order is preserved around v4 format helpers", func(t *testing.T) { + type Payload struct { + TrimmedThenEmail string `validate:"trim,email"` + EmailThenTrimmed string `validate:"email,trim"` + } + + customTagHandlers := map[string]CustomFn{ + "trim": func(c *Converter, t reflect.Type, validate string, i int) string { + return ".trim()" + }, + } + + for _, ver := range []string{"v3", "v4"} { + t.Run(ver, func(t *testing.T) { + opts := []Opt{WithCustomTags(customTagHandlers)} + if ver == "v3" { + opts = append(opts, WithZodV3()) + } + goldenAssert(t, NewConverterWithOpts(opts...).Convert(Payload{}), ver) + }) + } }) } @@ -524,26 +526,6 @@ func TestZodV4Defaults(t *testing.T) { assertSchema(t, Payload{}, "v4") }) - t.Run("string tag order is preserved around v4 format helpers", func(t *testing.T) { - type Payload struct { - TrimmedThenEmail string `validate:"trim,email"` - EmailThenTrimmed string `validate:"email,trim"` - } - - customTagHandlers := map[string]CustomFn{ - "trim": func(c *Converter, t reflect.Type, validate string, i int) string { - return ".trim()" - }, - } - - goldenAssert( - t, - []byte(NewConverterWithOpts(WithCustomTags(customTagHandlers)).Convert(Payload{})), - withGoldenZodVersion("v3"), - withGoldenZodVersion("v4"), - ) - }) - t.Run("custom tag before required base64 preserves min(1)", func(t *testing.T) { type Payload struct { Data string `validate:"trim,required,base64"` @@ -556,7 +538,7 @@ func TestZodV4Defaults(t *testing.T) { }, } - goldenAssert(t, []byte(NewConverterWithOpts(WithCustomTags(customTagHandlers)).Convert(Payload{})), withGoldenZodVersion("v4")) + goldenAssert(t, NewConverterWithOpts(WithCustomTags(customTagHandlers)).Convert(Payload{}), "v4") }) t.Run("ip unions inherit generic string constraints", func(t *testing.T) { @@ -616,7 +598,7 @@ func TestZodV4Defaults(t *testing.T) { Next *Node `json:"next"` } - goldenAssert(t, []byte(StructToZodSchema(Node{})), withGoldenZodVersion("v4")) + goldenAssert(t, StructToZodSchema(Node{}), "v4") }) t.Run("recursive embedded shapes keep named fields after spreads to override embedded fields", func(t *testing.T) { @@ -925,7 +907,7 @@ func TestCustomTypes(t *testing.T) { v3out := v3c.Convert(User{}) v4out := v4c.Convert(User{}) assert.Equal(t, v3out, v4out) - goldenAssert(t, []byte(v4out)) + goldenAssert(t, v4out, "") }) t.Run("custom type resolves inner generic type", func(t *testing.T) { @@ -950,7 +932,7 @@ func TestCustomTypes(t *testing.T) { v3out := v3c.Convert(User{}) v4out := v4c.Convert(User{}) assert.Equal(t, v3out, v4out) - goldenAssert(t, []byte(v4out)) + goldenAssert(t, v4out, "") }) t.Run("custom type with nullable control", func(t *testing.T) { @@ -970,7 +952,7 @@ func TestCustomTypes(t *testing.T) { v3out := v3c.Convert(User{}) v4out := v4c.Convert(User{}) assert.Equal(t, v3out, v4out) - goldenAssert(t, []byte(v4out)) + goldenAssert(t, v4out, "") }) } @@ -987,7 +969,7 @@ func TestWithIgnoreTags(t *testing.T) { assert.NotPanics(t, func() { StructToZodSchema(User{}, WithIgnoreTags("customtag")) }) - goldenAssert(t, []byte(StructToZodSchema(User{}, WithIgnoreTags("customtag")))) + goldenAssert(t, StructToZodSchema(User{}, WithIgnoreTags("customtag")), "") }) } @@ -1116,7 +1098,7 @@ func TestConvertSlice(t *testing.T) { v3out := v3c.ConvertSlice(types) v4out := v4c.ConvertSlice(types) assert.Equal(t, v3out, v4out) - goldenAssert(t, []byte(v4out)) + goldenAssert(t, v4out, "") } func TestConvertSliceWithValidations(t *testing.T) { @@ -1237,7 +1219,7 @@ func TestGenerics(t *testing.T) { v3out := v3c.Export() v4out := c.Export() assert.Equal(t, v3out, v4out) - goldenAssert(t, []byte(v4out)) + goldenAssert(t, v4out, "") } func TestSliceFields(t *testing.T) { @@ -1286,10 +1268,10 @@ func TestCustomTag(t *testing.T) { } t.Run("v3", func(t *testing.T) { - goldenAssert(t, []byte(NewConverterWithOpts(WithCustomTags(customTagHandlers), WithZodV3()).Convert(Request{})), withGoldenZodVersion("v3")) + goldenAssert(t, NewConverterWithOpts(WithCustomTags(customTagHandlers), WithZodV3()).Convert(Request{}), "v3") }) t.Run("v4", func(t *testing.T) { - goldenAssert(t, []byte(NewConverterWithOpts(WithCustomTags(customTagHandlers)).Convert(Request{})), withGoldenZodVersion("v4")) + goldenAssert(t, NewConverterWithOpts(WithCustomTags(customTagHandlers)).Convert(Request{}), "v4") }) } @@ -1364,7 +1346,7 @@ func TestRecursiveEmbeddedStruct(t *testing.T) { c.AddType(ItemD{}) c.AddType(ItemE{}) c.AddType(ItemF{}) - goldenAssert(t, []byte(c.Export()), withGoldenZodVersion("v3")) + goldenAssert(t, c.Export(), "v3") }) t.Run("v4", func(t *testing.T) { c := NewConverterWithOpts() @@ -1374,7 +1356,7 @@ func TestRecursiveEmbeddedStruct(t *testing.T) { c.AddType(ItemD{}) c.AddType(ItemE{}) c.AddType(ItemF{}) - goldenAssert(t, []byte(c.Export()), withGoldenZodVersion("v4")) + goldenAssert(t, c.Export(), "v4") }) }