-
Notifications
You must be signed in to change notification settings - Fork 19
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix: Deep partial stripping out fields in union #53
Fix: Deep partial stripping out fields in union #53
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for finding the cause of the issue and proposing a solution! I added some comments on how to improve this to ensure this works for all types, and suggested some tests to ensure there are no more related bugs
@@ -46,6 +93,19 @@ describe('zodDeepPartial', () => { | |||
expect(deepPartial.parse({})).toEqual({}) | |||
|
|||
expect(() => deepPartial.parse({ n: 'string' })).toThrow() | |||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- Can we add a test for nested object types? Here we are only fixing the issue for unions passed directly, but if the problematic union is on an object property, I think we would still have the same issue:
/** PR fixes parsing this type */
const ZTestUnion = z.union([
z.object({ a: z.number().optional() }),
z.object({ b: z.number().optional() })
[)
const ZNestedTestUnion = z.object({
field: z.number()
union: ZTestUnion
})
- If a test using the above object does not work, we should probably think of applying
mergeObjectSchemas
recursively. I'm mostly worried about affecting Typescript performance with that (I don't think it will cause performance issues on runtime though). - Given the above is true, we should apply this on
zodDeepPartial
even if the result is an object
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added this test and it passes. This works because zodDeepPartial
is recursive: https://github.com/cau777/ts-mongo/blob/a6011a6b376c276db4c0d0b32bb81871da79e1e4/src/zod-utils.ts#L97-L97
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks! Makes sense
) | ||
|
||
expect(deepPartial2.parse({ s: 'a' })).toEqual({ s: 'a' }) | ||
expect(deepPartial2.parse({ n: 2 })).toEqual({ n: 2 }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we also add a test for a union that includes this union?:
const ZTestUnion =
z.union([
z.object({
t: z.literal('a'),
s: z.string(),
}),
z.object({ t: z.literal('b'), n: z.number() }),
])
const ZUnionOfUnion = z.union([z.object({ randomField: z.string().optional()}), ZTestUnion])
const deepPartial3 = zodDeepPartial(ZUnionOfUnion)
expect(deepPartial3.parse({s: 'hi'})).toEqual({s: 'hi'})
The first commits adds a new assertion in the test, which fails unexpectedly:
Particularly, the first assertion (
expect(deepPartial2.parse({ s: 'a' })).toEqual({ s: 'a' })
) passes but the second one (expect(deepPartial2.parse({ n: 2 })).toEqual({ n: 2 })
) fails. This highlights a bug we are having on Aerial where Zod strips out some fields but not others in unions. After some investigation, it was found that this bug happens when you have a union where all fields are partial, like:In this case, no matter the object you pass to
ZTestUnion.parse()
, zod will always validate it against the first union member (z.object({ a: z.number().optional() })
) and strip out fields that aren't present in it, so:This is very bad because it silently omits fields from
zodCollection
updates, leading to subtle bugs.Another interesting observation is that the bug doesn't happen on discriminated unions where the
type
is required:The reason is that the discriminator helps zod know which union member we are parsing.
This PR proposed a solution to "merge" all the fields of all union object members into a single object. If a property appears on multiple members, it will create a
ZodUnion
with all possible values for that property.