I have a legacy project where a field can be a number, but the absence of value is defined by an empty string (instead of null).
I am looking for something like nullable (yup.number().allowEmptyString()) or yup.mixed.oneOf(['', yup.number()]
Does anyone have an idea on how to handle this in the cleanest way?
Currently I am doing
const schema = yup
.mixed()
.test(fn) // value === '' || yup.number().isValidSync(value)
.transform(fn) // value === '' ? value : +value
but this loses beautiful syntax of Yup and just feels dirty since yup.number() is burried inside test, but may be further customized for other fields.
Thanks
I would convert the empty strings to null with a transform ideally, but if you need to keep it as a string it may be worth extending the number schema to allow it.
const baseCheck = number.prototype._typeCheck
number.prototype._typeCheck = value => value === '' || baseCheck(value)
That will mutate the existing schema , you can also use inheritance to create a new EmptyStringNumber or something
Thanks, @jquense!
Your idea lead me to this implementation
yup.mixed.prototype.allowEmptyString = function (value) {
var next = this.clone()
next._allowEmptyString = value !== false
return next
}
yup.mixed.prototype.isType = function (v) {
if (this._nullable && v === null) {
return true
}
if (this._allowEmptyString && v === '') {
return true
}
return !this._typeCheck || this._typeCheck(v)
}
To make this abstract, would you be open to have a method allow in Yup that would take a single value or array of values and would circumvent the type check? I think Joi has a similar functionality.
this actually a great idea...I've been meaning to add some utilities for better dealing with empty form inputs for the html form use-case, and your approach is simple and effective 馃憤
Sweet! Should I prepare a PR?
sure!
@jquense Now that I've used my implementation a bit, it definitely has problems. :/
For example
yup.number().positive().allow('').validateSync('') // true
yup.number().positive().integer().allow('').validateSync('') // error: this must be an integer
yup.number().min(3).max(10).allow('').validateSync('') // error: this must be greater than or equal to 3
Looking at this commit, it looks like to make allow work, a can of worms has to be opened with many changes throughout the codebase.
Are you still open for this PR?
im still open to it, the current codebase uses isAbsent which could be put on the schema and could conditionally handle the empty strings. we may need to scope the PR tho to only allowing empty strings vs a more generic allow
My solution
class EmptyStringNumber extends Yup.number {
_typeCheck(value) {
return value === "" || super._typeCheck(value)
}
required(message = YupLocale.mixed.required) {
const result = this.test({
message,
name: "requiredString",
test: function(value) {
return value === "" ? false : true
}
})
return result
}
}
Unfortunately @gastonmorixe solution doesn't fully solve the issue as it will still throw during cast. I've modified the solution (in Typescript):
import { number } from 'yup';
// @ts-ignore
import YupLocale from 'yup/lib/locale';
class EmptyStringNumber extends number {
// tslint:disable-next-line: function-name
_typeCheck(value: unknown) {
// @ts-ignore
return value === '' || (super._typeCheck && super._typeCheck(value));
}
// @ts-ignore
cast(value: unknown, options: {}): number | undefined {
if (value === '') return undefined;
return super.cast(value, options);
}
required(message = YupLocale.mixed.required) {
const result = this.test({
message,
name: 'requiredString',
test (value) {
return value === '' ? false : true;
},
});
return result;
}
}
I then use it for integers together with a transform function:
new EmptyStringNumber()
.integer()
.transform((v: number) => isNaN(v) ? undefined : v)
I'm a little confused as to what the recommended solution is here.
This issue has been closed because of anticipating a PR by iamvanja but this never actually happened. The issue still has no resolution and should be reopened.
I don't get why doesn't the .transform() work for .number() ?!?! It seems that it fails validation before transform is applied. Example:
yup.number().isValidSync(undefined)
// returns true
yup.number().transform(val => val === "" ? undefined : val).isValidSync('')
// should return true, but returns false instead
I ended up using .mixed() instead.
Hey @MarkMurphy @martinchristov, I'm using a helper that combines the number.transform with nullable.
// Helper to validate the numbers that can be empty string
function EmptyNumber(typeErrorMessage = "Please enter a valid number") {
return Yup.number()
.transform(function(value, originalValue) {
if (this.isType(value)) return value
if (!originalValue || !originalValue.trim()) {
return null
}
// we return the invalid original value
return originalValue
})
.nullable(true)
.typeError(typeErrorMessage)
}
// Now, use this instead of Yup.number()
{
'cost_price': EmptyNumber("Cost price number is not valid")
.positive("Cost price should be a positive number")
}
Any thoughts, where this can go wrong ?
Has anyone had luck using transform to allow values besides empty strings? I'm working on a form that allows a wildcard "*" operator in inputs that would otherwise only allow floats or integers (it's a hack--I know). I tried this and the value seemed to be converted to NaN before it got to the transform function:
function transform(value) {
console.log("why is this nan", value);
if (value === "") {
return null;
} else if (value === "*") {
return null;
} else {
return value;
}
}
yup
.number()
.typeError()
.nullable()
.transform(value => transform(value))
I also looked into subclassing number like this:
export class numberext extends yup.number {
_typeCheck(value) {
console.log("value", value);
return value === "*" || super._typeCheck(value);
}
}
new numberext()
.typeError()
.nullable()
.transform(value => transform(value))
and like this:
export class numberext extends yup.number {
constructor() {
super();
this.withMutation(() => {
this.transform(function(value) {
console.log("constructor", value);
let parsed = value;
if (typeof parsed === "string") {
parsed = parsed.replace(/\s/g, "");
if (parsed === "*") return NaN;
if (parsed === "") return NaN;
// don't use parseFloat to avoid positives on alpha-numeric strings
parsed = +parsed;
}
if (this.isType(parsed)) return parsed;
return parseFloat(parsed);
});
});
}
_typeCheck(value) {
console.log("value", value);
return value === "*" || super._typeCheck(value);
}
}
new numberext()
.typeError()
.nullable()
.transform(value => transform(value))
I'd really appreciate any advice that you may have on these approaches. I come from Python land, and it's usually pretty easy to subclass and override behavior, but I'm stuck right now.
@hdoupe Have you tried my solution https://github.com/jquense/yup/issues/298#issuecomment-508346424 ? Just add one more check for your * i.e.
function EmptyNumber(typeErrorMessage = "Please enter a valid number") {
.
+ if (originalValue.trim() === "*") {
+ return null
+ }
.
}
Hope this helps.
Success! Thanks @sudkumar! Now to see if I can get this to work with cast...
function transform(value, originalValue) {
if (originalValue.trim() === "" || originalValue.trim() === "*") {
return null;
} else {
return value;
}
}
yup
.number()
.typeError(floatMsg)
.nullable()
.transform(transform);
This is my snippet for a "simple" nullable integer:
const nullableIntegerBetween0and100 = Yup.number()
.integer('Only intergers are accepted.')
.typeError('Only intergers are accepted.')
.min(0,'Min value is 0.')
.max(100, 'Max value is 100.')
.nullable()
.transform((value: string, originalValue: string) => originalValue.trim() === "" ? null: value);
This is my snippet for a "simple" nullable integer:
const nullableIntegerBetween0and100 = Yup.number() .integer('Only intergers are accepted.') .typeError('Only intergers are accepted.') .min(0,'Min value is 0.') .max(100, 'Max value is 100.') .nullable() .transform((value: string, originalValue: string) => originalValue.trim() === "" ? null: value);
Be _very_ careful with this @julianoappelklein -- I just had to track down an issue where the .trim() was failing due to attempting to trim a non-string value causing other validations to stop running--when this happens there isn't any sort of error in the console so it was very difficult to figure out.
I had a bit of hard times finding the right combination for my case (I don't know why but I wasn't able to make other solutions working in my code), so here is my working validation schema if it can help anybody:
// helper for yup transform function
function emptyStringToNull(value, originalValue) {
if (typeof originalValue === 'string' && originalValue === '') {
return null;
}
return value;
}
// to validate a number with min and max and allowing empty string
const mySchema = yup.number().min(-0.2).max(0.2).transform(emptyStringToNull).nullable();
@NicolasLetellier this worked for me. Thanks!
Worked for me. Thanks @NicolasLetellier.
Since I need to use strict: true, the solutions above don't work for me because the transformations don't run. So I use lazy:
lazy(value => {
switch (typeof value) {
case 'number':
return number()
default:
return string()
.oneOf([''], '${path} must be a number or an empty string')
// oneOf doesn't catch undefined
.required()
}
})
Nothing of those above solutions worked 100% for me.
I'm currently using Sequelize ORM and when data goes to database, throws an error because data type "" === 'string'.
Those solutions converts empty number to '' (string), not to null.
To avoid this, despite it is a work around (not a solution), out of yup, im using this:
database.key = value === '' ? null : value;
await database.save();
Hi @julianoappelklein and @cjones26
I have found that if you wrap originalValue with a String() constructor it can act as a safe guard.
.transform((value, originalValue) => String(originalValue).trim() === "" ? null: value)
The simplest solution is regex (with matches function):
someNumber: yup.string().matches(/^\d*$/, "Wrong number"),
So the input is a string but the match will fail if you put anything except digits inside.
\d* - look for 0 or more digits
^...& - make sure there are only digits in whole string
count: yup
.number()
.integer('Must be a valid number.')
.typeError('Must be a valid number.')
.min(0, 'Min value is 0.')
.nullable()
.transform((value, originalValue) => (String(originalValue).trim() === '' ? null : value))
This behavior should be baked into the library, maybe via a new function called allowEmptyString() -- @jquense thoughts?
Most helpful comment
I'm a little confused as to what the recommended solution is here.