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

Using checkboxes with zod is confusing #345

Open
fiws opened this issue Nov 22, 2023 · 7 comments
Open

Using checkboxes with zod is confusing #345

fiws opened this issue Nov 22, 2023 · 7 comments

Comments

@fiws
Copy link
Contributor

fiws commented Nov 22, 2023

Describe the bug and the expected behavior

Checkboxes do not behave as expected.

If you use a checkbox with zod you get the following behaviour:

  • z.boolean() – checking the checkbox is required. Unchecking it gives you a validation error
    • A bit unexpected, but i can work with this
  • z.boolean().optional() – checkbox not required, unchecking it gives you undefined
    • I wanted the checkbox to give me false
  • z.boolean().optional().refine(v => v ?? false) – Gives "Invalid input" during validation
    • I'm just trying to convert "undefined" to false here
    • This is already pretty ugly

I realize that this might not be solvable in conform directly. In that case there should be some documentation for this IMO. This is a pretty common pattern, no?

Conform version

v0.9.1

Steps to Reproduce the Bug or Issue

https://codesandbox.io/p/sandbox/infallible-bhabha-h5sxxp?file=%2Fsrc%2FApp.tsx%3A31%2C36

What browsers are you seeing the problem on?

No response

Screenshots or Videos

No response

Additional context

No response

@edmundhung
Copy link
Owner

Definitely agree that the type coercion behaviour should be documented. Would you be interested in contributing? I think we can have a dedicated guide about type coercion.

FYI, to set a default value with zod, you should use .transform(v => v ?? false). .refine() is meant for validation and Invalid input is the default error message.

With regards to the behaviour, what you found is pretty much correct. This is mainly because FormData express everything in the form of value. There is no false in the FormData and all we know is if an entry with a specific name and value exists. The server cannot tell whether the checkbox is unchecked or if it is never rendered in the form. Conform also make an additional assumption that you haven't customize the checkbox value. As it considers the result true only when the value is on (The default value of checkbox / radio button).

I am happy to adjust the behaviour if we believe there is a better approach. Here is a few concerns I had if we default the value to false:

  • We need to use .refine() to mark a checkbox as required (e.g. z.boolean().refine(value => value, 'Required'))
  • There is no different with z.boolean().optional() anymore as there will always be a value.
  • Zod has a feature called errorMap which you can map both standard and custom error using a specific code. After this change, boolean no longer works with the standard required code as it is now a custom validation (i.e. .refine())

@fiws
Copy link
Contributor Author

fiws commented Nov 23, 2023

My bad with the refine/transform. I think more/better documentation is the way to go here. Having even more special behaviour makes things more unpredictable.

Maybe this could just be a short chapter somewhere? Something like the following:


Working with checkboxes

Checkboxes only have an "on" state. Leaving them unchecked will result in the browser not sending any value for that field.
To work around this, you can adjust your zod schema based on the prefered outcome:

  • z.boolean() – Checkbox is required. Leaving it unchecked results in a validation error
  • z.boolean().optional() – Checkbox is not required. Leaving it unchecked results in undefined
  • z.boolean().default(false) – Checkbox is not required. Leaving it unchecked results in false.

@fiws
Copy link
Contributor Author

fiws commented Nov 23, 2023

Actually forgot about default: z.boolean().default(false) that should be the cleanest solution for my problem.

@sweeperq
Copy link

Actually forgot about default: z.boolean().default(false) that should be the cleanest solution for my problem.

I don't know what the deal is, but this doesn't work for me. When it is unchecked it indeed defaults to false. But when it is checked I get Expected boolean, received string. The only way I can get checkboxes to consistently work correctly is z.preprocess((x) => x === "on", z.boolean())

@edmundhung
Copy link
Owner

I don't know what the deal is, but this doesn't work for me. When it is unchecked it indeed defaults to false. But when it is checked I get Expected boolean, received string. The only way I can get checkboxes to consistently work correctly is z.preprocess((x) => x === "on", z.boolean())

There is a test dedicated for this. So I would expect it to work. It could be a regression on zod 3.22. Maybe try downgrading your zod version to 3.21.4 and see if the issue is resolved.

@edmundhung
Copy link
Owner

I will welcome a PR to add a tips here. Thanks!

@nianiam
Copy link

nianiam commented Mar 15, 2024

Under the assumption your check box state is either string | undefined (e.g. "on" | "") you can avoid the preprocess bugs using the built-in coerce: z.coerce.boolean().

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants