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

[PoC] Add refineWith #420

Closed

Conversation

oleg-codaio
Copy link

Adding a helper method that can be used to refine a value with a specified schema, in order to take advantage of Zod's built-in refinement functionality. This can be useful for refining a list of strings to a list of enums, for instance, or enforcing bounds on a number.

@oleg-codaio
Copy link
Author

oleg-codaio commented May 1, 2021

Here's some more background on this: consider that you're using Zod to parse URL query params, which are defined as optional strings, and transform them into some usable type (e.g., enum, boolean, integer, etc.). Specifically, let's consider a param that represents the limit on the number of records that should be returned from a query endpoint. We'd currently need to write that like this:

z
  .string()
  .transform(v => v ? +v : undefined)
  .refine(v => v === undefined || Number.isInteger(v), {message: 'should be an integer'})
  .refine(v => v === undefined || (v >= 0 && v <= 100), {message: 'should be between 0 and 100'})
  .optional()

Instead, I'd love to be able to write:

z
  .string()
  .transform(v => v ? +v : undefined) // Treat an empty string as undefined.
  .refineWith(z.number().int().min(1).max(100).optional())
  .optional()

This could be used for more complex validations too, such as transforming a comma-separated string of enums into an actual array of enums more elegantly (i.e., using nativeEnum()).

@oleg-codaio
Copy link
Author

See also: #345

@oleg-codaio
Copy link
Author

As an alternative to this, if transform handled Zod errors (instead of just returning some invalid symbol) that would work as well because we could call .transform(val -> z.number().parse(val)). Though that's a bit more verbose than the proposed syntax here.

@cakoose
Copy link

cakoose commented May 3, 2021

Could also do both, e.g.:

  1. Allow .transform to handle errors.
  2. Add .transformWith as a convenience for using a Zod schema with .transform.

): This extends ZodEffects<infer T, any>
? ZodEffects<T, NewOut>
: ZodEffects<This, NewOut> {
let parsed: ReturnType<typeof returnType.safeParse>;
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually I suppose there's a concurrency bug here due to this being in the scope of the schema rather than the specific parsing invocation of it. So parsed would have to be stashed somewhere within the context.

@cdauth
Copy link

cdauth commented Jul 28, 2021

I stumbled across this pull request while searching for a solution to the same problem.

Until this is merged, I am using this function:

/**
 * Transform the result of a zod scheme to another type and validates that type against another zod scheme.
 * The result of this function basically equals inputSchema.transform((val) => outputSchema.parse(transformer(val))),
 * but errors thrown during the transformation are handled gracefully (since at the moment, zod transformers
 * do not natively support exceptions).
 */
export function transformValidator<Output, Input1, Input2, Input3>(inputSchema: z.ZodType<Input2, any, Input1>, transformer: (input: Input2) => Input3, outputSchema: z.ZodType<Output, any, Input3>): z.ZodEffects<z.ZodEffects<z.ZodType<Input2, any, Input1>, Input2>, Output> {
    // For now we have to parse the schema twice, since transform() is not allowed to throw exceptions.
    // See https://github.com/colinhacks/zod/pull/420
    return inputSchema.superRefine((val, ctx) => {
        try {
            const result = outputSchema.safeParse(transformer(val));
            if (!result.success) {
                for (const issue of result.error.errors) {
                    ctx.addIssue(issue);
                }
            }
        } catch (err) {
            ctx.addIssue({
                code: z.ZodIssueCode.custom,
                message: err.message,
            });
        }
    }).transform((val) => outputSchema.parse(transformer(val)));
}

I'm using it for example to transform a JSON string to an object and then validating that object against a schema:

const jsonValidator = transformValidator(z.string().optional(), (val) => val ? JSON.parse(val) : undefined, z.object({
    ...
});

@stale
Copy link

stale bot commented Mar 2, 2022

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

@stale stale bot added the wontfix This will not be worked on label Mar 2, 2022
@stale stale bot closed this Mar 9, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
wontfix This will not be worked on
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants