# AJV

This tutorial shows you how you can validate your data with decorators.

Validation feature uses Ajv (opens new window) and json-schema (opens new window) to perform the model validation.

# Installation

Before using the validation decorators, we need to install the ajv (opens new window) module.

npm install --save ajv
npm install --save @tsed/ajv
1
2

Then import @tsed/ajv in your Server:

import {Configuration} from "@tsed/common";
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 {}
1
2
3
4
5
6
7
8
9

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/common";
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 {}
1
2
3
4
5
6
7
8
9
10

# Decorators

Ts.ED gives some decorators to write your validation model:

Loading in progress...

# Examples

# Model validation

A model can be used on a method controller along with BodyParams => BodyParams or other decorators, and will be validated by Ajv.

import {Required, MaxLength, MinLength, Minimum, Maximum, Format, Enum, Pattern, Email} from "@tsed/common";

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";
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# 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"
  }
]
1
2
3
4
5
6
7
8
9
10

# 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;
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

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/common";
    import {getJsonSchema} from "@tsed/schema";
    import {Product} from "./Product";
    import "../keywords/RangeKeyword";
    
    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);
      });
    });
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32

    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 (opens new window) for all pre-defined keywords - see codegen.md (opens new window) for details.

    Example even keyword:

      # Formats v6.36.0+

      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 (opens new window) 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);
        }
      }
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13

      Then, we can import this class to our server as follows:

      import {Configuration} from "@tsed/common";
      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 {}
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10

      Now, this example will be valid:

      import {Uri, getJsonSchema} from "@tsed/schema";
      import {PlatformTest} from "@tsed/common";
      import {AjvService} from "@tsed/ajv";
      import "./UriFormat";
      
      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: ""});
        });
      });
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32

      # Author

        # Maintainers

          Last Updated: 3/15/2024, 12:53:49 PM

          Other topics