How do I create a validator that meets all of the following non-exclusive requirements and displays a different error message for each? I have a Formik form field for phone number. The validation requirement is
(,),-
That requirement is easily handled with the catch-all regex: /^[\s\d\)\(\-]+$/
using:
phoneNo: yup
.string()
.min(6, 'Phone must be at least 6 characters')
.max(35, 'Phone number must be at most 35 characters')
.matches(/^[\s\d\)\(\-]+$/, { //original matcher
message: 'Invalid format.',
})
.matches(/\./, { //my test matcher
message: 'No period',
}),
However, the business wants individual error messages for each validator. So, for 1) no letters, 2) no period, 3) no symbols, and 4) only numbers and (,),-
. At first I tried chaining multiple .matches()
as above. If I input 111222.
, the message from the first matcher, invalid format
would display. However, I expected No period
. Since .
is non-exclusive to both matchers, .
fails the first check.
I tried both yup.test()
and yup.addMethod
but having no experience with yup beyond this, I could not get the syntax right in my head. I've spent hours researching this issue, so it's not for a lack of trying.
P.S. There is no request for validating the actual phone number schema, like (xxx)xxx-xxxx
, just the values themselves. So a complete phone number regex is not necessary.
UPDATE: I've been trying to modify this solution to use this.matches()
, but I don't understand how to pass in the value to be matched.
As an additional test, I tried using addMethod()
yup.addMethod(yup.string, 'noPeriod', function(errorMessage){
const regexPeriod = /\./;
return this.matches(regexPeriod, errorMessage);
});
However, when adding this function to the .shape()
method
.shape({
phoneNo: yup.string()
.noPeriod('Period not allowed')
})
The TS linter rejects it with Property 'noPeriod' does not exist on type 'StringSchema<string, AnyObject, string>'.
I think the challenge you are running into is that matches
is only checking that the string passed to it actually matches the provided regex. In your case, since the .
character is not part of your character class in your Invalid format
regex, it is failing that check and reporting the message.
Since you want to ensure that the provided string does NOT contain a period, you should pass a regex with a negated character class to matches
. If one of the characters in the negated character class is present, then matches
will bark with its provided error message.
Another import aspect is that the order of the yup validation chain matters, yup validates each one in order from top to bottom. The first one that fails is the one who's error message gets thrown. You can see this by running the below tests, which all currently pass. Change the order and some of them will start to fail because different messages are thrown.
Sample code with (passing) (jest) tests:
schema.js
import * as yup from 'yup';
const schema = yup.object({
phoneNo: yup
.string()
.min(6, 'Phone must be at least 6 characters')
.max(35, 'Phone number must be at most 35 characters')
.matches(/^[^.]*$/, {
message: 'No period'
})
.matches(/^[^!@#$%^&*+=<>:;|~]*$/, {
message: 'No symbols'
})
.matches(/^[\s\d)(-]+$/, { //original matcher
message: 'Invalid format.',
})
});
export default schema;
schema.test.js
import { ValidationError } from 'yup';
import schema from './schema';
const testValidation = (obj) => {
return schema.validateSync(obj);
};
it('catches too few characters', () => {
const testTooFew = () => {
testValidation({phoneNo: 'foo'});
};
expect(testTooFew).toThrowError(new ValidationError('Phone must be at least 6 characters'));
});
it('catches too many characters', () => {
const testTooMany = () => {
testValidation({phoneNo: 'foobarbazfoobarbazfoobarbazfoobarbaz'});
};
expect(testTooMany).toThrowError(new ValidationError('Phone number must be at most 35 characters'));
});
it('throws No period error', () => {
const testPeriod = () => {
schema.validateSync({phoneNo: '......'});
};
expect(testPeriod).toThrow(new ValidationError('No period'));
});
it ('throws No symbols error', () => {
const testPlus = () => {
testValidation({phoneNo: '12345+'});
};
const testAmpersand = () => {
testValidation({phoneNo: '12345$'});
};
const testPercent = () => {
testValidation({phoneNo: '12345%'});
};
expect(testPlus).toThrow(new ValidationError('No symbols'));
expect(testAmpersand).toThrow(new ValidationError('No symbols'));
expect(testPercent).toThrow(new ValidationError('No symbols'));
});
it('allows my allowable characters', () => {
const allowableObj = {phoneNo: ' \t\n1234567890(-)'};
expect(testValidation(allowableObj)).toBe(allowableObj);
});