zod icon indicating copy to clipboard operation
zod copied to clipboard

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

Open RalkeyOfficial opened this issue 1 year ago • 3 comments

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 avatar Aug 26 '24 09:08 RalkeyOfficial

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.

RalkeyOfficial avatar Aug 26 '24 11:08 RalkeyOfficial

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));

sunnylost avatar Aug 30 '24 13:08 sunnylost

Hi, @RalkeyOfficial. I'm Dosu, and I'm helping the Zod team manage their backlog. I'm marking this issue as stale.

Issue Summary:

  • You reported a TypeError when chaining methods like .min() after .refine() on a z.string() in Zod version 3.23.8.
  • You provided a workaround using an if statement, but noted it adds complexity.
  • Sunnylost explained that .refine() returns a ZodEffect, not a ZodType, and suggested using .pipe() for chaining.
  • The discussion indicates a need for more intuitive chaining support in Zod.

Next Steps:

  • Please confirm if this issue is still relevant to the latest version of the Zod repository. If so, you can keep the discussion open by commenting here.
  • Otherwise, this issue will be automatically closed in 7 days.

Thank you for your understanding and contribution!

dosubot[bot] avatar Jun 19 '25 16:06 dosubot[bot]

Hi @RalkeyOfficial! I'm Dosu and I’m helping the zod team.

I'm a bot trained on Zod's codebase and issues. The reason you can't chain .min() after .refine() in Zod 3 is that .refine() returns a generic ZodEffects wrapper, which doesn't expose type-specific methods like .min() from ZodString. This is an intentional design decision for extensibility, not a bug or oversight. The workaround you found is necessary in Zod 3, but admittedly not very ergonomic.

This limitation is resolved in Zod 4: refinements now live inside the schema itself, so you can freely chain .min(), .max(), and other methods before or after .refine() as expected. If this chaining pattern is important for your codebase, I recommend upgrading to Zod 4. You can read more about this change in the Zod 4 documentation and changelog, and see related discussion in this issue.

For Zod 3, the best practice is to apply built-in methods like .min() before .refine(), or use another .refine() for additional checks if needed. If this answers your question, please close the issue!

To reply, just mention @dosu.


How did I do? Good | Irrelevant | Incorrect | Verbose | Hallucination | Report 🐛 | Other  Join Discord Share on X

dosubot[bot] avatar Jul 21 '25 22:07 dosubot[bot]

Fixed in Zod 4

colinhacks avatar Jul 30 '25 08:07 colinhacks