Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Parse a boolean value represented by string logically #2985

Closed
jlandowner opened this issue Nov 23, 2023 · 16 comments
Closed

Parse a boolean value represented by string logically #2985

jlandowner opened this issue Nov 23, 2023 · 16 comments
Labels
enhancement New feature or request

Comments

@jlandowner
Copy link

jlandowner commented Nov 23, 2023

From #1630 (comment)

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
@JacobWeisenburger
Copy link
Contributor

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 JacobWeisenburger added the enhancement New feature or request label Nov 24, 2023
@JacobWeisenburger JacobWeisenburger changed the title [Feature request] Parse a boolean value represented by string logically Parse a boolean value represented by string logically Nov 24, 2023
@jlandowner
Copy link
Author

jlandowner commented Nov 24, 2023

@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 [Suggestion] Stricter boolean coerce option #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.

@bhouston
Copy link

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
Copy link

@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
Copy link

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.

@Soviut
Copy link

Soviut commented Jan 23, 2024

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

@jlandowner
Copy link
Author

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.

@bmunoz89
Copy link

This is what is working for me:

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

bodinsamuel added a commit to NangoHQ/nango that referenced this issue Apr 25, 2024
## Describe your changes

- Zod can't coerce boolean colinhacks/zod#2985
I only tested with true-ish values, that what's you got when not fuzzing
unit test

- Do not log runner logs in Persist

- Trace opensearch in datadog
@IlmariKu
Copy link

IlmariKu commented Apr 30, 2024

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

@jlandowner
Copy link
Author

See #2989 (comment)

@jlandowner jlandowner closed this as not planned Won't fix, can't repro, duplicate, stale May 3, 2024
@vitorklock
Copy link

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()),
}

@leandronn
Copy link

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.

@ThomasDupont
Copy link

Workaround works with :
true, 'true', 'True', false, 'false', 'False', 0, 1, '0', '1':

const castStringToBool = z.preprocess((val) => {
  if (typeof val === "string") {
    if (['1', 'true'].includes(val.toLowerCase())) return true;
    if (['0', 'false'].includes(val.toLowerCase())) return false;
  }
  return val;
}, z.coerce.boolean());

perfect for env vars

iainsproat added a commit to specklesystems/speckle-server that referenced this issue Dec 6, 2024
@AlexSapoznikov
Copy link

AlexSapoznikov commented Dec 16, 2024

const castStringToBool = z.preprocess((val) => {
 if (typeof val === "string") {
  if (['1', 'true'].includes(val.toLowerCase())) return true;
   if (['0', 'false'].includes(val.toLowerCase())) return false;
 }
 return val;
}, z.coerce.boolean());

The thing is you can not really re-use this if you'd like to do:
castStringToBool.default(true)
Or make it strict and be able to apply nullish()

@ilyazub
Copy link

ilyazub commented Feb 9, 2025

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

Thank you @IlmariKu! This coercion finally worked for me today.

@alif-devcon
Copy link

Using the following schema, which is be better than val === 'true' as any invalid values will be flagged.

export const BooleanStringZod = z.union([
    z.literal('true').transform(() => true),
    z.literal('false').transform(() => false),
    z.boolean(),
]);

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet