-
-
Notifications
You must be signed in to change notification settings - Fork 200
Description
Is your feature request related to a problem? Please describe.
When using any resolver together with conditional fields (fields that may or may not be rendered depending on user input), I noticed that the resolver has no awareness of which fields are actually rendered on the screen.
As a result, it validates all fields defined in the schema — even those that aren’t currently visible or mounted.
This leads to a very frustrating user experience:
a user can end up in a situation where they cannot submit the form, cannot see any validation message, and cannot understand what’s wrong, because the validation errors belong to fields that don’t exist in the current UI.
In other words, the resolver in its current form creates two major issues:
- It breaks centralized form management.
- It forces schema authors to write unreadable validation logic full of complex refinements and conditions for fields that may not even be active — making schemas extremely hard to maintain.
Describe the solution you'd like
Resolvers should have awareness of currently rendered (registered) fields.
Since the resolver already receives actual values (especially when shouldUnregister = true), it can infer which fields are currently active and restrict validation accordingly.
That alone would solve most issues with conditional forms without requiring any schema-level hacks or complicated conditional refinements.
The following code may be a solution:
import { zodResolver } from "@hookform/resolvers/zod";
import { FieldValues } from "react-hook-form";
import { ZodObject, object } from "zod";
export const zodAwareResolver = ((...args: Parameters<typeof zodResolver>) => {
const [schema, schemaOptions] = args;
return ((values, context, options) => {
if (Object.keys(values).length === 0)
return {
values: {},
errors: {
"": {
type: "custom",
message: "",
},
},
};
const pickedSchema =
schema instanceof ZodObject ? deepPickByValues(schema, values) : schema;
return zodResolver(pickedSchema, schemaOptions)(values, context, options);
}) as ReturnType<typeof zodResolver>;
}) as typeof zodResolver;
function deepPickByValues<Schema extends ZodObject<any>>(
schema: Schema,
values: FieldValues,
) {
function reducer(
resultSchema: ZodObject,
sourceSchema: ZodObject,
path: string,
): ZodObject {
const dotIndex = path.indexOf(".");
if (dotIndex === -1)
return resultSchema.extend(sourceSchema.pick({ [path]: true }).shape);
const shapeKey = path.slice(0, dotIndex);
const pathEnd = path.slice(dotIndex + 1);
const nestedResultSchema = resultSchema.shape[shapeKey] ?? object();
const nestedSourceSchema = sourceSchema.shape[shapeKey];
return resultSchema.extend({
[shapeKey]: reducer(nestedResultSchema, nestedSourceSchema, pathEnd),
});
}
const paths = getDeepKeys(values);
return paths.reduce(
(resultSchema, path) => reducer(resultSchema, schema, path),
object(),
) as Schema;
}
function getDeepKeys(obj: Record<string, any>, prefix = ""): string[] {
const keys: string[] = [];
for (const key in obj) {
if (!Object.prototype.hasOwnProperty.call(obj, key)) continue;
const value = obj[key];
const path = prefix ? `${prefix}.${key}` : key;
if (value && typeof value === "object" && !Array.isArray(value)) {
keys.push(...getDeepKeys(value, path));
} else {
keys.push(path);
}
}
return keys;
}So the main idea is to implement schema deep pick by values passed to resolver.
With this wrapper schema like this
z
.object({
comment: z.string().nonempty("Comment required"),
addToTarget: z.boolean(),
fraudTypology: z
.custom<FraudTypology>(
v => typeof v === "string",
"Fraud typology is required",
)
.optional(),
})
.refine(data => data.addToTarget && !data.fraudTypology, {
error: "Fraud typology is required",
path: ["fraudTypology"],
});can be transform to schema like this
z.object({
comment: z.string().nonempty("Comment required"),
reason: z.string().nonempty("Reason required"),
addToTarget: z.boolean(),
fraudTypology: z.string<FraudTypology>("Fraud typology is required"),
});which looks much more simple.
This schema says to developer "fraudTypology field must be typeof FraudTypology, but ONLY when it is on screen". So the developer doesn't even think about the validation of field when it is not on the screen.
Describe alternatives you've considered
Using form-level refine conditions to mimic field awareness — becomes redundant, unreadable and unmaintainable for complex forms.
Additional context
This issue is reproducible with all major resolvers (zodResolver, yupResolver, etc.), as they don’t have built-in awareness of what’s actually rendered.
The problem is especially visible with shouldUnregister = true, since the values already represent the active form state — the resolver simply ignores that fact.