Open alexgleason opened 11 months ago
This helper function is doing what I want:
/** zod schema to force the value into an object, if it isn't already. */
function coerceObject<T extends z.ZodRawShape>(shape: T) {
return z.object({}).passthrough().catch({}).pipe(z.object(shape));
}
Now instead of z.object({ ...shape })
I use coerceObject({ ...shape })
.
Resulting in this behavior:
const configurationSchema = coerceObject({
chats: coerceObject({
max_characters: z.number().catch(5000),
max_media_attachments: z.number().catch(1),
}),
groups: coerceObject({
max_characters_description: z.number().catch(160),
max_characters_name: z.number().catch(50),
}),
media_attachments: coerceObject({
image_matrix_limit: z.number().optional().catch(undefined),
image_size_limit: z.number().optional().catch(undefined),
supported_mime_types: mimeSchema.array().optional().catch(undefined),
video_duration_limit: z.number().optional().catch(undefined),
video_frame_rate_limit: z.number().optional().catch(undefined),
video_matrix_limit: z.number().optional().catch(undefined),
video_size_limit: z.number().optional().catch(undefined),
}),
polls: coerceObject({
max_characters_per_option: z.number().catch(25),
max_expiration: z.number().catch(2629746),
max_options: z.number().catch(4),
min_expiration: z.number().catch(300),
}),
statuses: coerceObject({
max_characters: z.number().catch(500),
max_media_attachments: z.number().catch(4),
}),
});
configurationSchema.parse({}) // { chats: { max_characters: 5000, ... }, ... }
What do you think @colinhacks, should this belong in the main library? It was one of my biggest issues with zod until I learned how to get good at it.
It's especially useful when you have deeply-nested data and you cannot guarantee certain deep values exist. Some APIs really do this! Eg: https://mastodon.social/api/v1/instance Between different Mastodon servers I have no clue what fields will be available.
@alexgleason, I think the helper below should work the same as yours; the one benefit is a narrower return type that works with z.input<typeof schema>
type inference, which was the one additional thing we needed for our codebase.
function coerceObject<T extends z.ZodRawShape>(
shape: T,
params?: z.RawCreateParams,
) {
return new z.ZodEffects({
schema: z.object(shape, params),
effect: { type: "preprocess", transform: Object },
typeName: z.ZodFirstPartyTypeKind.ZodEffects,
});
}
This is basically z.preprocess(Object, z.object(shape))
, just without forcing the input type to unknown
.
Here it is with an explicit function return type:
Unfortunately it still doesn't return a ZodObject
, so you still can't .extend
or .merge
it. I tried to overcome this by subclassing ZodObject
subclass to override its _parse
method, but this didn't pan out.
Instead of:
We should be able to:
z.coerce.object
would be roughly equivalent to:All keys of the shape need to be a
ZodCatch
orZodOptional
, otherwise it's a TypeError.