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

Option to create params from input in custom validation #88

Closed
chrbala opened this issue Jul 13, 2020 · 5 comments
Closed

Option to create params from input in custom validation #88

chrbala opened this issue Jul 13, 2020 · 5 comments
Labels
enhancement New feature or request

Comments

@chrbala
Copy link
Contributor

chrbala commented Jul 13, 2020

zod custom validation allows params to be passed in to the error object. It would be nice if these could be created based on the initial data.

That is, instead of this type signature:
.refine(validator: (data:T)=>any, params?: RefineParams)

zod could use:
.refine(validator: (data:T)=>any, params?: RefineParams | (data: T) => RefineParams

@colinhacks colinhacks added the enhancement New feature or request label Jul 17, 2020
@colinhacks
Copy link
Owner

Great idea. I'll definitely do something like this. Might leave the .refine method as is but add a .refinement for more advanced use cases.

@FranckBontemps
Copy link

For a more advanced use, I would also like to have the method with a single parameter:

type Success = {
    isValid: true
}

type Error = {
    isValid: false,
    params: RefineParams
}

.refinement((data: T) => Success | Error)

with this, the customs validations can be reused more easily:

isFoo(data) {
    if(data === 'Foo') {
        return {
            isValid: true;
        }
    }

    return { 
        isValid: false,
        params: {
            message: 'it's not Foo'
        }
    }
}

z.string().refinement(isFoo)

That is only a draft to present the option

@brabeji
Copy link
Contributor

brabeji commented Sep 2, 2020

@FranckBontemps check this one out #132 I think that this can be even more powerful when the ZodError is exposed.

@tmtron
Copy link
Contributor

tmtron commented Sep 15, 2020

I have a related use-case, where I'd like to use information from the validation function in the error message.

E.g. a simple database definition where we can define a record of column names and a primary key array:

z.object({
    columns: z.record(z.string()),
    primaryKey: z.array(apiString({}).nonempty()).nonempty()
  })

in this case the primaryKey array must only contain keys of the columns record.

The custom validation function could look like this:

.refine(
   (data) => {
     for (const pkItem of data.primaryKey) {
       if (!(pkItem in data.columns)) {
         // We'd like to include pkItem in the error message
         return false;
       }
     }
     return true;
   },
   {
     message: `the primaryKey array must only contain keys from the columns record`,
     path: ['primaryKey']
   }
 );

In this function we already have the logic to find the item that violates the check. It would be great if we could easily reuse this in the error-message.
Maybe we could have a new refine function that returns undefined (when everything is okay) or the RefineParams type (including message, path, params) in case of an error?

I hope that this can also be considered when working on a solution for this issue.

@colinhacks
Copy link
Owner

colinhacks commented Sep 17, 2020

This is now possible in Zod 2 (now in beta)

const dynamicRefine = z.string().refine(
    val => val === val.toUpperCase(),
    val => ({ params: { val } }),
  );

  console.log(dynamicRefine.safeParse('asdf'));

returns

{
    Error: [
      {
        code: 'custom_error',
        params: {
          val: 'asdf',
        },
        path: [],
        message: 'Invalid input.',
      },
    ];
  }

@tmtron this is a pretty advanced use case but it is possible. Internally .refine delegates to the more advanced ._refinement method which you can use to implement this. You can use this method without worrying about breaking changes, I consider it a part of the public API; it only starts with an underscore mostly because it's a "power user" feature.

Some other context: in Zod 2 the errors property of a ZodError has been renamed to issues. This clarifies the confusing "error" vs "suberror" aspect of the previous naming.

const apiString = z.string();
  z.object({
    columns: z.record(z.string()),
    primaryKey: z.array(apiString.nonempty()).nonempty(),
  })._refinement((data, ctx) => {
    const invalidPks = data.primaryKey.filter(
      pk => !Object.keys(data.columns).includes(pk),
    );
    if (invalidPks.length) {
      ctx.addIssue({
        code: z.ZodIssueCode.custom,
        message: `Invalid PKs: ${invalidPks.join(', ')}`,
      });
    }
  });

As you can see there's no need to return anything from _refinement(). If ctx.addIssue is called at any point during the execution of the refinement, the parse will fail. You can call ctx.addIssue multiple times to create multiple errors. You can also make this function async if you want to do your database fetches inside your refinement.

You need to manually provide the error code using the z.ZodErrorCode enum. This isn't required in .refine because .refine only every throws a single ZodCustomIssue whereas `refinement lets you throw arbitrarily many issues of any kind. The error handling guide has more information on this.


@brabeji The approach above also enables what you were trying to achieve with .dynamicRefinement. Great job on that PR 👍

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

No branches or pull requests

5 participants