Skip to content
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

z.coerce.object() should be a thing #2786

Open
alexgleason opened this issue Sep 24, 2023 · 2 comments
Open

z.coerce.object() should be a thing #2786

alexgleason opened this issue Sep 24, 2023 · 2 comments

Comments

@alexgleason
Copy link

Instead of:

const mrfSimpleSchema = z.object({
  accept: z.string().array().catch([]),
  avatar_removal: z.string().array().catch([]),
  banner_removal: z.string().array().catch([]),
  federated_timeline_removal: z.string().array().catch([]),
  followers_only: z.string().array().catch([]),
  media_nsfw: z.string().array().catch([]),
  media_removal: z.string().array().catch([]),
  reject: z.string().array().catch([]),
  reject_deletes: z.string().array().catch([]),
  report_removal: z.string().array().catch([]),
}).catch({
  accept: [],
  avatar_removal: [],
  banner_removal: [],
  federated_timeline_removal: [],
  followers_only: [],
  media_nsfw: [],
  media_removal: [],
  reject: [],
  reject_deletes: [],
  report_removal: [],
});

We should be able to:

const mrfSimpleSchema = z.coerce.object({
  accept: z.string().array().catch([]),
  avatar_removal: z.string().array().catch([]),
  banner_removal: z.string().array().catch([]),
  federated_timeline_removal: z.string().array().catch([]),
  followers_only: z.string().array().catch([]),
  media_nsfw: z.string().array().catch([]),
  media_removal: z.string().array().catch([]),
  reject: z.string().array().catch([]),
  reject_deletes: z.string().array().catch([]),
  report_removal: z.string().array().catch([]),
});

z.coerce.object would be roughly equivalent to:

z.object({}).passthrough().catch({}).pipe(myObjSchema)

All keys of the shape need to be a ZodCatch or ZodOptional, otherwise it's a TypeError.

@alexgleason
Copy link
Author

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.

@aaronadamsCA
Copy link

@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:

function coerceObject<T extends z.ZodRawShape>(
  shape: T,
  params?: z.RawCreateParams,
): z.ZodEffects<
  z.ZodObject<
    T,
    "strip",
    z.ZodTypeAny,
    z.objectOutputType<T, z.ZodTypeAny, "strip">,
    z.objectInputType<T, z.ZodTypeAny, "strip">
  >,
  z.objectOutputType<T, z.ZodTypeAny, "strip">,
  z.objectInputType<T, z.ZodTypeAny, "strip">
> {
  return new z.ZodEffects({
    schema: z.object(shape, params),
    effect: { type: "preprocess", transform: Object },
    typeName: z.ZodFirstPartyTypeKind.ZodEffects,
  });
}

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants