# Model

The classes can be used as a model in your application. Ts.ED uses these models to convert JSON objects to their class equivalents.

The classes models can be used in the following cases:

To create a model, Ts.ED provides decorators which will store and generate a standard JsonSchema (opens new window) model.

WARNING

Validation is only available when you import @tsed/ajv package in your server.

import {Configuration} from "@tsed/common";
import "@tsed/ajv";

@Configuration()
class Server {}
1
2
3
4
5

Without this package, decorators like Email won't have any effect.

# Example

The example below uses decorators to describe a property of the class and store metadata such as the description of the field.

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

enum Categories {
  CAT1 = "cat1",
  CAT2 = "cat2"
}

export class MyModel {
  _id: string;

  @Required()
  unique: string;

  @MinLength(3)
  @MaxLength(50)
  indexed: string;

  @Minimum(0)
  @Maximum(100)
  @Default(0)
  rate: Number = 0;

  @Enum(Categories)
    // or @Enum("type1", "type2")
  category: Categories;

  @Pattern(/[a-z]/)
  pattern: String;

  @Format("date-time")
  @Default(Date.now)
  dateCreation: Date = new Date();
}
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
33
34
35
36
37
38
39
40
41
42
43

TIP

The Model will generate a JsonSchema which can be used by modules supporting JsonSchema spec

Our model is now described, we can use it inside a Controller as input type parameter for our methods. Ts.ED will use the model to convert the raw data to an instance of your model.

import {BodyParams, Controller, Post} from "@tsed/common";
import {PersonModel} from "../models/PersonModel";

@Controller("/")
export class PersonsCtrl {

  @Post("/")
  save(@BodyParams() model: PersonModel): PersonModel {
    console.log(model instanceof PersonModel); // true
    return model; // will be serialized according to your annotation on PersonModel class.
  }

  // OR

  @Post("/")
  save(@BodyParams("person") model: PersonModel): PersonModel {
    console.log(model instanceof PersonModel); // true
    return model; // will be serialized according to your annotation on PersonModel class.
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# Primitives

Just use at least Property decorator any other schema decorator (like Email ), to create a new property on a model. Ts.ED will get the type from Typescript metadata and transform this type to a valid Json type.

    # Integer

    The Integer decorator is used to set integer type for integral numbers.

      # Any types

      The Any , decorator is used to set one or more types on property. Use this method when you want to set explicitly the json type or when you use a mixed TypeScript types.

        # Regular expressions

        The Pattern decorator is used to restrict a string to a particular regular expression. The regular expression syntax is the one defined in JavaScript (ECMA 262 (opens new window) specifically). See Regular Expressions (opens new window) for more information.

          # Format

          The Format decorator allows basic semantic validation on certain kinds of string values that are commonly used. This allows values to be constrained beyond what the other tools in JSON Schema, including Regular Expressions (opens new window) can do.

            The following formats are supported for string validation with format keyword by AJV (opens new window):

            • date: full-date according to RFC3339 (opens new window).
            • time: time with optional time-zone.
            • date-time: date-time from the same source (time-zone is mandatory).
            • uri: full uri with optional protocol.
            • email: email address.
            • hostname: host name according to RFC1034 (opens new window).
            • ipv4: IP address v4.
            • ipv6: IP address v6.
            • regex: tests whether a string is a valid regular expression by passing it to RegExp constructor.

            See built-in formats types on Jsonp-schema.org (opens new window) for more details:

            # MultipleOf

            Numbers can be restricted to a multiple of a given number, using the MultipleOf decorator. It may be set to any positive number. See json-schema documentation (opens new window) for more details.

              # Ranges

              Ranges of numbers are specified using a combination of the Minimum and Maximum decorators, (or ExclusiveMinimum and ExclusiveMaximum for expressing exclusive range). See json-schema documentation (opens new window) for more details.

                # Enumerated values

                The Enum decorator is used to restrict a value to a fixed set of values. It must be an array with at least one element, where each element is unique or a TypeScript enum.

                  # Constant values

                  The Const decorator is used to restrict a value to a single value. For example, if you only support shipping to the United States for export reasons:

                    # Collections

                    Declaring a property that uses a collection is a bit different than declaring a simple property. TypeScript stores only the Array/Set/Map type when you declare the type of your property. The type used by the collection is lost.

                    To tell Ts.ED (and other third party which uses JsonSchema) that a property uses a collection with a specific type, you must use CollectionOf (before v5.62.0, use Unable to find something: symbolName === "PropertyType") decorator as following:

                      Ts.ED provides others related collection decorators:

                      # Required properties

                      By default, the properties defined with a decorator are not required. However, one can use Required decorator to add a required property to the json schema.

                      # Additional properties

                      Sometimes, it can be useful to create model with additional properties. By default, Json schema is strict over extra properties not declared in a model (see Properties json schema documentation (opens new window)).

                      Use AdditionalProperties on your model to allow this behavior:

                        It is also possible to add contraint on additional properties, by giving a raw Json schema:

                          Or by using getJsonSchema in combination with Unable to find something: symbolName === "AdditionalProperty" as following:

                            # Circular ref

                            Circular reference can be resolved by using arrow with a Property and CollectionOf decorators:

                            import {CollectionOf, Groups, Property} from "@tsed/schema";
                            
                            export class Photo {
                              @Property(() => User)
                              owner: User;
                            }
                            
                            export class User {
                              @CollectionOf(Photo)
                              @Groups("group.roles")
                              photos: Photo[];
                            }
                            1
                            2
                            3
                            4
                            5
                            6
                            7
                            8
                            9
                            10
                            11

                            # Custom Keys 6.17.0+

                            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
                            16
                            17
                            18
                            19
                            20

                            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
                              33
                              34
                              35

                              # Groups 6.14.0+

                              Groups decorator allows you to manage your serialized/deserialized fields by using group label. For example, with a CRUD controller, you can have many methods like POST, PUT, GET or PATCH to manage creation, update and read usecases for the exposed resource.

                              For the creation, you don't need to have the id field but for the update, you need to have it. With the previous version for Ts.ED, you had to create the model twice, one for the creation (without id) and another one for update and read (with id). Managing many models can be a pain point for the developer, this is why the Groups decorator exists.

                              For example, we have a User model with the following properties:

                              import {CollectionOf, Groups, Required} from "@tsed/schema";
                              
                              export class User {
                                @Groups("!creation")
                                id: string;
                              
                                @Required()
                                firstName: string;
                              
                                @Required()
                                lastName: string;
                              
                                @Required()
                                @Groups("group.email", "creation")
                                email: string;
                              
                                @Groups("creation")
                                password: string;
                              
                                @CollectionOf(String)
                                @Groups("group.roles")
                                roles: string[];
                              }
                              
                              1
                              2
                              3
                              4
                              5
                              6
                              7
                              8
                              9
                              10
                              11
                              12
                              13
                              14
                              15
                              16
                              17
                              18
                              19
                              20
                              21
                              22
                              23

                              Explanation:

                              • !creation: This annotation indicates that the field will never be exposed when using the creation group.
                              • group.email: This annotation indicates that the field will be exposed only if the group match with group.email or with a glob pattern like group.*.

                              So by using the deserialize function with the extra groups options, we can map data to the expected user instance:

                                Note

                                The same principle works with the serialize and getJsonSchema functions!

                                Now let's see how groups work with controllers.

                                  We can see that the Groups decorator can be used on parameter level as well as on the method through the Returns decorator. The generated OpenSpec will create automatically the appropriate JsonSchema according to the groups configuration!

                                  TIP

                                  You can combine different group labels or use a glob pattern to match multiple group labels. It's also possible to use negation by prefixing the group label with !.

                                  # Generics

                                  # Declaring a generic model

                                  Sometimes, it might be useful to use generic models. TypeScript doesn't store the generic type in the metadata. This is why we need to declare explicitly the generic models with the decorators.

                                  One of the generic's usage can be a paginated list. With Returns decorator, it's now possible to declare generic type and generate the appropriate OpenSpec documentation.

                                  Starting with the pagination model, by using Generics and CollectionOf :

                                  import {CollectionOf, Generics, Property} from "@tsed/schema";
                                  
                                  @Generics("T")
                                  class Pagination<T> {
                                    @CollectionOf("T")
                                    data: T[];
                                  
                                    @Property()
                                    totalCount: number;
                                  }
                                  1
                                  2
                                  3
                                  4
                                  5
                                  6
                                  7
                                  8
                                  9

                                  Now, we need a model to be used with the generic Pagination model:

                                  import {Property} from "@tsed/schema";
                                  
                                  class Product {
                                    @Property()
                                    id: string;
                                  
                                    @Property()
                                    title: string;
                                  }
                                  1
                                  2
                                  3
                                  4
                                  5
                                  6
                                  7
                                  8

                                  Finally, we can use our models on a method as following:

                                    # Declaring nested generic models

                                    It's also possible to declare nested generic models in order to have this type Pagination<Submission<Product>>:

                                      # Pagination

                                      The following advanced example will show you how you can combine the different Ts.ED features to describe Pagination. The used features are the following:

                                        # Annotations

                                        JSON Schema includes a few keywords and Ts.ED provide also theses corresponding decorators like Title , Description , Default , Unable to find something: symbolName === "Examples" that aren’t strictly used for validation, but are used to describe parts of a schema.

                                        None of these annotation keywords are required, but they are encouraged for good practice, and can make your schema self-documenting.

                                          # Alias

                                          Name decorator lets you to rename the exposed property in your json schema.

                                          For example mongo db uses the _id property. In order not to give any indication to our consumer about the nature of the database, it's better to rename the property to id.

                                          import {Description, Example, Name} from "@tsed/schema";
                                          import {ObjectID} from "@tsed/mongoose";
                                          
                                          export class Model {
                                            @Name("id")
                                            @Description("Object ID")
                                            @Example("5ce7ad3028890bd71749d477")
                                            _id: string;
                                          }
                                          
                                          // same example with mongoose
                                          export class Model2 {
                                            @ObjectID("id")
                                            _id: string;
                                          }
                                          
                                          1
                                          2
                                          3
                                          4
                                          5
                                          6
                                          7
                                          8
                                          9
                                          10
                                          11
                                          12
                                          13
                                          14
                                          15

                                          # Set Schema

                                          If Ts.ED doesn't provide the expected decorator to describe your json schema, you can use the Schema decorator from @tsed/common to set a custom schema.

                                          # Using JsonSchemaObject

                                          You can declare schema by using the JsonSchemaObject interface:

                                          import {BodyParams, Controller, Post} from "@tsed/common";
                                          import {JsonSchemaObject, Returns, Schema} from "@tsed/schema";
                                          
                                          const ProductSchema: JsonSchemaObject = {
                                            type: "object",
                                            properties: {
                                            }
                                          };
                                          
                                          export class MyModel {
                                            @Schema({
                                              contains: {
                                                type: "string"
                                              }
                                            })
                                            prop: string;
                                          }
                                          
                                          @Controller("/")
                                          class MyController {
                                            @Post("/")
                                            @Returns(200).Description("description").Schema(ProductSchema)
                                            async method(@BodyParams() @Schema(ProductSchema) product: any): Promise<null> {
                                              return null;
                                            }
                                          }
                                          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

                                          # Using functions 6.14.0+

                                          It's also possible to write a valid JsonSchema by using the functional approach (Joi like):

                                          import {BodyParams, Controller, Post} from "@tsed/common";
                                          import {array, number, object, Returns, Schema, string} from "@tsed/schema";
                                          
                                          const ProductSchema = object({
                                            id: string().required().description("Product ID"),
                                            title: string().required().minLength(3).example("CANON D300").description("Product title"),
                                            price: number().minimum(0).example(100).description("Product price"),
                                            description: string().description("Product description"),
                                            tags: array()
                                              .minItems(1)
                                              .items(string().minLength(2).maxLength(10).description("Product tag"))
                                              .description("Product tags")
                                          })
                                            .label("ProductModel");
                                          
                                          @Controller("/")
                                          class MyController {
                                            @Post("/")
                                            @Returns(200).Description("description").Schema(ProductSchema)
                                            async method(@BodyParams() @Schema(ProductSchema) product: any): Promise<null> {
                                              return null;
                                            }
                                          }
                                          1
                                          2
                                          3
                                          4
                                          5
                                          6
                                          7
                                          8
                                          9
                                          10
                                          11
                                          12
                                          13
                                          14
                                          15
                                          16
                                          17
                                          18
                                          19
                                          20
                                          21
                                          22

                                          Here is the list of available functions:

                                          # Get Json schema

                                          In some cases, it may be useful to retrieve the JSON Schema from a Model to use with another library. This is possible by using getJsonSchema . Here is a small example:

                                            # Expose a JsonSchema

                                            You can create a controller, or an endpoint to expose a specific schema with the custom keys. This can allow your consumers to retrieve a validation template so that they can use it to validate a form.

                                            import {Controller, Get} from "@tsed/common";
                                            import {getJsonSchema} from "@tsed/schema";
                                            import {Product} from "../models/Product";
                                            
                                            @Controller("/products")
                                            export class ProductsCtrl {
                                              @Get("/.schema")
                                              get(@QueryParams('customKeys') customKeys: boolean, @QueryParams('groups') groups: string[]) {
                                                return getJsonSchema(Product, {customKeys, groups});
                                              }
                                            }
                                            
                                            1
                                            2
                                            3
                                            4
                                            5
                                            6
                                            7
                                            8
                                            9
                                            10
                                            11

                                            # Get OpenSpec

                                            In some cases, it may be useful to retrieve the OpenSpec from a Controller to generate the Swagger OpenSpec. This is possible by using getSpec . Here is a small example:

                                              # Decorators

                                              Last Updated: 1/14/2021, 10:28:24 AM

                                              Other topics