The validation of data is an essential part of an application, used in
api endpoints
, input data validation
, ... just to name a few examples. Most
commonly in nodejs
the express validators
, joi validators
, or
class validators
are used.
In our case we needed something with type save validation
similar to
class validators
but combined with the method chaining and flexibility
of
joi validators
.Furthermore we need to use the
validators in frontend and backend
and have to be therefor independent of a
library. Because of all of these reasons we decided to create our on validators.
-
Class Validators
-
Joi Validators
-
Express Validator
A class schema
is a TypeScript class
that serves as a
blueprint for the data
we send or receive between different parts of our
application, such as between the client and server. Beyond just describing the
shape of the data, our Validators also have built-in
rules for validating and transforming
this data. This ensures that the data is
not only correctly formatted but also meets specific conditions before we
process it further. Think of it as a smart
data package that knows how to
check itself for errors and even correct some of them automatically.
The advantages
of Class Schemas
are that they have strict type safety
and
are cleaner then the object validators
in highting in the IDE (class
definitions pop out more then constants)
class UserSchema extends ValidatorSchema<IUser> {
//0 - validate the name, which is MANDATORY
// > Note: by default all fields are "REQUIRED"
name = Validator.String()
.isNotEmpty()
.minLength(5)
.hasNoSpace() // example of VALIDATORS
.trim()
.toLowerCase()
.encodeBase64(); // example of TRANSFORMERS
//1 - OPTIONAL field
// > Note: alternatively use ".optionalIf((o: IUser) => ...)"
nickName = Validator.String().optional();
//2 - set DEFAULT value
age = Validator.Number().isPositive().default(-1);
//3 - conditional "IF" validation
adult = Validator.Boolean().validateIf((value: boolean) => adult);
//4 - validate an ARRAY, of strings
friends = Validator.Array()
.isNotEmpty()
.maxLength(10)
.shuffle()
.each(Validator.String().trim());
//5 - validate an OBJECT, external class schema
contact = Validator.Object().hasNoNestedObjects().inspect(ContactSchema); // nested object of "IAddress"
//7 - validate an ARRAY, of OBJECTS
friendContacts = Validator.Array().isNotEmpty().inspectEach(ContactSchema);
}
class ContactSchema extends ValidatorSchema<IContact> {
phone = Validator.String().isNotEmpty();
email = Validator.String().isNotEmpty().isEmail();
}
Use the ValidatorSchema
that was created before to validate the data
with
one of the below approaches.
//0 - validate by boolean
const isValid: boolean = Validator.Schema.isValid(data, UserSchema);
if (isValid) console.log(" > user is valid");
else console.log(" > user is invalid");
//1 - validate with getting the detailed infos
// > interface IValidatorError {
// > property : string;
// > value : boolean;
// > errors : string[];
// > children : IValidatorError[];
// > }
const validationErrors: IValidatorError[] = Validator.Schema.validate(
data,
UserSchema
);
console.log(" > validationErrors: ", validationErrors);
Note: the
transformers
are alsoexecuted during the validation
Use the UserSchema
that was created before to process / clean the data
based
on the transformers.
// process the user data by the schema
// > Note: this will execute logic like trim values or apply default values,
// > and more. It will return THROW an ERROR for an invalid object.
const userData: IUser = Validator.Schema.transform(requestBodyData, UserSchema);
Note: the
validation
is alsoexecuted during the processing
, so if the data is invalid we get the error
The advantages
of schemas
are, that they give high type safety
(it forces
us to have a validator for each value and forces the exact validator type
).
The disadvantage is we can only have a single validation chain
for each value.
Note: use the
Schema validation
forconfigs
and otherpartial inline validations
const userSchema: ValidatorSchema<IUser> = {
//0 - simple validators
name: Validator.String().isNotEmpty().trim().toLowerCase(),
nickName: Validator.String().optional(),
//1 - validate an ARRAY, of strings
friends: Validator.Array()
.each(Validator.String().trim())
.isNotEmpty()
.maxLength(10)
.shuffle(),
//2 - validate an OBJECT, as INLINE OBJECT
address: Validator.Object()
.inspect({
phone: Validator.String().toLowerCase(),
email: Validator.String().toLowerCase()
})
.hasNoNestedObjects(),
//3 - validate an OBJECT, as INLINE
contact: {
phone: Validator.String().toLowerCase(),
email: Validator.String().toLowerCase()
}
};
Note: the
schemas
can haveinline objects / nested validators
, but youcan not have a child schema
Use the Schema
that was created before to validate the data
with one of the
below approaches.
//0 - validate by boolean
const isDataValid: boolean = Validator.Schema.isValid(data, userSchema);
if (isDataValid) console.log(" > user is valid");
else console.log(" > user is invalid");
//1 - validate with getting the detailed infos
const validationErrors: IValidatorError[] = Validator.Schema.validate(
data,
userSchema
);
console.log(" > validationErrors: ", validationErrors);
//2 - validate inline
const validationErrors: IValidatorError[] = Validator.Schema.validate(data, {
name: Validator.String().isNotEmpty().trim().toLowerCase(),
nickName: Validator.String().optional()
// ... add other validators
});
Note: the
transformers
are alsoexecuted during the validation
Use the objectSchema
that was created before to process / clean the data
based on the transformers.
//0 - process the user data by the schema
// > Note: this will execute logic like trim values or apply default values,
// > and more. It will return THROW an ERROR for an invalid object.
const userData: IUser = Validator.Schema.transform(requestBodyData, userSchema);
//1 - transform inline
const validationErrors: IValidatorError[] = Validator.Schema.transform(data, {
name: Validator.String().isNotEmpty().trim().toLowerCase(),
nickName: Validator.String().optional()
// ... add other validators
});
Note: the
validation
is alsoexecuted during the processing
, so if the data is invalid we get the error
The validators
can also be executed directly
//0 - validate
const isValid: boolean = Validator.String()
.isNotEmpty()
.isLowerCase()
.isValid("My Cat");
//1 - processing value
const processedValue: string = Validator.String()
.isNotEmpty()
.toLowerCase()
.process("My Cat");
The validators can even be used with Angular FormGroup / FormControls
and can
provide the following standard required by angular
// standard required by angular
export function passwordValidator(): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
const password: string = control.value; // ... check "control.value"
return this.isValidPassword(password)
? { weekPassword: "Password is week" }
: null;
};
}
To convert the validator
into a FormControls Validator
use the approach
below
//0 - get the validator as an wrapped "angular validator"
// > Note: returns "(control: AbstractControl): ValidationErrors | null"
const angularValidator: ValidatorFn = Validator.String().isPassword().ngForm();
//1 - use the validator
const signupForm: FormGroup<ILoginForm> = new FormGroup<ILoginForm>({
password: new FormControl<string>("", angularValidator),
name: new FormControl<string>("", Validator.String().isNotEmpty().ngForm()) // use it directly inline
// ...
});
The validation returns the following result. Be aware that arrays
and
objects
can have children
.
interface IValidatorError {
property: string;
value: boolean;
errors: string[];
children: IValidatorError[];
}
If the following example data is validated, it would give the following result below.
// data which has invalid entries
const data = {
name: "Yoda",
age: -1, // negative number is invalid
hobbies: ["swimming", undefined, "running"], // undefined is invalid
address: {
country: null, // null is not a valid country
zipCode: 45678
}
};
// validation result
const validationResult: Partial<IValidatorError>[] = [
{
property: "age",
value: -1,
errors: ["No negative value"],
children: []
},
{
property: "hobbies",
value: ["swimming", undefined, 22, "running"],
errors: ["Some items are invalid"],
children: [
{
property: "1", // index in the array
value: undefined,
errors: ["Item is undefined"],
children: []
}
]
},
{
property: "address",
value: { country: undefined, zipCode: 45678 },
errors: ["Object is invalid"],
children: [
{
property: "country", // property of the object
value: null,
errors: ["Has to be a valid string"],
children: []
}
]
}
];
The below example is advanced and can be used as following: lets assume the
frontend is sending a user's password in an encrypted format
, then the below
validator would be executed as following.
-
Check if we got any string value
isNotEmpty()
-
Decrypt the received password
decryptASE128('9sdsdafsdafafh8asdsdafsdaffh9h89')
-
Validate if the password is secure enough
isPassword()
-
Hash the password, as we only need the hash to compare it
transform((value: string) => Util.Hash(value))
class UserSchema extends ValidatorSchema<IUser> {
password = Validator.String()
.isNotEmpty()
.decryptASE128("9sdsdafsdafafh8asdsdafsdaffh9h89")
.isPassword()
.map((value: string) => Util.Hash(value));
}
The Validator Utility is a handy tool that offers simple and effective validation for various entities such as emails, domains, passwords, etc. It provides two core functions for handling validation tasks, allowing you to either simply check validity or obtain the specific reasons for validation failures.
Each validator has the following two methods
-
isValid(entity: string): boolean
, use this function when you only need to know if an entity is valid or not. The function returns a boolean value.if (Validator.Email.isValid(email)) { console.log("Email is valid!"); } else { console.log("Email is invalid!"); }
-
validate(entity: string): string[]
, use this function when you need to know the reasons for validation failure, such as when you need to display specific feedback to the user. The function returns an array of strings that describe the validation errors.//0 - validate the mail const errors: string[] = Validator.Email.validate(email); //1 - show the errors if (errors.length === 0) { console.log("Email is valid!"); } else { console.log("Email is invalid for the following reasons: ", errors); }