Skip to content

Commit

Permalink
feat: support embedded structs by emitting .merge methods
Browse files Browse the repository at this point in the history
This commit adds support for embedded Go structs, by emitting
the schema for the nested type then adding .merge(NestedSchema)
directives onto the emitted zod schemas.
  • Loading branch information
sd2k committed Nov 20, 2023
1 parent 2017324 commit 53aeb16
Show file tree
Hide file tree
Showing 2 changed files with 74 additions and 11 deletions.
38 changes: 27 additions & 11 deletions zod.go
Original file line number Diff line number Diff line change
Expand Up @@ -217,19 +217,30 @@ func (c *Converter) convertStruct(input reflect.Type, indent int) string {
output.WriteString(`z.object({
`)

merges := []string{}

fields := input.NumField()
for i := 0; i < fields; i++ {
field := input.Field(i)
optional := isOptional(field)
nullable := isNullable(field)

line := c.convertField(field, indent+1, optional, nullable)
line, shouldMerge := c.convertField(field, indent+1, optional, nullable, field.Anonymous)

output.WriteString(line)
if !shouldMerge {
output.WriteString(line)
} else {
merges = append(merges, line)
}
}

output.WriteString(indentation(indent))
output.WriteString(`})`)
if len(merges) > 0 {
for _, merge := range merges {
output.WriteString(merge)
}
}

return output.String()
}
Expand Down Expand Up @@ -395,12 +406,12 @@ func (c *Converter) getType(t reflect.Type, name string, indent int) string {
return zodType
}

func (c *Converter) convertField(f reflect.StructField, indent int, optional, nullable bool) string {
func (c *Converter) convertField(f reflect.StructField, indent int, optional, nullable, anonymous bool) (string, bool) {
name := fieldName(f)

// fields named `-` are not exported to JSON so don't export zod types
if name == "-" {
return ""
return "", false
}

// because nullability is processed before custom types, this makes sure
Expand All @@ -417,13 +428,18 @@ func (c *Converter) convertField(f reflect.StructField, indent int, optional, nu
nullableCall = ".nullable()"
}

return fmt.Sprintf(
"%s%s: %s%s%s,\n",
indentation(indent),
name,
c.ConvertType(f.Type, typeName(f.Type), f.Tag.Get("validate"), indent),
optionalCall,
nullableCall)
t := c.ConvertType(f.Type, typeName(f.Type), f.Tag.Get("validate"), indent)
if !anonymous {
return fmt.Sprintf(
"%s%s: %s%s%s,\n",
indentation(indent),
name,
t,
optionalCall,
nullableCall), false
} else {
return fmt.Sprintf(".merge(%s)", t), true
}
}

func (c *Converter) getTypeField(f reflect.StructField, indent int, optional, nullable bool) string {
Expand Down
47 changes: 47 additions & 0 deletions zod_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,38 @@ export type BotUser = z.infer<typeof BotUserSchema>
StructToZodSchemaWithPrefix("Bot", User{}))
}

func TestNestedStruct(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<typeof HasIDSchema>
export const HasNameSchema = z.object({
name: z.string(),
})
export type HasName = z.infer<typeof HasNameSchema>
export const UserSchema = z.object({
Tags: z.string().array().nullable(),
}).merge(HasIDSchema).merge(HasNameSchema)
export type User = z.infer<typeof UserSchema>
`,
StructToZodSchema(User{}))
}

func TestStringArray(t *testing.T) {
type User struct {
Tags []string
Expand All @@ -128,6 +160,21 @@ export type User = z.infer<typeof UserSchema>
StructToZodSchema(User{}))
}

func TestStringNestedArray(t *testing.T) {
type TagPair [2]string
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<typeof UserSchema>
`,
StructToZodSchema(User{}))
}

func TestStructSlice(t *testing.T) {
type User struct {
Favourites []struct {
Expand Down

0 comments on commit 53aeb16

Please sign in to comment.