Zod Schema: How to make a field optional OR have a minimum string contraint?
Asked Answered
A

6

42

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?

Arthrospore answered 2/9, 2022 at 12:0 Comment(0)
E
41

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 );
Extraterritoriality answered 11/10, 2022 at 13:3 Comment(3)
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.Faller
@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.Extraterritoriality
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).Philosophy
T
53

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('')),
});
Tweeter answered 23/11, 2022 at 12:22 Comment(1)
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.Dirk
E
41

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 );
Extraterritoriality answered 11/10, 2022 at 13:3 Comment(3)
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.Faller
@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.Extraterritoriality
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).Philosophy
F
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" }));
Faller answered 2/9, 2022 at 12:33 Comment(6)
It's not working for me for some reason. If my field is empty it still fails.Arthrospore
Working for me on stackblitz using the latest zod versionFaller
Weird, must be something else in my implementation affecting it, I'm resorting to using the following regex to solve the issue: /^(\S{4,})?$/ thanks for the help though, I'm sure your solution is correct in normal contexts!Arthrospore
I had the exact same issue, did you ever find a solution apart from the regex?Brott
Nah we actually just decide to fully move away from using zod schemas because of issues like this...Arthrospore
@CamParry See if the answer I just posted can help you.Extraterritoriality
S
2

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);
Shem answered 17/8, 2023 at 19:33 Comment(0)
P
2

Ilmari Kumpula's is a good solution.

Best way to use zod effectively is to follow the logical flow.

OPINION ONLY: If you are working on a medium+ size project, consider using joi or even writing your own as you go if you have the resources. We have run into many different issues with Zod and have had to implement that many different solutions.

The options we have work, but they get more convoluted the more you add. One of the major ones is having an optional field that can become required, ie, if yes, then now this field is required. There are ways to do it, but not pretty.

We are that far into the project now its going to be a nightmare to change it all, and the testers dont want to regression test everything. We have made around 30 forms so far, with tons of fields, some optional until another option is selected, using refine or superRefine is not very helpful, it doesnt validate until the first parse unless you make sure the error type doesnt return invalid_type. Preprocess works in some cases, discriminated unions in others.

We are purely typescript and this library helps with that, but the effort required in the long run hasnt been worth it.

Some of the outcomes we have found is using literals and discriminated unions can solve a lot of problems you might be scratching your head over. transform and piping also uselful.

We have written a wrapper type function that uses a swiss army knife type field with the options you want to have, ie: name: asCreate({type: string, conditional: true, conditionalField: otherField, min, max, etc....}),

which then runs through the function an returns the built schema type, using a combo of if else, refine, superrefine, transform, pipe, zod functions, disc unions.

No offense to the authors and contributors, but I feel it needs a rewrite to be more concise and simplified. And the parse don't validate forced opinion has it drawbacks.

Having said all that, our forms are very smart and typesafe, and the zod schemas are the source of truth for the app, it also works perfect as a resolver with react-hook-form, types for TRPC, and keeping dexie indexed db safe too. It just took a ton of unit tests for all of our cases before we got it right.

Plymouth answered 28/9, 2023 at 6:3 Comment(1)
I'm using Yup with my next project and it works a lot better.Arthrospore
B
0

Here's a reusable function to make a field optional where empty strings are transformed to undefined:

import { z } from "zod";

function optional<T extends z.ZodTypeAny>(schema: T) {
    return z
        .union([schema, z.literal("")])
        .transform((value) => (value === "" ? undefined : value))
        .optional();
}

const myString = optional(z.string().min(4));
type MyString = z.infer<typeof myString>; // string | undefined
Birt answered 5/6, 2024 at 9:21 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.