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

Feature Request: Yup-like Conditional Schema Validation with .when() #3874

Open
9nitK opened this issue Nov 29, 2024 · 0 comments
Open

Feature Request: Yup-like Conditional Schema Validation with .when() #3874

9nitK opened this issue Nov 29, 2024 · 0 comments

Comments

@9nitK
Copy link

9nitK commented Nov 29, 2024

Proposal: Add Yup-like Conditional Schema Validation (.when())

Problem Statement

Zod currently lacks an elegant way to handle conditional validation similar to Yup's .when() method. This makes it challenging to implement dependent field validations, especially in complex forms where validation rules depend on other field values.

Current Solutions and Limitations

1. Using .refine()

// Current approach using refine
const schema = z.object({
  employmentType: z.enum(['employed', 'self-employed', 'unemployed']),
  companyName: z.string().refine(
    (val, ctx) => {
      const employmentType = ctx.parent.employmentType;
      if (employmentType === 'employed' && !val) {
        return false;
      }
      return true;
    },
    { message: 'Company name is required for employed individuals' }
  )
});

2. Using .superRefine()

const schema = z.object({
  employmentType: z.enum(['employed', 'self-employed', 'unemployed']),
  companyName: z.string(),
}).superRefine((data, ctx) => {
  if (data.employmentType === 'employed' && !data.companyName) {
    ctx.addIssue({
      code: z.ZodIssueCode.custom,
      message: "Company name is required for employed individuals",
      path: ["companyName"]
    });
  }
});

3. Using Separate Schemas with Union

const employedSchema = z.object({
  employmentType: z.literal('employed'),
  companyName: z.string().min(1, "Company name is required")
});

const unemployedSchema = z.object({
  employmentType: z.literal('unemployed'),
  companyName: z.string().optional()
});

const schema = z.discriminatedUnion("employmentType", [
  employedSchema,
  unemployedSchema
]);

Limitations of Current Approaches

  • Verbose and complex code
  • Less readable and maintainable
  • Harder to reuse validation logic
  • Not as intuitive as Yup's approach
  • Difficult to handle multiple dependent fields

Proposed Solution

1. Basic .when() Implementation

// Simple field dependency
const schema = z.object({
  employmentType: z.enum(['employed', 'self-employed', 'unemployed']),
  companyName: z.string().when('employmentType', {
    is: 'employed',
    then: (schema) => schema.min(1, "Company name is required"),
    otherwise: (schema) => schema.optional()
  })
});

2. Multiple Field Dependencies

// Multiple field dependencies
const schema = z.object({
  country: z.string(),
  state: z.string(),
  zipCode: z.string().when(['country', 'state'], {
    is: ({country, state}) => country === 'US' && state === 'CA',
    then: (schema) => schema.regex(/^\d{5}$/, "Must be 5 digits for US/CA"),
    otherwise: (schema) => schema.optional()
  })
});

3. Array-based Validation

// Array validation
const schema = z.object({
  hasPhones: z.boolean(),
  phones: z.array(z.string()).when('hasPhones', {
    is: true,
    then: (schema) => schema.min(1, "At least one phone required"),
    otherwise: (schema) => schema.max(0)
  })
});

4. Chaining Multiple Conditions

// Chained conditions
const schema = z.object({
  age: z.number(),
  income: z.number()
    .when('age', {
      is: (age) => age >= 18,
      then: (schema) => schema.min(1000)
    })
    .when('age', {
      is: (age) => age >= 65,
      then: (schema) => schema.min(2000)
    })
});

Type Safety

// Type-safe implementation
type WhenOptions<T> = {
  is: boolean | ((value: any) => boolean);
  then: (schema: z.ZodType<T>) => z.ZodType<T>;
  otherwise?: (schema: z.ZodType<T>) => z.ZodType<T>;
};

declare module "zod" {
  interface ZodType<T> {
    when<U>(
      field: string | string[],
      options: WhenOptions<T>
    ): ZodType<T>;
  }
}

Implementation Considerations

Path Resolution:

  • Support for deep path resolution (e.g., 'user.profile.type')
  • Array index access
  • Multiple field dependencies

Type Inference:

  • Maintain type safety throughout conditions
  • Handle optional fields correctly
  • Support for discriminated unions

Performance:

  • Efficient validation of dependent fields
  • Caching of intermediate results
  • Minimal overhead compared to current solutions

Examples of Complex Use Cases

1. Form with Dynamic Sections

const formSchema = z.object({
  formType: z.enum(['personal', 'business']),
  personalInfo: z.object({
    name: z.string(),
    ssn: z.string()
  }).when('formType', {
    is: 'personal',
    then: (schema) => schema.required(),
    otherwise: (schema) => schema.optional()
  }),
  businessInfo: z.object({
    companyName: z.string(),
    ein: z.string()
  }).when('formType', {
    is: 'business',
    then: (schema) => schema.required(),
    otherwise: (schema) => schema.optional()
  })
});

2. Multi-step Validation

const multiStepSchema = z.object({
  step: z.number(),
  email: z.string().email().when('step', {
    is: (step) => step >= 1,
    then: (schema) => schema.required(),
  }),
  password: z.string().when('step', {
    is: (step) => step >= 2,
    then: (schema) => schema.min(8),
  }),
  profile: z.object({
    bio: z.string()
  }).when('step', {
    is: (step) => step >= 3,
    then: (schema) => schema.required(),
  })
});

Benefits

  • More declarative validation logic
  • Better code organization
  • Easier migration from Yup
  • Improved readability
  • Better maintainability
  • Reduced boilerplate
  • Type-safe conditional validation

Next Steps

  1. Gather community feedback on the API design
  2. Create proof of concept implementation
  3. Write comprehensive tests
  4. Document edge cases and limitations
  5. Create migration guide from Yup

Questions for Discussion

  1. Should we support async validation in .when() conditions?
  2. How should we handle circular dependencies?
  3. Should we support multiple .when() conditions on the same field?
  4. How can we optimize performance for large forms?
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant