-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
This issue was moved to a discussion.
You can continue the conversation there. Go to discussion →
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
z.coerce.number() defaults empty strings to 0 #2461
Comments
Zod just uses the What do you want |
Thanks for the reply @colinhacks, I was able to get around this by creating an actual But at the time, I was hoping for it to just return the empty string as opposed to Zod, because I dont know if the zero came from the user or from Zod. So the validation of having a minimum 0 to X would always pass even though there was no value inside of the field and the user never touched the field. |
If I may add some color to this: Zod is built to represent the TypeScript type system at runtime. I doesn't know anything about React or HTML Forms or anything else, it's main job is to parse some value of type The fact that HTML Form controls, in general, are very loosely/stringily typed is kind of a well-known footgun and the source for a lot of what people make fun of JavaScript about 😂 . Having said that, I think @colinhacks 's question gets to the heart of the matter: Zod maps some input domain to a well-defined output domain, so if the output domain is In my view, form libraries should be doing the heavy lifting for you here: setting "empty" values to |
@scotttrinh Totally understand. It's been a pretty well known problem and massive disconnect when dealing with types and values coming back from input fields. When setting fields to null/undefined and then add a value (if its controlled) then you get the error that a field has been changed from uncontrolled to controlled. so the empty string was the only logical thing to add as the default value and to also keep the input field empty (0 wouldn't work here because the field should be empty). so thats why we're here lol. but i totally understand your point and how Zod was developed. I just have to be more explicit about when dealing with the types of data i define in the schema and what components will be used for that type. This issue can be closed since this isn't an issue for Zod to fix anyway. For anyone curious what i did, I just created a |
We had a similar issue and settled on |
Several UI libraries use My current workaround to the problem is to extend the import {
ParseInput,
ParseReturnType,
ProcessedCreateParams,
RawCreateParams,
ZodErrorMap,
ZodFirstPartyTypeKind,
ZodNumber,
} from "zod";
// Directly copied from zod/src/types.ts
function processCreateParams(params: RawCreateParams): ProcessedCreateParams {
if (!params) return {};
const { errorMap, invalid_type_error, required_error, description } = params;
if (errorMap && (invalid_type_error || required_error)) {
throw new Error(
`Can't use "invalid_type_error" or "required_error" in conjunction with custom error map.`
);
}
if (errorMap) return { errorMap: errorMap, description };
const customMap: ZodErrorMap = (iss, ctx) => {
if (iss.code !== "invalid_type") return { message: ctx.defaultError };
if (typeof ctx.data === "undefined") {
return { message: required_error ?? ctx.defaultError };
}
return { message: invalid_type_error ?? ctx.defaultError };
};
return { errorMap: customMap, description };
}
export class CustomZodNumber extends ZodNumber {
_parse(input: ParseInput): ParseReturnType<number> {
// Alot of input ui libraries will send empty strings instead of undefined
// This is a workaround for not triggering invalid_type_error and rather required_error if not optional
input.data = input.data === "" ? undefined : input.data;
return super._parse(input);
}
static create = (
params?: RawCreateParams & { coerce?: boolean }
): CustomZodNumber => {
return new CustomZodNumber({
checks: [],
typeName: ZodFirstPartyTypeKind.ZodNumber,
coerce: params?.coerce || false,
...processCreateParams(params),
});
};
}
export const number = CustomZodNumber.create; Would love if |
Is there a way to validate that the is not an empty string before coercing? z.number().or(z.string().nonempty())
.pipe(z.coerce.number())
// Or only in the context of form inputs
z.string().nonempty()
.pipe(z.coerce.number()) And for optional fields z.literal("").transform(() => undefined)
.or(z.coerce.number())
.optional() |
@jacknevitt That would work but |
Oh yeah, of course. I had forgotten about that one. Thanks! |
For anyone interested, I found this solution (but without using const ZodStringNumberOrNull = z
.string()
.transform((value) => (value === '' ? null : value))
.nullable()
.refine((value) => value === null || !isNaN(Number(value)), {
message: 'Invalid number',
})
.transform((value) => (value === null ? null : Number(value))); This returns
|
Using @maximeburri approach, I made a function that wraps the import { z, ZodTypeAny } from 'zod';
export const zodInputStringPipe = (zodPipe: ZodTypeAny) =>
z
.string()
.transform((value) => (value === '' ? null : value))
.nullable()
.refine((value) => value === null || !isNaN(Number(value)), {
message: 'Nombre Invalide',
})
.transform((value) => (value === null ? 0 : Number(value)))
.pipe(zodPipe); In use : const schema = zodInputStringPipe(z.number().positive('Le nombre doit être supérieur à 0')); May be handier to use in your schemas |
@srowe0091, |
Thank you, that's exactly what I was looking for. |
@JacobWeisenburger Yes its been addressed, thank you! |
This issue was moved to a discussion.
You can continue the conversation there. Go to discussion →
When working with react-hook-form and using controlled inputs, we need to default values to empty strings or else there will be errors of going from uncontrolled to controlled.
So using z.number() does not work because every input returns a string even if the input type is set number, it still returns a string in the event.
Switching to
z.coerce.number()
works fine with the inputs not complaining about types, but when the validation runs, empty strings are getting converted to a0
. The state of the form shows an empty string. This causes a weird behavior where if the field is required, validation passes because the schema sees a 0 which gives a false positive.The text was updated successfully, but these errors were encountered: