-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
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
Refine validations on object definitions don't get triggered until all fields in the object exist. #479
Comments
What I mentioned related to the form is just an example. My point is still that I created a little code sandbox for you to try. Here, this is what I'm doing in the codesandbox.
For this, I expect these validation results to be produced: 1) "name" is required and 2) "age" must be greater than or equal to 0. However, what I see is only "name" is required. |
@ivan-kleshnin I went through this again, but I still think what you mentioned contradicts with my findings. It'd be great if you could let me know how zod solves this issue. |
Let me reiterate the issue in more detail. It may be by design that One solution is to create a nested object schema and use z.object({
name: z.string,
dates: z.object({
startDate: z.date(),
endDate: z.date()
})
.refine((data) => checkValidDates(data.startDate, data.endDate), 'Start date must be earlier than End date.'),
}) Sure, this works, but what if there's another field that depends on start and end dates? In my scenario, the start and end date are the dates of an event. I also have a due date field which specifies when an event registration is due. So, I need to make sure it's earlier than the start date. Now, I need to wait until start and end dates are both filled in before using a refine validation. But, what I want is to trigger the validation as soon as start and due date are filled in. zod cannot currently handle this if I'm correct. Also, doing this breaks the structure, so it doesn't play nice with Blitz.js. My suggestion is to have something like yup.when which gives you access to values of other fields. To enable this, the z.object({
name: z.string,
startDate: z
.date()
.refine((value, getField) => checkValidDates(value, getField("endDate")), 'Start date must be earlier than End date.'),
endDate: z
.date()
.refine((value, getField) => checkValidDates(getField("startDate"), value), 'Start date must be earlier than End date.'),
dueDate: z
.date()
.refine((value, getField) => checkValidDates(value, getField("startDate")), 'Due date must be earlier than Start date.'),
}) This is my suggestion. By moving the refine into the field schema, then we can makes sure it gets triggered as soon as that field is modified. There's also no need to create a nested object schema. If there's an existing solution, then that's great. Please let met know. I hope I made my point clear enough. I'm not sure if this is technically possible with how zod is written, but this feels like such a basic scenario, so it'd be great support this. |
What I mentioned related forms are still just examples. It is not really what I want to discuss. (Well, if you are curious why missing keys in those "forms" do not have Currently, zod doesn't have a good way to do validations against more than one field and that's what I'm trying to point out here. I also think this is a not a corner case. It's not uncommon to have fields that depend on some other fields (e.g. password and password confirmation, start/end dates, etc.), whether it's a form or any other json data, and we usually want to surface the error as soon as we detect them. The suggestion to use I skimmed through the source code a bit today, but isn't it possible to hold a reference to the original data inside the |
My two cents: You could be explicit bout what you are trying to do and make two schemas: one for the form and one for the model. Something like this? TypeScript Playground import { z } from "zod";
const formSchema = z.object({
name: z.string(),
startDate: z.date(),
endDate: z.date(),
}).partial();
type Form = z.infer<typeof formSchema>;
const modelSchema = formSchema.required().refine((input) => {
return input.startDate < input.endDate;
}, "Start date must be earlier than end date");
type Model = z.infer<typeof modelSchema>;
const blankModel: Model = {
name: "",
startDate: new Date(),
endDate: new Date(),
};
const currentForm: Form = {
name: "A name",
};
const result = modelSchema.safeParse({ ...blankModel, ...currentFrom }); |
A version that embraces the partial nature of the form: TypeScript Playground import { z } from "zod";
const formSchema = z.object({
name: z.string(),
startDate: z.date(),
endDate: z.date(),
}).partial().refine((partialInput) => {
if (!partialInput.startDate || !partialInput.endDate) {
return true;
}
return partialInput.startDate < partialInput.endDate;
});
type Form = z.infer<typeof formSchema>;
const currentForm: Form = {
name: "A name",
};
const result = formSchema.safeParse(currentForm); |
Hi, thanks for you two for taking time for this issue. Your example gets the job done, but there's still one issue in the real world scenarios. If there are separate schemas, then I'll need to add the same I don't think there's a better way to handle this? const CreateTournamentBaseSchema = z
.object({
name: z.string(),
organizer: z.string().optional(),
startDate: z.date(),
endDate: z.date(),
dueDateTime: z.date(),
})
.strict()
const CreateTournamentBaseSchemaPartial = CreateTournamentBaseSchema.partial()
// Accept a partial object type as an argument, so the refine methonds doesn't need to assume
// the fields in the object are always defined and it'll cover both cases.
const addRefines = (schema: typeof CreateTournamentBaseSchemaPartial) => {
return schema.refine((data) => {
if (!partialInput.startDate || !partialInput.endDate) {
return true;
}
return partialInput.startDate < partialInput.endDate;
});
};
export const CreateTournamentFormSchema = addRefines(CreateTournamentBaseSchemaPartial)
export const CreateTournamentModelSchema = addRefines(
// I'll need to force cast it to the partial type to make the compiler happy.
CreateTournamentBaseSchema as unknown as typeof CreateTournamentBaseSchemaPartial
) |
Since those two schemas are not compatible with each other, I think you'll either need to be ok with the cast here, or use the fact that the partial schema will match the non-partial schema and define the refinement over the partial data. Something like this: TypeScript Playground import { z } from "zod";
const CreateTournamentBaseSchema = z
.object({
name: z.string(),
organizer: z.string().optional(),
startDate: z.date(),
endDate: z.date(),
dueDateTime: z.date(),
})
.strict()
const CreateTournamentBaseSchemaPartial = CreateTournamentBaseSchema.partial()
// Accept a partial object type as an argument, so the refine methonds doesn't need to assume
// the fields in the object are always defined and it'll cover both cases.
const checkDates = (data: z.infer<typeof CreateTournamentBaseSchemaPartial>) => {
if (!data.startDate || !data.endDate) {
return true;
}
return data.startDate < data.endDate;
}
export const CreateTournamentFormSchema = CreateTournamentBaseSchemaPartial.refine(checkDates)
export const CreateTournamentModelSchema = CreateTournamentBaseSchema.refine(checkDates) |
Yeah, ok. That works out nicely. I did not think of using Though, I still think this should be somehow built-in to zod. I'm happy with this workaround now, but I believe supporting it would make zod even easier to work with. Well, I'll let the community decide... Anyways, thanks a lot for the help. |
Ultimately, I see the value of Somewhat related to this, there is a PR in the works that adds a concept of "pre-processing" (#468) that allows transforming the input before the schema is checked that might help a bit with this, but not much more elegant (imo) than the solution I outlined above I think.
Glad to help! |
I'm currently in the process of testing Zod to migrate from Yup. I love it so far, but the issue is also an irritant for us |
This seems to be similar to #690, and based on @colinhacks response he is working on a change to the parser to support this in 3.10. You can test it by trying the beta release via |
I don't think this is similar to #690, a use case for triggering |
I think if you have multiple subsets of the final data structure that need to be validated together, you might want to consider making these separate schemas and then combining them together into a single schema (if you need that single schema for something else). Conversely you could use |
I think this is the approach I'm looking for since I need the combined schema because I'm using Zod as a resolver on react-hook-forms. |
Didn't do the trick, or at least I didn't find how to make it work. Let's imagine that I have the following two schemas const step1 = z.object({ name: z.string().nonempty('Name is required') });
const step2 = z.object({
company: z.string().nonempty('Company is required'),
position: z.string(),
});
const step3 = z.object({ team: z.string().nonempty('Team is required') });
const allSteps = step1.merge(step2);
const allStepsWithRefine = step1
.merge(
step2.refine(
({ company, position }) => {
if (company) {
return '' !== position;
}
return true;
},
{
message: "String can't be more than 255 characters",
},
),
)
.merge(step3); I'm looking for a way to make
|
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. |
This is a non-starter for us to switch from Yup to Zod. The use case that we have might be addressed by #1394 |
Really sad about this behaviour, tbh. Idk if I'm being too shallow in my thoughts, but to me it seems you could just execute the refinements even if the schema validation fails, then this problem would be solved. But ok, let's try to make workarounds for this |
Just come across this 2 years after it was opened, and also find that this is an annoyance for us. We're using If we had someway to reference other data, that would be perfect:
But this is not made available, and so it seems we are doomed to post-validation refine, or splitting our Zod up into pieces |
SolutionUse
Hope this helps someone |
I'm currently facing the same problem as everyone has mentioned already, refine/superRefine function is not getting called or triggered, I was going through the docs to see why it is happening but nth was mentioned there, if thats the expected behaviour then please update the docs, otherwise I do hope this interesting behaviour is actually fixed. |
can you like give some detailed example bro, im trying to use inside useForm also plus we need zod solution... |
This was amazing @Mjtlittle , thanks! |
this is working. thanks @Mjtlittle |
this just saved me hours of manual reverse-engineering of automatically-generated zod objects. thank you so much @Mjtlittle |
This works for me nicely, thanks |
Thanks for this helper @Mjtlittle! @colinhacks this solution would be amazing as a flag option as requested here |
Over a year since my last input on this and we still don't have a good solution, but I'm back with a new approach, I'll preface this by saying I equally hate this approach and I'm not yet sure how it'll impact React Hook Form. My goal was to find a solution that gives me the "everything gets validated" that I wanted to reach without splitting out the schema. The concept is:
Like I said, I don't like it, but it gives a workable result that is still type-strong, and is decently readable. type ZodPath = (string | number)[] | undefined;
function customRequired(ctx: z.RefinementCtx, value: unknown, path: ZodPath, message: string) {
if (!value && value !== 0 && value !== false) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message,
path,
})
}
}
export const theZodSchema = z.object({
iAmRequired: z.string().optional(),
startDate : z.date().optional(),
variableABoolean : z.coerce.boolean().optional(),
variableBString: z.string().optional(),
})
.superRefine((data, ctx) => {
customRequired(
ctx,
data.iAmRequired,
['iAmRequired'],
'This is required',
);
if (!data.startDate || data.startDate < new Date()) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'Start date must be in the future',
path: ['startDate'],
})
}
if (data.variableABoolean && !data.variableBString) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'If Variable A is true, then Variable B must be specficied',
path: ['variableB'],
})
}
})
; Resulting type: export const theZodSchema:
z.ZodEffects<
z.ZodObject<{
iAmRequired: z.ZodOptional<z.ZodString>,
startDate: z.ZodOptional<z.ZodDate>,
variableABoolean: z.ZodOptional<z.ZodBoolean>,
variableBString: z.ZodOptional<z.ZodString>
},
"strip",
z.ZodTypeAny,
{},
{}
>, // ZodObject
{},
{}
> // ZodEffects This is an imperfect solution for a problem without one, so take it as you like; feedback and extension welcome. |
Edit: My previous wrong suggestion:
|
Isn't it a bug that causes the issue that is discussed here? const obj = z
.object({
foo: z.string().min(1),
})
.superRefine((val, ctx) => {
ctx.addIssue({
message: 'Error from superRefine',
});
}); Why does the
but
Shouldn't the custom error from |
The proper solution for this already exists, it's Don't use @Mjtlittle's solution or @david-arteaga's workaround that supports transforms. They aren't type safe and overcomplicate what In this example, we want to validate that const base = z.object({
foo: z.string(),
min: z.string().transform((s) => Number(s)),
max: z.number(),
})
const schema = z.preprocess((input, ctx) => {
// input is unknown so parse only the fields that are involved in cross-field validation
// (this is what you should be doing anyway if using @Mjtlittle's workaround!)
const parsed = base.pick({ min: true, max: true }).safeParse(input)
if (parsed.success) {
const { min, max } = parsed.data
// the code you would usually put in `refine` or `superRefine` goes here:
if (min > max) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ['min'],
message: 'must be <= max',
})
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ['max'],
message: 'must be >= min',
})
}
}
// pass through the input so that subsequent parsing works
return input
}, base)
for (const value of [
{ foo: 'a', min: '1', max: 2 },
{ min: '1', max: 2 },
{ min: '2', max: 1 },
{ foo: 'a', min: '2', max: 1 },
]) {
console.log('Value:', value, 'safeParse:', { ...schema.safeParse(value) })
} Output:
|
Thanks @jedwards1211 . |
@david-arteaga yeah it's understandable, I landed on this issue because I was about to make a similar feature request, but then I thought to double-check the Here's code for a helper I created for doing conditional validation in const schema = conditionalValidate(
z.object({
foo: z.string(),
min: z.number().finite(),
max: z.number().finite(),
})
).conditionalRefine(
// Pick the fields the refinement depends on here
(s) => s.pick({ min: true, max: true }),
// This refinement will only be checked if min and max are successfully parsed
({ min, max }) => min <= max,
[
{ path: ['min'], message: 'must be <= max' },
{ path: ['max'], message: 'must be >= min' },
]
) import z from 'zod'
type DeepPartial<T> = T extends object | any[]
? { [K in keyof T]?: DeepPartial<T[K]> }
: T
type ConditionalCheck<T extends z.ZodTypeAny> = {
schema: T
check: (value: z.output<T>, ctx: z.RefinementCtx) => void | Promise<void>
async: boolean
}
interface ConditionalValidatorDef<T extends z.ZodTypeAny>
extends z.ZodEffectsDef<T> {
checks: ConditionalCheck<any>[]
}
type ConditionalRefineMessage<Output> =
| string
| z.CustomErrorParams
| z.CustomErrorParams[]
| ((value: Output) => z.CustomErrorParams | z.CustomErrorParams[])
type ConditionalRefineSchema<T extends z.ZodTypeAny> =
| z.ZodType<DeepPartial<z.output<T>>, any, DeepPartial<z.input<T>>>
| ((
schema: T
) => z.ZodType<DeepPartial<z.output<T>>, any, DeepPartial<z.input<T>>>)
function resolveSchema<T extends z.ZodTypeAny>(
baseSchema: T,
refineSchema: ConditionalRefineSchema<T>
) {
return typeof refineSchema === 'function'
? refineSchema(baseSchema)
: refineSchema
}
export class ConditionalValidator<
T extends z.ZodTypeAny,
Output = z.output<T>,
Input = z.input<T>
> extends z.ZodEffects<T, Output, Input> {
declare _def: ConditionalValidatorDef<T>
constructor(schema: T, checks: ConditionalCheck<any>[]) {
super({
...makePreprocess(schema, checks)._def,
checks,
} satisfies ConditionalValidatorDef<T> as z.ZodEffectsDef<T>)
}
conditionalRefine(
schema: ConditionalRefineSchema<T>,
check: (value: z.output<T>) => unknown,
message: ConditionalRefineMessage<z.output<T>>
) {
return this.conditionalSuperRefine(schema, (value, ctx) => {
if (!check(value)) {
addIssues(ctx, value, message)
}
})
}
conditionalRefineAsync(
schema: ConditionalRefineSchema<T>,
check: (value: z.output<T>) => unknown | Promise<unknown>,
message: ConditionalRefineMessage<z.output<T>>
) {
return this.conditionalSuperRefineAsync(schema, async (value, ctx) => {
if (!(await check(value))) {
addIssues(ctx, value, message)
}
})
}
conditionalSuperRefine(
schema: ConditionalRefineSchema<T>,
check: (value: z.output<T>, ctx: z.RefinementCtx) => void
) {
return new ConditionalValidator(this._def.schema, [
...this._def.checks,
{ schema: resolveSchema(this._def.schema, schema), check, async: false },
])
}
conditionalSuperRefineAsync(
schema: ConditionalRefineSchema<T>,
check: (value: z.output<T>, ctx: z.RefinementCtx) => void | Promise<void>
) {
return new ConditionalValidator(this._def.schema, [
...this._def.checks,
{ schema: resolveSchema(this._def.schema, schema), check, async: true },
])
}
}
export function conditionalValidate<T extends z.ZodTypeAny>(
schema: T
): ConditionalValidator<T> {
return new ConditionalValidator(schema, [])
}
function makePreprocess<T extends z.ZodTypeAny>(
schema: T,
checks: ConditionalCheck<any>[]
): z.ZodEffects<T> {
return z.preprocess((input, ctx) => {
const results = checks.map(({ schema, check, async }) => {
function handleParsed(
parsed: z.SafeParseReturnType<z.input<T>, z.output<T>>
) {
if (!parsed.success) return
return check(parsed.data, ctx)
}
return async
? schema.safeParseAsync(input).then(handleParsed)
: handleParsed(schema.safeParse(input))
})
return checks.some((c) => c.async)
? Promise.all(results).then(() => input)
: input
}, schema)
}
const asArray = <T>(value: T | T[]) => (Array.isArray(value) ? value : [value])
function addIssues<Output>(
ctx: z.RefinementCtx,
value: Output,
message: ConditionalRefineMessage<Output>
) {
const issues = asArray(
typeof message === 'function'
? message(value)
: typeof message === 'string'
? { message }
: message
)
for (const issue of issues) {
ctx.addIssue({ code: z.ZodIssueCode.custom, ...issue })
}
} |
Hey @jedwards1211, I have the same problem with conditional validation and have been following this thread for a long time. I am working on an application that involves a lot of conditional validation, which needs to be written into My example: const { data, success, error } = parentIncomeSchema().safeParse({ ...req.body })
export const parentIncomeSchema = () => {
const schema = z
.object({
'BAFOEG.PARENT_ONE_INCOME_EXIST': z.enum(['YES', 'NO'], {
message: 'Please select an option.',
}),
'BAFOEG.PARENT_ONE_INCOME': z.coerce.number().min(0, 'Please enter a valid amount.').optional(),
'BAFOEG.PARENT_ONE_INCOME_CAPITAL': z.coerce.number().min(0, 'Please enter a valid amount.').optional(),
'BAFOEG.PARENT_ONE_INCOME_MINI': z.coerce.number().min(0, 'Please enter a valid amount.').optional(),
'BAFOEG.PARENT_ONE_INCOME_ABROAD_DEDUCT': z.coerce.number().min(0, 'Please enter a valid amount.').optional(),
'BAFOEG.PARENT_ONE_INCOME_ABROAD': z.string().optional(),
'BAFOEG.PARENT_ONE_INCOME_ABROAD_TAX': z.coerce.number().min(0, 'Please enter a valid amount.').optional(),
'BAFOEG.PARENT_ONE_INCOME_ABROAD_BRUTTO': z.coerce.number().min(0, 'Please enter a valid amount.').optional(),
'BAFOEG.PARENT_ONE_INCOME_PENSION': z.enum(['YES', 'NO', ''], { message: 'Please select an option.' }).optional(),
'BAFOEG.PARENT_ONE_INCOME_PENSION_1_TYPE': z.string().optional(),
'BAFOEG.PARENT_ONE_INCOME_PENSION_1_AMOUNT': z.coerce.number().min(0, 'Please enter a valid amount.').optional(),
'BAFOEG.PARENT_ONE_INCOME_PENSION_1_START': z
.string()
.refine((date) => !date || /^\d{4}-\d{2}-\d{2}$/.test(date), 'Please enter a valid date in the format YYYY-MM-DD.')
.optional(),
'BAFOEG.PARENT_ONE_INCOME_ALIMONY': z.enum(['YES', 'NO', ''], { message: 'Please select an option.' }).optional(),
'BAFOEG.PARENT_ONE_INCOME_ALIMONY_NAME': z.string().optional(),
'BAFOEG.PARENT_ONE_INCOME_ALIMONY_RELATION': z.string().optional(),
'BAFOEG.PARENT_ONE_INCOME_ALIMONY_AMOUNT': z.coerce.number().min(0, 'Please enter a valid amount.').optional(),
'BAFOEG.PARENT_ONE_INCOME_OTHER_BENEFITS': z.enum(['YES', 'NO', ''], { message: 'Please select an option.' }).optional(),
'BAFOEG.PARENT_ONE_INCOME_OTHER_BENEFITS_1_TYPE': z.string().optional(),
'BAFOEG.PARENT_ONE_INCOME_OTHER_BENEFITS_1_AMOUNT': z.coerce.number().min(0, 'Please enter a valid amount.').optional(),
})
.strip()
return z.preprocess((input, ctx) => {
const { data } = schema.safeParse(input)
if (!data) {
return input
}
if (data['BAFOEG.PARENT_ONE_INCOME_EXIST'] === 'YES') {
if (!data['BAFOEG.PARENT_ONE_INCOME_PENSION']) {
ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'Please select an option.', path: ['BAFOEG.PARENT_ONE_INCOME_PENSION'] })
}
if (!data['BAFOEG.PARENT_ONE_INCOME_OTHER_BENEFITS']) {
ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'Please select an option.', path: ['BAFOEG.PARENT_ONE_INCOME_OTHER_BENEFITS'] })
}
}
if (data['BAFOEG.PARENT_ONE_INCOME_ABROAD'] && data['BAFOEG.PARENT_ONE_INCOME_ABROAD'].trim() !== '') {
if (!data['BAFOEG.PARENT_ONE_INCOME_ABROAD_TAX']) {
ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'Please provide the taxes paid abroad.', path: ['BAFOEG.PARENT_ONE_INCOME_ABROAD_TAX'] })
}
if (!data['BAFOEG.PARENT_ONE_INCOME_ABROAD_BRUTTO']) {
ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'Please provide the gross annual income earned abroad.', path: ['BAFOEG.PARENT_ONE_INCOME_ABROAD_BRUTTO'] })
}
}
if (data['BAFOEG.PARENT_ONE_INCOME_PENSION'] === 'YES') {
if (!data['BAFOEG.PARENT_ONE_INCOME_PENSION_1_TYPE']) {
ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'Please provide the type of pension.', path: ['BAFOEG.PARENT_ONE_INCOME_PENSION_1_TYPE'] })
}
if (!data['BAFOEG.PARENT_ONE_INCOME_PENSION_1_AMOUNT']) {
ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'Please provide the pension amount.', path: ['BAFOEG.PARENT_ONE_INCOME_PENSION_1_AMOUNT'] })
}
if (!data['BAFOEG.PARENT_ONE_INCOME_PENSION_1_START']) {
ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'Please provide the start date of the pension.', path: ['BAFOEG.PARENT_ONE_INCOME_PENSION_1_START'] })
}
if (!data['BAFOEG.PARENT_ONE_INCOME_ALIMONY']) {
ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'Please select an option.', path: ['BAFOEG.PARENT_ONE_INCOME_ALIMONY'] })
}
if (data['BAFOEG.PARENT_ONE_INCOME_ALIMONY'] === 'YES') {
if (!data['BAFOEG.PARENT_ONE_INCOME_ALIMONY_NAME']) {
ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'Please provide the name of the person who provided alimony.', path: ['BAFOEG.PARENT_ONE_INCOME_ALIMONY_NAME'] })
}
if (!data['BAFOEG.PARENT_ONE_INCOME_ALIMONY_RELATION']) {
ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'Please provide the relationship.', path: ['BAFOEG.PARENT_ONE_INCOME_ALIMONY_RELATION'] })
}
if (!data['BAFOEG.PARENT_ONE_INCOME_ALIMONY_AMOUNT']) {
ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'Please provide the amount of alimony.', path: ['BAFOEG.PARENT_ONE_INCOME_ALIMONY_AMOUNT'] })
}
}
}
if (data['BAFOEG.PARENT_ONE_INCOME_OTHER_BENEFITS'] === 'YES') {
if (!data['BAFOEG.PARENT_ONE_INCOME_OTHER_BENEFITS_1_TYPE']) {
ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'Please provide the type of benefit.', path: ['BAFOEG.PARENT_ONE_INCOME_OTHER_BENEFITS_1_TYPE'] })
}
if (!data['BAFOEG.PARENT_ONE_INCOME_OTHER_BENEFITS_1_AMOUNT']) {
ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'Please provide the amount of benefit.', path: ['BAFOEG.PARENT_ONE_INCOME_OTHER_BENEFITS_1_AMOUNT'] })
}
}
return input
}, schema)
} |
@alanost Yeah if you use the const schema = conditionalValidate(
z.object({
// I'd recommend putting .trim() on any strings you want to trim in here
...
})
).conditionalRefine(
// pick the properties this refinement depends on
(s) =>
s.pick({
'BAFOEG.PARENT_ONE_INCOME_EXIST': true,
'BAFOEG.PARENT_ONE_INCOME_PENSION': true,
}),
// this function only gets called if the input was successfully
// parsed with the subschema you picked
(data) =>
data['BAFOEG.PARENT_ONE_INCOME_EXIST'] !== 'YES' ||
data['BAFOEG.PARENT_ONE_INCOME_PENSION'],
// this issue gets added if the above function returns false
{
path: ['BAFOEG.PARENT_ONE_INCOME_PENSION'],
message: 'Please select an option.',
}
)
.conditionalRefine(
(s) =>
s.pick({
'BAFOEG.PARENT_ONE_INCOME_EXIST': true,
'BAFOEG.PARENT_ONE_INCOME_OTHER_BENEFITS': true,
}),
(data) =>
data['BAFOEG.PARENT_ONE_INCOME_EXIST'] !== 'YES' ||
data['BAFOEG.PARENT_ONE_INCOME_OTHER_BENEFITS'],
{
path: ['BAFOEG.PARENT_ONE_INCOME_OTHER_BENEFITS'],
message: 'Please select an option.',
}
)
// etc If you do it manually, it would have to be like z.preprocess((input, ctx) => {
function withSafeParse<T extends z.ZodTypeAny, R>(
schema: T,
handler: (data: z.output<T>) => R
): R | void {
const parsed = schema.safeParse(data)
if (parsed.success) return handler(parsed.data)
}
withSafeParse(
schema.pick({
'BAFOEG.PARENT_ONE_INCOME_EXIST': true,
'BAFOEG.PARENT_ONE_INCOME_PENSION': true,
}),
(data) => {
if (
data['BAFOEG.PARENT_ONE_INCOME_EXIST'] === 'YES' &&
!data['BAFOEG.PARENT_ONE_INCOME_PENSION']
) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'Please select an option.',
path: ['BAFOEG.PARENT_ONE_INCOME_PENSION'],
})
}
}
)
// etc for other groups of fields
return input
}, schema) Of course, you might want to extract each This is basically what |
@alanost btw doing one big |
What happened here haha :) what's the solution guy's ? |
@LeulAria starts here: #479 (comment) |
@PiotrSkrzynski88 in your example const obj = z
.object({
foo: z.string().min(1),
})
.superRefine((val, ctx) => {
ctx.addIssue({
message: 'Error from superRefine',
});
});
|
@jedwards1211 I believe that TypeScript shouldn’t justify this behavior or limit functionality here. We should focus on logical consistency and user expectations rather than TypeScript constraints. In my opinion, the issue is that |
Totally agree with you. But lets face the reality, this issue has been around since 2021, apparently they don't want to deal with/change it so we have 2 options:
I remember when I had to face it for the first time and rewrite my entire forms, splitting them up into isolated schemas and creating intersections, it was a pain. Zod's owner has never replied here so... |
I think this issue is not actually a real bug or feature request. If anyone wants to solve this, use That’s the API that’s meant to allow you to add custom errors even if schema validation fails, which is what this issue is about. Maybe this should be mentioned in the docs more clearly to prevent further confusion. |
@PiotrSkrzynski88 it's not about TypeScript for TypeScript's sake, it's about not giving people a false expectation about the value type so they don't accidentally create bugs. If Zod called For anyone just getting here see #479 (comment) |
|
@Effanuel it doesn't actually override, just seems like that because it doesn't even
Where
I guess a way to mark a In the meantime, if you want to be able to get all the refinement errors together, you would have to refactor from
to (I haven't type checked this, but you get the gist):
|
I genuinely don't understand why .superRefine()/.refine() is a problem meanwhile everything else like .min() or .max() can run just fine and all the errors are present withing issues array. Is there no sane workaround? No plans to at least maybe add something like idk .refineImmediately()? This is such a disappointing issue to deal with and having to deal with this after selling zod to the team is just sad :/ |
@mk26710 I feel the same way. According to the documentation,
Refinements are documented as:
Given this, performing custom validation with |
For anyone just getting here see #479 (comment)
Min/max can run as long as the field is valid, even if other fields aren't. It's the same if you put refinements on an individual field. The refinements won't run if the value doesn't parse (min/max or custom refinements won't run if the value isn't a number) It's the same if you put refinements on an entire object. The refinements won't run if the value doesn't parse (doesn't have all the required fields, or some fields fail to parse) It just seems different because you're only thinking about when the fields you care about are valid, but Zod doesn't know which fields those are. The solution is to specify which fields you care about, as in my This is a fundamental challenge in form validation, it's not just specific to Zod. For example, in
Transforms and refinements are designed to operate on parsed (output) values. "Preprocessing" is just doing operations on the raw unparsed (input) values. When transforming, the input could be wrong and untransformable; that's why |
likely switching to yup because of zod lacks this feature. would really, really appreciate its addition! |
Applies to zod v3. I have not tried the same scenario on older versions.
I have two dates,
startDate
andendDate
and I want to verify that the former is always earlier than the latter. I tried doing something like this:The problem is that the
.refine()
function defined on theobject
type doesn't get triggered until the all fields in the object are defined. So, imagine you have a form and user entered bothstartDate
andendDate
first, but in a wrong order. At this point, the validations for these dates do not trigger because user has not enteredname
. Then, user goes ahead entersname
and user now sees the validations for the dates kicking in. This is very awkward experience.I can see that in this issue #61, the example adds a
.partial()
to get around this problem, but this really isn't a solution because all of these fields are required in my case. It'd be nice if the.refine
function defined on each individual field had access to the current form values, but it has access to its own value only. Any ideas, or did I miss something?The text was updated successfully, but these errors were encountered: