AJV
This tutorial shows you how you can validate your data with decorators.
Validation feature uses Ajv and json-schema to perform the model validation.
Installation
Before using the validation decorators, we need to install the ajv module.
npm install --save ajv @tsed/ajv
yarn add ajv @tsed/ajv
pnpm add ajv @tsed/ajv
bun add ajv @tsed/ajv
Then import @tsed/ajv
into your Server:
import {Configuration} from "@tsed/di";
import "@tsed/ajv"; // import ajv ts.ed module
@Configuration({
ajv: {
returnsCoercedValues: true // returns coerced value to the next pipe instead of returns original value (See #2355)
}
})
export class Server {}
The AJV module allows a few settings to be added through the ServerSettings (all are optional):
- options are AJV specific options passed directly to the AJV constructor,
- errorFormatter can be used to alter the output produced by the
@tsed/ajv
package.
The error message could be changed like this:
import {Configuration} from "@tsed/di";
import "@tsed/ajv"; // import ajv ts.ed module
@Configuration({
ajv: {
errorFormatter: (error) => `At ${error.modelName}${error.dataPath}, value '${error.data}' ${error.message}`,
verbose: true
}
})
export class Server {}
Decorators
Ts.ED gives some decorators to write your validation model:
Examples
Model validation
A model can be used on a method controller along with BodyParams or other decorators, and will be validated by Ajv.
import {Required, MaxLength, MinLength, Minimum, Maximum, Format, Enum, Pattern, Email} from "@tsed/schema";
export class CalendarModel {
@MaxLength(20)
@MinLength(3)
@Required()
title: string;
@Minimum(0)
@Maximum(10)
rating: number;
@Email()
email: string;
@Format("date") // or date-time, etc...
createDate: Date;
@Pattern(/hello/)
customInput: string;
@Enum("value1", "value2")
customInput: "value1" | "value2";
}
Validation error
When a validation error occurs, AJV generates a list of errors with a full description like this:
[
{
"keyword": "minLength",
"dataPath": ".password",
"schemaPath": "#/properties/password/minLength",
"params": {"limit": 6},
"message": "should NOT be shorter than 6 characters",
"modelName": "User"
}
]
User defined keywords
Ajv allows you to define custom keywords to validate a property.
You can find more details on the different ways to declare a custom validator on this page: https://ajv.js.org/docs/keywords.html
Ts.ED introduces the Keyword decorator to declare a new custom validator for Ajv. Combined with the CustomKey decorator to add keywords to a property of your class, you can use more complex scenarios than what basic JsonSchema allows.
For example, we can create a custom validator to support the range
validation over a number. To do that, we have to define the custom validator by using Keyword decorator:
import {Keyword, KeywordMethods} from "@tsed/ajv";
import {array, number} from "@tsed/schema";
@Keyword({
keyword: "range",
type: "number",
schemaType: "array",
implements: ["exclusiveRange"],
metaSchema: array().items([number(), number()]).minItems(2).additionalItems(false)
})
class RangeKeyword implements KeywordMethods {
compile([min, max]: number[], parentSchema: any) {
return parentSchema.exclusiveRange === true ? (data: any) => data > min && data < max : (data: any) => data >= min && data <= max;
}
}
Then we can declare a model using the standard decorators from @tsed/schema
:
Finally, we can create a unit test to verify if our example works properly:
import "@tsed/ajv";
import {PlatformTest} from "@tsed/platform-http/testing";
import {getJsonSchema} from "@tsed/schema";
import {Product} from "./Product.js";
import "../keywords/RangeKeyword.js";
describe("Product", () => {
beforeEach(PlatformTest.create);
afterEach(PlatformTest.reset);
it("should call custom keyword validation (compile)", () => {
const ajv = PlatformTest.get<Ajv>(Ajv);
const schema = getJsonSchema(Product, {customKeys: true});
const validate = ajv.compile(schema);
expect(schema).to.deep.equal({
properties: {
price: {
exclusiveRange: true,
range: [10, 100],
type: "number"
}
},
type: "object"
});
expect(validate({price: 10.01})).toEqual(true);
expect(validate({price: 99.99})).toEqual(true);
expect(validate({price: 10})).toEqual(false);
expect(validate({price: 100})).toEqual(false);
});
});
WARNING
If you planed to create keyword that transform the data, you have to set returnsCoercedValues
to true
in your configuration.
With "code" function
Starting from v7 Ajv uses CodeGen module for all pre-defined keywords - see codegen.md for details.
Example even
keyword:
Formats
You can add and replace any format using Formats decorator. For example, the current format validator for uri
doesn't allow empty string. So, with this decorator you can create or override an existing ajv-formats validator.
import {Formats, FormatsMethods} from "@tsed/ajv";
const NOT_URI_FRAGMENT = /\/|:/;
const URI =
/^(?:[a-z][a-z0-9+\-.]*:)(?:\/?\/(?:(?:[a-z0-9\-._~!$&'()*+,;=:]|%[0-9a-f]{2})*@)?(?:\[(?:(?:(?:(?:[0-9a-f]{1,4}:){6}|::(?:[0-9a-f]{1,4}:){5}|(?:[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){4}|(?:(?:[0-9a-f]{1,4}:){0,1}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){3}|(?:(?:[0-9a-f]{1,4}:){0,2}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){2}|(?:(?:[0-9a-f]{1,4}:){0,3}[0-9a-f]{1,4})?::[0-9a-f]{1,4}:|(?:(?:[0-9a-f]{1,4}:){0,4}[0-9a-f]{1,4})?::)(?:[0-9a-f]{1,4}:[0-9a-f]{1,4}|(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?))|(?:(?:[0-9a-f]{1,4}:){0,5}[0-9a-f]{1,4})?::[0-9a-f]{1,4}|(?:(?:[0-9a-f]{1,4}:){0,6}[0-9a-f]{1,4})?::)|[Vv][0-9a-f]+\.[a-z0-9\-._~!$&'()*+,;=:]+)\]|(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)|(?:[a-z0-9\-._~!$&'()*+,;=]|%[0-9a-f]{2})*)(?::\d*)?(?:\/(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})*)*|\/(?:(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})+(?:\/(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})*)*)?|(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})+(?:\/(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})*)*)(?:\?(?:[a-z0-9\-._~!$&'()*+,;=:@/?]|%[0-9a-f]{2})*)?(?:#(?:[a-z0-9\-._~!$&'()*+,;=:@/?]|%[0-9a-f]{2})*)?$/i;
@Formats("uri", {type: "string"})
export class UriFormat implements FormatsMethods<string> {
validate(str: string): boolean {
// http://jmrware.com/articles/2009/uri_regexp/URI_regex.html + optional protocol + required "."
return str === "" ? true : NOT_URI_FRAGMENT.test(str) && URI.test(str);
}
}
Then, we can import this class to our server as follows:
import {Configuration} from "@tsed/di";
import "@tsed/ajv"; // import ajv ts.ed module
import "./formats/UriFormat"; // just import the class, then Ts.ED will mount automatically the new format
@Configuration({
ajv: {
// ajv options
}
})
export class Server {}
Now, this example will be valid:
import {Uri, getJsonSchema} from "@tsed/schema";
import {PlatformTest} from "@tsed/platform-http/testing";
import {AjvService} from "@tsed/ajv";
import "./UriFormat.js";
describe("UriFormat", () => {
beforeEach(() => PlatformTest.create());
afterEach(() => PlatformTest.reset());
it("should validate empty string when we load the our custom Formats for AJV", async () => {
class MyModel {
@Uri() // or @Format("uri")
uri: string;
}
const service = PlatformTest.get<AjvService>(AjvService);
const jsonSchema = getJsonSchema(MyModel);
expect(jsonSchema).to.deep.equal({
properties: {
uri: {
format: "uri",
type: "string"
}
},
type: "object"
});
const result = await service.validate({uri: ""}, {type: MyModel});
expect(result).to.deep.eq({uri: ""});
});
});