Skip to content

Resolvers cause more problems than they solve when using conditional fields #823

@deviant310

Description

@deviant310

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:

  1. It breaks centralized form management.
  2. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions