# Converters

The service is responsible for serializing and deserializing objects.

It has two operation's modes:

  • The first case uses the class models to convert an object into a class (and vice versa).
  • The second case is based on the JSON object itself to provide an object with the right types. For example the deserialization of dates.

The ConverterService is used by the following decorators:

# Usage

Models can be used at the controller level. Here is our model:

import {CollectionOf, Minimum, Property, Description} from "@tsed/common";

export class Person {
  @Property()
  firstName: string;

  @Property()
  lastName: string;

  @Description("Age in years")
  @Minimum(0)
  age: number;

  @CollectionOf(String)
  skills: string[];
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

TIP

allows to specify the type of a collection.

And its use on a controller:

import {BodyParams, Controller, Get, Post, Returns, ReturnsArray} from "@tsed/common";
import {Person} from "../models/Person";

@Controller("/")
export class PersonsCtrl {

  @Post("/")
  @Returns(Person) // tell to swagger that the endpoint return a Person
  async save1(@BodyParams() person: Person): Promise<Person> {
    console.log(person instanceof Person); // true

    return person; // will be serialized according to your annotation on Person class.
  }

  // OR
  @Post("/")
  @Returns(Person) // tell to swagger that the endpoint return a Person
  async save2(@BodyParams("person") person: Person): Promise<Person> {
    console.log(person instanceof Person); // true

    return person; // will be serialized according to your annotation on Person class.
  }

  @Get("/")
  @ReturnsArray(Person) // tell to swagger that the endpoint return an array of Person[]
  async getPersons(): Promise<Person[]> {
    return [
      new Person()
    ];
  }
}
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

In this example, Person model is used both as input and output types.

TIP

Because in most cases we use asynchronous calls (with async or promise), we have to use or decorators to tell swagger what is the model returned by your endpoint. If you don't use swagger, you can also use decorator instead to force converter serialization.

import {BodyParams, Controller, Get, Post, Returns, ReturnsArray} from "@tsed/common";
import {Person} from "../models/Person";

@Controller("/")
export class PersonsCtrl {

  @Post("/")
  @Returns(Person)
  async save1(@BodyParams() person: Person): Promise<Person> {
    console.log(person instanceof Person); // true

    return person; // will be serialized according to your annotation on Person class.
  }


  @Get("/")
  @ReturnsArray(Person)
  async getPersons(): Promise<Person[]> {
    return [
      new Person()
    ];
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# Serialisation

When you use a class model as a return parameter, the Converters service will use the JSON Schema of the class to serialize the JSON object.

Here is an example of a model whose fields are not voluntarily annotated:

import {Property} from "tsed/common";

class User {
    _id: string;
    
    @Property()
    firstName: string;
    
    @Property()
    lastName: string;
    
    password: string;
}
1
2
3
4
5
6
7
8
9
10
11
12
13

And our controller:

import {Get, Controller} from "@tsed/common";
import {User} from "../models/User";

@Controller("/")
export class UsersCtrl {

    @Get("/")
    get(): User {
        const user = new User();
        user._id = "12345";
        user.firstName = "John";
        user.lastName = "Doe";
        user.password = "secretpassword";
        return user
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

Our serialized User object will be:

{
  "firstName": "John",
  "lastName": "Doe"
}
1
2
3
4

Non-annotated fields will not be copied into the final object.

You can also explicitly tell the Converters service that the field should not be serialized with the decorator .

import {Ignore, Property} from "@tsed/common";

export class User {
  @Ignore()
  _id: string;

  @Property()
  firstName: string;

  @Property()
  lastName: string;

  @Ignore()
  password: string;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# Type converters

The Converters service relies on a subservice set to convert the following types:

Set and Map types will be converted into an JSON object (instead of Array).

Any of theses converters can be overrided with decorators:

# Example

Here an example of a type converter:

import {ValidationError} from "../../mvc/errors/ValidationError";
import {Converter} from "../decorators/converter";
import {IConverter} from "../interfaces/index";

/**
 * Converter component for the `String`, `Number` and `Boolean` Types.
 * @converters
 * @component
 */
@Converter(String, Number, Boolean)
export class PrimitiveConverter implements IConverter {
  deserialize(data: string, target: any): String | Number | Boolean | void | null {
    switch (target) {
      case String:
        return "" + data;

      case Number:
        if ([null, "null"].includes(data)) return null;

        const n = +data;

        if (isNaN(n)) {
          throw new ValidationError("Cast error. Expression value is not a number.", []);
        }

        return n;

      case Boolean:
        if (["true", "1", true].includes(data)) return true;
        if (["false", "0", false].includes(data)) return false;
        if ([null, "null"].includes(data)) return null;
        if (data === undefined) return undefined;

        return !!data;
    }
  }

  serialize(object: String | Number | Boolean): any {
    return object;
  }
}
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

# Create a custom converter

Ts.ED creates its own converter in the same way as the previous example.

To begin, you must add to your configuration the directory where are stored your classes dedicated to converting types.

import {Configuration} from "@tsed/common";
import {resolve} from "path";
const rootDir = resolve(__dirname);

@Configuration({
   componentsScan: [
       `${rootDir}/converters/**/**.js`
   ]
})
export class Server {
   
}       
1
2
3
4
5
6
7
8
9
10
11
12

Then you will need to declare your class with the annotation:

import {Converter} from "../decorators/converter";
import {IConverter, IDeserializer, ISerializer} from "../interfaces/index";

/**
 * Converter component for the `Array` Type.
 * @converters
 * @component
 */
@Converter(Array)
export class ArrayConverter implements IConverter {
  deserialize<T>(data: any, target: any, baseType: T, deserializer: IDeserializer): T[] {
    return [].concat(data).map((item) => deserializer!(item, baseType));
  }

  serialize(data: any[], serializer: ISerializer) {
    return [].concat(data as any).map((item) => serializer(item));
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

Note

This example will replace the default Ts.ED converter.

It is therefore quite possible to replace all converters with your own classes (especially the Date).

# Validation

The Converters service provides some validation of a class model. It will check the consistency of the JSON object with the data model. For example :

  • If the JSON object contains one more field than expected in the model (validationModelStrict or ).
  • If the field is mandatory ,
  • If the field is mandatory but can be null (@Allow(null)).

Here is a complete example of a model:

import  {Required, Name, Property, CollectionOf, Allow} from "@tsed/common";

class EventModel {
    @Required()
    name: string;
     
    @Name('startDate')
    startDate: Date;

    @Name('end-date')
    endDate: Date;

    @CollectionOf(TaskModel)
    @Required()
    @Allow(null)
    tasks: TaskModel[];
}

class TaskModel {
    @Required()
    subject: string;
    
    @Property()
    rate: number;
}
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

# Additional properties Policy

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

@Configuration({
  converter: {
    additionalProperties: "error" // default: "error", "accept" | "ignore"
  }
})
export class Server {

}
1
2
3
4
5
6
7
8
9
10

additionalProperties define the policy to adopt if the JSON object contains one more field than expected in the model.

You are able to change the behavior about any additional properties when it try to deserialize a Plain JavaScript Object to a Ts.ED Model.

# Emit error

By setting error on converter.additionalProperties, the deserializer will throw an .

If the model is the following:

import {Property} from "@tsed/common";

export class Person {
  @Property()
  firstName: string;
}
1
2
3
4
5
6

Sending this object will throw an error:

{
  "firstName": "John",
  "unknownProp": "Doe"
}
1
2
3
4

Note

The legacy validationModelStrict: true has the same behavior has converter.additionalProperties: true.

# Merge additional property

By setting accept on converter.additionalProperties, the deserializer will merge Plain Object JavaScript with the given Model.

Here is the model:

import {Property} from "@tsed/common";

export class Person {
  @Property()
  firstName: string;

  /**
   * Accept additional properties on this model (Type checking)
   */
  [key: string]: any;
}
1
2
3
4
5
6
7
8
9
10
11

And his controller:

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

@Controller("/")
export class PersonsCtrl {

  // The payload request.body = { firstName: "John", lastName: "Doe"  }
  @Post("/")
  @Returns(Person) // tell to swagger that the endpoint return a Person
  async save1(@BodyParams() person: Person): Promise<Person> {
    console.log(person instanceof Person); // true
    console.log(person.firtName); // John
    console.log(person.lastName); // Doe - additional property is available

    return person;
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

So sending this object won't throw an error:

{
  "firstName": "John",
  "unknownProp": "Doe"
}
1
2
3
4

Note

The legacy validationModelStrict: true has the same behavior has converter.additionalProperties: true.

# Ignore additional property

By setting ignore on converter.additionalProperties, the deserializer will ignore additional properties when a Model is used on a Controller.

import {Property} from "@tsed/common";

export class Person {
  @Property()
  firstName: string;
}
1
2
3
4
5
6
import {BodyParams, Controller, Post, Returns} from "@tsed/common";
import {Person} from "../models/Person";

@Controller("/")
export class PersonsCtrl {

  // The payload request.body = { firstName: "John", lastName: "Doe"  }
  @Post("/")
  @Returns(Person) // tell to swagger that the endpoint return a Person
  async save1(@BodyParams() person: Person): Promise<Person> {
    console.log(person instanceof Person); // true
    console.log(person.firtName); // John
    console.log(person.lastName); // undefined

    return person;
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# AdditionalProperties decorator

v5.55.0+

It also possible to change the behavior by using the decorator directly on a model.

This example will accept additional properties regardless of the value configured on converter.additionalProperties:

import {Property, AdditionalProperties} from "@tsed/common"; 

@AdditionalProperties(true) // is equivalent to converter.additionalProperties: 'accept'
export class Person {
  @Property()
  firstName: string;
 
  [key: string]: any;
}
1
2
3
4
5
6
7
8
9

Also with falsy value, the converter will emit an error:

import {Property, AdditionalProperties} from "@tsed/common"; 

@AdditionalProperties(false) // is equivalent to converter.additionalProperty: 'error'
export class Person {
  @Property()
  firstName: string;
 
  [key: string]: any;
}
1
2
3
4
5
6
7
8
9