colinhacks / zod

TypeScript-first schema validation with static type inference
https://zod.dev
MIT License
33.86k stars 1.18k forks source link

Unable to Chain .min() and Other Validation Methods After .refine() on z.string() #3725

Open RalkeyOfficial opened 2 months ago

RalkeyOfficial commented 2 months ago

Zod version: 3.23.8

When writing the following Zod schema:

z.string()
  .refine((val) => val === "John", {
    message: "String must be equal to 'John'",
  })
  .min(1)

I expected this validation code to be valid and check whether the string has a minimum length of 1. However, instead of that, I get an error stating that .min() is not a function. This error suggests that chaining .min() or other validation methods after .refine() is not supported.

Expected Behavior:

I expect to still be able to use .min() and other validation methods (such as .max(), etc.) after using .refine(). This would allow me to build more complex validation rules without running into method chaining issues.

Current Behavior:

Currently, attempting to chain .min() (or similar validation methods) after .refine() on z.string() results in an error: TypeError: .min is not a function. This behavior limits the flexibility of combining custom validation logic with built-in methods.

Use Case:

In my codebase, I rely on the ability to combine custom validation logic using .refine() with built-in methods such as .min(). Being able to chain these methods makes the code more concise and maintainable.

Proposed Solution:

It would be helpful if .min(), .max(), and similar validation methods were still accessible after using .refine(), or if there were an alternative approach to achieve the same result.

Request for Feedback:

I would appreciate feedback on whether this behavior is intentional, and if so, whether there are recommended workarounds or plans to support this kind of chaining in the future.

RalkeyOfficial commented 2 months ago

I've come up with a temporary workaround using an if statement to check whether the .min() method is available:

import z from "zod";

let schema = z.string();

schema = schema.refine((val) => val === "John", {
  message: "String must be equal to 'John'",
});

if (schema.min) {
  console.log("min is available to use");
  schema = schema.min(1);
} else {
  console.log("min is NOT available to use");
  schema = schema.refine((val) => val.length >= 1);
}

console.log(schema.safeParse("John"));

While this approach works, it adds unnecessary complexity and feels a bit messy. It would be much more straightforward if chaining methods like .min() after .refine() were supported natively in Zod.

sunnylost commented 2 months ago

After you use something like refine() or transform(), the return value will be a ZodEffect, not a ZodType anymore. While you can use pipe() to chain another schema, it can seem verbose in this scenario.

z
  .string()
  .refine((val) => val === "John", {
    message: "String must be equal to 'John'",
  })
  .pipe(z.string().min(10));