conform
conform copied to clipboard
Using checkboxes with zod is confusing
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 youundefined
- I wanted the checkbox to give me
false
…
- I wanted the checkbox to give me
-
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
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()
)
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 inundefined
-
z.boolean().default(false)
– Checkbox is not required. Leaving it unchecked results infalse
.
Actually forgot about default: z.boolean().default(false)
that should be the cleanest solution for my problem.
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())
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.
I will welcome a PR to add a tips here. Thanks!
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()
.