# 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[];
}
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()
];
}
}
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()
];
}
}
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;
}
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
}
}
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"
}
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;
}
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;
}
}
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 {
}
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));
}
}
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;
}
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 {
}
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;
}
2
3
4
5
6
Sending this object will throw an error:
{
"firstName": "John",
"unknownProp": "Doe"
}
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;
}
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;
}
}
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"
}
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;
}
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;
}
}
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;
}
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;
}
2
3
4
5
6
7
8
9
← Model Middlewares →
- Session & cookies
- Passport.js
- TypeORM
- Mongoose
- GraphQL
- Socket.io
- Swagger
- AJV
- Multer
- Serve static files
- Templating
- Throw HTTP Exceptions
- Customize 404
- AWS
- Jest
- Seq
- Controllers
- Providers
- Model
- Converters
- Middlewares
- Pipes
- Interceptors
- Authentication
- Hooks
- Injection scopes
- Custom providers
- Custom endpoint decorator
- Testing