20

I have a field where I want the value to either be optional OR have the field have a minimum length of 4.

I've tried the following:

export const SocialsSchema = z.object({
  myField: z.optional(z.string().min(4, "Please enter a valid value")),
});

This passes if I used a value like: "good", but if I've got an empty value then it fails.

How do I correctly implement a constraint using zod schemas to make an optional value with a minimum constraint if the value is not empty?

Is it possible to do this without using regex or a regex solution the only way?

Barry Michael Doyle
  • 9,333
  • 30
  • 83
  • 143

4 Answers4

26

In your case, you consider "" to be the same as undefined (i.e.: when the string is empty, it's like there's no string at all).

It's implementable in Zod this way:

import { z } from "zod";
import { strict as assert } from "node:assert";

// `myString` is a string that can be either optional (undefined or missing),
// empty, or min 4
const myString = z
  .union([z.string().length(0), z.string().min(4)])
  .optional()
  .transform(e => e === "" ? undefined : e);

const schema = z.object({ test: myString });

assert( schema.parse({}).test === undefined ); // missing string
assert( schema.parse({ test: undefined }).test === undefined ); // string is undefined
assert( schema.parse({ test: "" }).test === undefined ); // string is empty
assert( schema.parse({ test: "1234" }).test === "1234" ); // string is min 4

// these successfully fail
assert( schema.safeParse({ test: "123" }).success !== true );
assert( schema.safeParse({ test: 3.14 }).success !== true );
Luca Polito
  • 2,387
  • 14
  • 20
  • Nice! Not sure why mine wasn't working for them. It seemed fine on StackBlitz based on my code snippet above but maybe I didn't read the spec well enough. – Robert Rendell Oct 12 '22 at 08:23
  • 2
    @RobertRendell OP wanted to treat an empty string `""` the same as `undefined`, but for Zod an empty string is still a valid non-missing string, therefore the validation is a little bit more tricky. – Luca Polito Oct 12 '22 at 09:35
  • 4
    Thank you, this does trick. However, might want to change the order to `[z.string().min(4), z.string().length(0)]` so that the error message for `min(4)` takes precedence over `length(0)`. – Drew Goodwin Nov 06 '22 at 07:30
18

Based on this Github issue and it's answer

Use the or-option in-combined with optional & literal, like this.

export const SocialsSchema = z.object({
  myField: z
    .string()
    .min(4, "Please enter a valid value")
    .optional()
    .or(z.literal('')),
});
Dennis
  • 56,821
  • 26
  • 143
  • 139
Ilmari Kumpula
  • 831
  • 1
  • 10
  • 18
  • Thanks! I was looking for `scheme_type: z.number().or(z.literal("")),` where MUI select required a default empty string, but I wanted the value to remain a number. – deadbyte May 24 '23 at 21:21
5

Here you are:

import { z } from "zod";

export const SocialsSchema = z.object({
  myField: z.string().min(4, "Please enter a valid value").optional()
});
// ok
console.log(SocialsSchema.parse({ myField: undefined }));

// ok
console.log(SocialsSchema.parse({ myField: "1234" }));

// ok
console.log(SocialsSchema.parse({ myField: "" }));

// throws min error
console.log(SocialsSchema.parse({ myField: "123" }));
Robert Rendell
  • 1,658
  • 15
  • 18
1

The current top answer is good but has one flaw. The Zod error message for a string that does not match the constraint will come from z.string().length(0) as opposed to z.string().min(4).

By reversing the order of the union the precedence of the error will be corrected.

import { z } from "zod";

// `myString` is a string that can be either optional (undefined or missing),
// empty, or min 4
const myString = z
  .union([z.string().min(4), z.string().length(0)])
  .optional()
  .transform(e => e === "" ? undefined : e);
ChrisGPT was on strike
  • 127,765
  • 105
  • 273
  • 257
tschumann
  • 11
  • 2