Southclaws / supervillain

Converts Go structs to Zod schemas
MIT License
77 stars 6 forks source link

Modify schemas to more closely match possible Go output when `omitempty` is used #5

Closed 8gian closed 1 year ago

8gian commented 1 year ago

Overview

There are a few cases where the library overzealously applies both optional() and nullable(). This change aims to restrict the cases to match the actual possible output from marshaling JSON in Go (see examples. It also fixes one case where the type was too restrictive. These cases are outlined below.

Changes

Remove optional() from struct fields tagged as omitempty that are themselves structs because these cannot be empty

Example:

type Foo struct {
  X struct {
     S string
  } `json:",omitempty"`
}

Output before for X:

X: z.object({
  S: z.string()
}).optional()

Output after for X:

X: z.object({
  S: z.string()
})

Remove nullable() from pointer types not pointing to a slice, another pointer, or a map that are tagged as omitempty because these cannot be exported as null

Example:

type Foo struct {
  A string `json: ",omitempty"`
  B *string `json: ",omitempty"`
  C **string `json: ",omitempty"`
}

Output before for A, B, and C:

A: z.string().optional()
B: z.string().optional().nullable()
C: z.string().optional().nullable()

Output after:

A: z.string().optional()
B: z.string().optional()
C: z.string().optional().nullable()

Note that pointers to pointers can still be null because only the value outermost pointer is checked for empty value in the case of omitempty.

Remove nullable() from slice fields that are tagged as omitempty because these cannot be exported as null

Example:

type Foo struct {
  X []string `json:",omitempty"`
  Y *[]string `json:",omitempty"`
}

Output before for X, Y:

X: z.string().array().optional().nullable()
Y: z.string().array().optional().nullable()

Output after for X, Y:

X: z.string().array().optional()
Y: z.string().array().optional().nullable()

In the case of pointers to slices and maps tagged as omitempty, the result is unchanged because these can be exported with the value null or undefined. Pointers to interfaces can also be exported with the value null but interfaces already have type any so adding nullable() to that doesn't make sense.

Add nullable() to map fields that are not tagged as omitempty because these can be exported as null

Example:

type Foo struct {
  M map[string]string
  M map[string]string `json:",omitempty"`
  O *map[string]string `json:",omitempty"`
}

Output before for N, M, and O:

M: z.record(z.string(), z.string())
N: z.record(z.string(), z.string()).optional()
O: z.record(z.string(), z.string()).optional().nullable()

Output after for N, M, and O:

M: z.record(z.string(), z.string()).nullable()
N: z.record(z.string(), z.string()).optional()
O: z.record(z.string(), z.string()).optional().nullable()

In the case of maps tagged as omitempty, the result is unchanged because these cannot be exported with the value null, but since the previous code didn't correctly add the nullable() annotation, these issues cancel either out. Also, in the case of pointers to maps tagged as omitempty, the result is unchanged because these can be exported with null or undefined values.

8gian commented 1 year ago

This is finally ready for review. I was having trouble figuring out how to rebase properly rebase onto upstream but I managed to do that eventually.