zod icon indicating copy to clipboard operation
zod copied to clipboard

Parse a boolean value represented by string logically

Open jlandowner opened this issue 1 year ago • 8 comments

From https://github.com/colinhacks/zod/issues/1630#issuecomment-1824670600

Currently z.boolean() simply replesents Boolean() for coerce. So it handles string value as boolean along with the standard Boolean() specification. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Boolean#boolean_coercion

const b = z.coerce.boolean()
b.parse("true");    // true
b.parse("false");   // true
b.parse("True");    // true
b.parse("False");   // true
b.parse("1");       // true
b.parse("0");       // true
b.parse("");        // false
b.parse(undefined); // false
b.parse("others");  // true

However we often need to handle string as boolean, "false" or "0" as false, such as parsing environment valiables.

Current workaround is like

const b1 = z.string().toLowerCase().transform(JSON.parse).pipe(z.boolean());
b1.parse("true");   // true
b1.parse("false");  // false
b1.parse("True");   // true
b1.parse("False");  // false
b1.parse("tRue");   // true
b1.parse("fAlse");  // false
b1.parse("1");      // ZodError
b1.parse("0");      // ZodError
b1.parse("others"); // ZodError

My suggestion is a new string transformation: z.string().boolean()

The transforming specification in Go standard library's strconv.ParseBool() is good for it.

https://pkg.go.dev/strconv#ParseBool

ParseBool returns the boolean value represented by the string. It accepts 1, t, T, TRUE, true, True, 0, f, F, FALSE, false, False. Any other value returns an error.

// Suggestions
const b1 = z.string().boolean()
b1.parse("true");    // true
b1.parse("false");   // false
b1.parse("True");    // true
b1.parse("False");   // false
b1.parse("tRue");    // ZodError in boolean transformation
b1.parse("fAlse");   // ZodError in boolean transformation
b1.parse("1");       // true
b1.parse("0");       // false
b1.parse("");        // ZodError in boolean transformation
b1.parse(undefined); // ZodError by string() as required
b1.parse("others");  // ZodError in boolean transformation

// false for undefined string
const b2 = z.string().boolean().default("false")
b2.parse(undefined); // false

// false for any boolean transformation errors and string errors
const b3 = z.string().boolean().catch(false)
b3.parse("tRue");    // false
b3.parse("fAlse");   // false
b3.parse("");        // false
b3.parse(undefined); // false
b3.parse("others");  // false

jlandowner avatar Nov 23 '23 18:11 jlandowner

const b1 = z.string().toLowerCase().transform(JSON.parse).pipe(z.boolean());

If there is such a simple work around, then why should this be a part of zod core?

JacobWeisenburger avatar Nov 24 '23 01:11 JacobWeisenburger

@JacobWeisenburger Thank you for your feedback.

My opinions are:

  • z.string().toLowerCase().transform(JSON.parse).pipe(z.boolean()) is not clear for users. As you can see #1630, many users are struggling to do this in different ways and sometimes mistake in their programs. It is worth to be on the lineup of builtin string transformation and helps many users in the future.
  • It is gereral to set "1" or "0" as a boolean in environment variables for lot of tools or products but JSON.parse() does not works. I think boolean is a last piece of zod advantage in the area of environment variables parsing.
const envSchema = z.object({
  SERVER_URL: z.string().min(5, {message: "Required"}),
  SOMETHING_VALUE: z.string(),
  EXPIRE_DAYS: z.coerce.number().default(30),
  SOMETHING_NUMERIC_CONFIG: z.coerce.number().min(1),
  DEBUG: z.string().boolean(),
  ENABLE_SOMETHING: z.string().boolean(),
});
const env = envSchema.parse(Deno.env.toObject());

I beleave that it is not much complex implementation. Sorry for my previous comment with lack of understaing of the zod architecutre. Now I am deepdiving zod codes.

jlandowner avatar Nov 24 '23 02:11 jlandowner

This is a really good idea. I lost a couple hours of debugging today trying to figure out why "false" provided as a string to a ZOD parse function was resulting in "true" for a z.boolean() value. I know it is fault of Boolean('false') being truthy, but this is a foot gun. No one reasonable expects "false" to be parsed as Boolean true.

bhouston avatar Jan 15 '24 15:01 bhouston

@jlandowner this is both useful for environment variable parsing as well as URL query parameter parsing. It is incredibly common to have "feature=true" or "feature=false" on an URL and for that to parse as always "true" by default is sort of crazy.

bhouston avatar Jan 15 '24 15:01 bhouston

Also I should add that this bug isn't present with Joi. So when I converted over to Zod from Joi, this was a huge surprise.

bhouston avatar Jan 15 '24 15:01 bhouston

A really quick heads up, rather than invoking the entire JSON.parse the transform can also be just

z.string().toLowerCase().transform((x) => x === 'true').pipe(z.boolean())

Soviut avatar Jan 23 '24 09:01 Soviut

This issue is for a feature request and the implementation in PR #2989.

If you would like to discuss about workarounds, #1630 is a better place.

jlandowner avatar Mar 02 '24 06:03 jlandowner

This is what is working for me:

z.enum(['true', 'false']).transform((value) => value === 'true')

bmunoz89 avatar Mar 27 '24 18:03 bmunoz89

I didn't find any working solutions for my use case (query params), but I modified one example from here

export const BooleanStringZod = z
  .string()
  .refine((value) => value === 'true')
  .transform((value) => value === 'true');

Now it does what I want. string "true" turns into boolean true, everything else is false and basically fails.

EDIT:

For my use-case, that actually was the wrong answer. I should have just done this:

export const BooleanStringZod = z.preprocess((val) => val === 'true', z.boolean()).default(false);

This code won't fail on any input and just returns false if you don't pass it "true"-value. Not the "optimal" use of Zod for now, but it fits into my schema, where I just want to convert some JSON string-fields from another API into booleans

IlmariKu avatar Apr 30 '24 12:04 IlmariKu

See https://github.com/colinhacks/zod/pull/2989#issuecomment-2091965974

jlandowner avatar May 03 '24 01:05 jlandowner

I'm going to leave a comment here in case anyone has the same problem as me and finds this same GitHub issue on Google.

I needed a schema that handles both boolean values and boolean strings such as "true" and "false", turning them into a true boolean. The solution I found was to use z.preprocess:

const schemas = {
    foo: z.preprocess((val) => {
        if (typeof val === "string") {
            if (val.toLowerCase() === "true") return true;
            if (val.toLowerCase() === "false") return false;
        }
        return val;
    }, z.boolean()),
}

vitorklock avatar Jul 16 '24 11:07 vitorklock

I'm going to leave a comment here in case anyone has the same problem as me and finds this same GitHub issue on Google.

I needed a schema that handles both boolean values and boolean strings such as "true" and "false", turning them into a true boolean. The solution I found was to use z.preprocess:

const schemas = {
    foo: z.preprocess((val) => {
        if (typeof val === "string") {
            if (val.toLowerCase() === "true") return true;
            if (val.toLowerCase() === "false") return false;
        }
        return val;
    }, z.boolean()),
}

Like a charm! Thank you.

leandronn avatar Jul 24 '24 18:07 leandronn