JsonMapper
The @tsed/json-mapper
package is responsible to map a plain object to a model and a model to a plain object.
It provides two functions serialize and deserialize to transform object depending on which operation you want to perform. It uses all decorators from @tsed/schema
package and TypeScript metadata to work.
Ts.ED use this package to transform any input parameters sent by your consumer to a class and transform returned value by your endpoint to a plain javascript object to your consumer.
Configuration
@Configuration({
jsonMapper: {
additionalProperties: false,
disableUnsecureConstructor: false,
strictGroups: false
}
})
jsonMapper.additionalProperties
Enable additional properties on model. By default, false
.
WARNING
Enable this option is dangerous and may be a potential security issue.
jsonMapper.disableUnsecureConstructor
Pass the plain object to the model constructor. By default, true
.
It may be a potential security issue if you have as constructor with this followings code:
class MyModel {
constructor(obj: any = {}) {
Object.assign(this, obj); // potential prototype pollution
}
}
jsonMapper.strictGroups
Enable strict mode for @Groups
decorator. By default, false
. See Groups for more information.
WARNING
The strictGroups
option is enabled by default in the next major version of Ts.ED.
Usage
JsonMapper works with a class and decorators. Use decorators on properties to describe a model and use this model as an input parameter or return value by your endpoint. Here is a model example:
import {CollectionOf, Minimum, Property, Description} from "@tsed/schema";
export class Person {
@Property()
firstName: string;
@Property()
lastName: string;
@Description("Age in years")
@Minimum(0)
age: number;
@CollectionOf(String)
skills: string[];
}
import {deserialize, serialize} from "@tsed/json-mapper";
import {Person} from "./Person";
describe("Person", () => {
it("should deserialize a model", () => {
const input = {
firstName: "firstName",
lastName: "lastName",
age: 0,
skills: ["skill1"]
};
const result = deserialize(input, {
type: Person
});
expect(result).toBeInstanceOf(Person);
expect(result).toEqual({
firstName: "firstName",
lastName: "lastName",
age: 0,
skills: ["skill1"]
});
});
it("should serialize a model", () => {
const person = new Person();
person.firstName = "firstName";
person.lastName = "lastName";
person.person = 0;
person.skills = ["skill1"];
const result = serialize(person);
expect(result).not.toBeInstanceOf(Person);
expect(result).toEqual({
firstName: "firstName",
lastName: "lastName",
age: 0,
skills: ["skill1"]
});
});
});
import {deserialize, serialize} from "@tsed/json-mapper";
import {Person} from "./Person";
describe("Person", () => {
it("should deserialize a model", () => {
const input = {
firstName: "firstName",
lastName: "lastName",
age: 0,
skills: ["skill1"]
};
const result = deserialize(input, {
type: Person
});
expect(result).toBeInstanceOf(Person);
expect(result).toEqual({
firstName: "firstName",
lastName: "lastName",
age: 0,
skills: ["skill1"]
});
});
it("should serialize a model", () => {
const person = new Person();
person.firstName = "firstName";
person.lastName = "lastName";
person.person = 0;
person.skills = ["skill1"];
const result = serialize(person);
expect(result).not.toBeInstanceOf(Person);
expect(result).toEqual({
firstName: "firstName",
lastName: "lastName",
age: 0,
skills: ["skill1"]
});
});
});
Note
Take a look on Jest/Mocha tabs to see serialize and deserialize functions usage.
Now we can use the Person
model on a controller:
import {BodyParams} from "@tsed/platform-params";
import {Get, Post, Returns} from "@tsed/schema";
import {Controller} from "@tsed/di";
import {Person} from "../models/Person";
@Controller("/")
export class PersonsCtrl {
@Post("/")
@Returns(200, Person)
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(200, Person)
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("/")
@Returns(200, Array).Of(Person) // Add the correct json schema for swagger essentially.
getPersons(): Promise<Person[]> {
return Promise.resolve([new Person()]);
}
}
Note
In the previous example, we can see Returns decorator usage. In all case, Ts.ED infer the returned value and apply the correct transformation on your response.
Returns decorator is used to generate the correct swagger documentation only.
WARNING
When a model is provided, JsonMapper will follow exactly the JsonSchema generated by @tsed/schema
package.
It means, if you missed decorating one or more properties on your model, these properties won't be appear after the transformation.
import {Property} from "@tsed/schema";
export class User {
_id: string;
@Property()
firstName: string;
@Property()
lastName: string;
password: string;
}
import {serialize} from "@tsed/json-mapper";
import {User} from "./User";
describe("User", () => {
it("should serialize a model", () => {
const user = new User();
user._id = "12345";
user.firstName = "John";
user.lastName = "Doe";
user.password = "secretpassword";
const result = serialize(user);
expect(result).toEqual({
firstName: "John",
lastName: "Doe"
});
});
});
import {serialize} from "@tsed/json-mapper";
import {expect} from "chai";
import {User} from "./User";
describe("User", () => {
it("should serialize a model", () => {
const user = new User();
user._id = "12345";
user.firstName = "John";
user.lastName = "Doe";
user.password = "secretpassword";
const result = serialize(user);
expect(result).to.deep.equal({
firstName: "John",
lastName: "Doe"
});
});
});
Note: Result is displayed in Jest/Mocha tabs.
Ignore properties (deprecated)
deprecated
This decorator is deprecated. Use Groups decorator instead of.
Usage
Ignore decorator can be used to ignore explicitly a property when a transformation have been performed.
For example, you have a base model to create a User named UserCreation
where the password
is required, but you don't want to expose this field in other cases. One of the solution is to use class inheritance to solve this problem.
import {Ignore, Property, Required} from "@tsed/schema";
export class UserCreation {
@Ignore()
_id: string;
@Property()
firstName: string;
@Property()
lastName: string;
@Required()
password: string;
}
export class User extends UserCreation {
@Ignore()
password: string;
}
import {serialize} from "@tsed/json-mapper";
import {User} from "./User";
describe("User", () => {
it("should serialize a model", () => {
const user = new User();
user._id = "12345";
user.firstName = "John";
user.lastName = "Doe";
user.password = "secretpassword";
const result = serialize(user);
expect(result).toEqual({
firstName: "John",
lastName: "Doe"
});
});
});
import {serialize} from "@tsed/json-mapper";
import {User} from "./User";
describe("User", () => {
it("should serialize a model", () => {
const user = new User();
user._id = "12345";
user.firstName = "John";
user.lastName = "Doe";
user.password = "secretpassword";
const result = serialize(user);
expect(result).toEqual({
firstName: "John",
lastName: "Doe"
});
});
});
With a callback
Ignore decorator since v6.13.0 accept a callback which will be called when a property have been serialized or deserialized. The callback will give you more control over the way to ignore a property.
class User {
@Name("id")
_id: string;
@Property()
firstName: string;
@Property()
lastName: string;
@Ignore((value, ctx) => ctx.endpoint) // should not serialized when the object is returned by an endpoint.
password: string;
@Ignore((value, ctx) => ctx.mongoose) // should be serialized when the object is returned by an endpoint.
scopes: string[];
@Ignore()
alwaysIgnored: string;
}
Here is the available options on ctx:
Prop | Type | Description |
---|---|---|
endpoint | boolean | It's an endpoint context |
mongoose | boolean | It's a mongoose context |
Additional properties
AdditionalProperties decorator can be used to accept any additional properties on a specific model.
import {AdditionalProperties, CollectionOf, Description, Minimum, Property} from "@tsed/schema";
@AdditionalProperties(true)
export class Person {
@Property()
firstName: string;
@Property()
lastName: string;
@Description("Age in years")
@Minimum(0)
age: number;
@CollectionOf(String)
skills: string[];
[type: string]: any;
}
import {deserialize} from "@tsed/json-mapper";
import {Person} from "./Person";
describe("Person", () => {
it("should deserialize a model", () => {
const input = {
firstName: "firstName",
lastName: "lastName",
age: 0,
skills: ["skill1"],
job: "Tech lead"
};
const result = deserialize(input, {
type: Person
});
expect(result).toBeInstanceOf(Person);
expect(result).toEqual({
firstName: "firstName",
lastName: "lastName",
age: 0,
skills: ["skill1"],
job: "Tech lead"
});
});
});
import {deserialize} from "@tsed/json-mapper";
import {Person} from "./Person";
describe("Person", () => {
it("should deserialize a model", () => {
const input = {
firstName: "firstName",
lastName: "lastName",
age: 0,
skills: ["skill1"],
job: "Tech lead"
};
const result = deserialize(input, {
type: Person
});
expect(result).toBeInstanceOf(Person);
expect(result).toEqual({
firstName: "firstName",
lastName: "lastName",
age: 0,
skills: ["skill1"],
job: "Tech lead"
});
});
});
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;
}
OnSerialize
OnSerialize decorator can be used to intercept and change the property value when a serialization is performed on class.
import {OnSerialize} from "@tsed/schema";
export class Person {
@OnSerialize((v) => v + "Test")
property: string;
}
OnDeserialize
OnDeserialize decorator can be used to intercept and change the property value when a deserialization is performed on class.
import {OnDeserialize} from "@tsed/schema";
export class Person {
@OnDeserialize((v) => v + "Test")
property: string;
}
Type mapper
@tsed/json-mapper
use classes to transform an input value to the expected value:
Type | Mapper |
---|---|
Primitives | PrimitiveMapper, |
Symbol | SymbolMapper, |
Objects | DateMapper, |
It's possible to add your own type mapper by using the JsonMapper decorator on a class. Just copy a mapper implementation and import the mapper in your application.
Primitives
PrimitiveMapper is responsible to map the primitive value like Boolean
, Number
or String
.
import {nameOf} from "@tsed/core";
import {JsonMapper} from "../decorators/jsonMapper.js";
import {JsonMapperCtx, JsonMapperMethods} from "../interfaces/JsonMapperMethods.js";
function isNullish(data: any) {
return [null, "null"].includes(data);
}
export class CastError extends Error {
name = "CAST_ERROR";
constructor(message: string) {
super(`Cast error. ${message}`);
}
}
/**
* Mapper for the `String`, `Number`, `BigInt` and `Boolean` types.
* @jsonmapper
* @component
*/
@JsonMapper(String, Number, Boolean, BigInt)
export class PrimitiveMapper implements JsonMapperMethods {
deserialize<T>(data: any, ctx: JsonMapperCtx): string | number | boolean | void | null | BigInt {
return (this as any)[nameOf(ctx.type)] ? (this as any)[nameOf(ctx.type)](data, ctx) : undefined;
}
serialize(object: string | number | boolean | BigInt, ctx: JsonMapperCtx): string | number | boolean | BigInt {
return (this as any)[nameOf(ctx?.type)] && typeof object !== "object" ? (this as any)[nameOf(ctx.type)](object, ctx) : object;
}
protected String(data: any) {
return data === null ? null : "" + data;
}
protected Boolean(data: any) {
if (["true", "1", true].includes(data)) return true;
if (["false", "0", false].includes(data)) return false;
if (isNullish(data)) return null;
if (data === undefined) return undefined;
return !!data;
}
protected Number(data: any) {
if (isNullish(data)) return null;
if (data === undefined) return data;
const n = +data;
if (isNaN(n)) {
throw new CastError("Expression value is not a number.");
}
return n;
}
protected BigInt(data: any) {
if (isNullish(data)) return null;
return BigInt(data);
}
}
import {catchError} from "@tsed/core";
import {PrimitiveMapper} from "./PrimitiveMapper.js";
describe("PrimitiveMapper", () => {
describe("deserialize()", () => {
it("should return value (number => string)", () => {
const mapper = new PrimitiveMapper();
const data = 1;
const ctx = {
type: String,
collectionType: undefined,
next: vi.fn()
};
const value = mapper.deserialize(data, ctx as never);
expect(value).toEqual("1");
});
it("should return value (string => string)", () => {
const mapper = new PrimitiveMapper();
const data = "1";
const ctx = {
type: String,
collectionType: undefined,
next: vi.fn()
};
const value = mapper.deserialize(data, ctx as never);
expect(value).toEqual("1");
});
it("should return value (null => number)", () => {
const mapper = new PrimitiveMapper();
const data = null;
const ctx = {
type: Number,
collectionType: undefined,
next: vi.fn()
};
const value = mapper.deserialize(data, ctx as never);
expect(value).toEqual(null);
});
it("should return value ('null' => number)", () => {
const mapper = new PrimitiveMapper();
const data = "null";
const ctx = {
type: Number,
collectionType: undefined,
next: vi.fn()
};
const value = mapper.deserialize(data, ctx as never);
expect(value).toEqual(null);
});
it("should return value (string => number)", () => {
const mapper = new PrimitiveMapper();
const data = "1";
const ctx = {
type: Number,
collectionType: undefined,
next: vi.fn()
};
const value = mapper.deserialize(data, ctx as never);
expect(value).toEqual(1);
});
it("should return value (number => number)", () => {
const mapper = new PrimitiveMapper();
const data = 1;
const ctx = {
type: Number,
collectionType: undefined,
next: vi.fn()
};
const value = mapper.deserialize(data, ctx as never);
expect(value).toEqual(1);
});
it("should return value (wrong number => number)", () => {
const mapper = new PrimitiveMapper();
const data = "t1";
const ctx = {
type: Number,
collectionType: undefined,
next: vi.fn()
};
let actualError: any = catchError(() => mapper.deserialize(data, ctx as never));
expect(actualError.message).toEqual("Cast error. Expression value is not a number.");
});
it("should return value (truthy => boolean)", () => {
const mapper = new PrimitiveMapper();
const ctx: any = {
type: Boolean,
collectionType: undefined,
next: vi.fn()
};
expect(mapper.deserialize(1, ctx)).toEqual(true);
expect(mapper.deserialize("1", ctx)).toEqual(true);
expect(mapper.deserialize("true", ctx)).toEqual(true);
expect(mapper.deserialize(true, ctx)).toEqual(true);
});
it("should return value (falsy => boolean)", () => {
const mapper = new PrimitiveMapper();
const ctx: any = {
type: Boolean,
collectionType: undefined,
next: vi.fn()
};
expect(mapper.deserialize(0, ctx)).toEqual(false);
expect(mapper.deserialize("0", ctx)).toEqual(false);
expect(mapper.deserialize("", ctx)).toEqual(false);
expect(mapper.deserialize("false", ctx)).toEqual(false);
expect(mapper.deserialize(false, ctx)).toEqual(false);
expect(mapper.deserialize(undefined, ctx)).toBeUndefined();
});
it("should return value (null => boolean)", () => {
const mapper = new PrimitiveMapper();
const ctx: any = {
type: Boolean,
collectionType: undefined,
next: vi.fn()
};
expect(mapper.deserialize(null, ctx)).toEqual(null);
expect(mapper.deserialize("null", ctx)).toEqual(null);
});
});
describe("serialize()", () => {
it("should return value (string to string)", () => {
const mapper = new PrimitiveMapper();
const value = mapper.serialize("1", {type: String} as any);
expect(value).toEqual("1");
});
it("should return value (string to number)", () => {
const mapper = new PrimitiveMapper();
const value = mapper.serialize("1", {type: Number} as any);
expect(value).toEqual(1);
});
it("should return value (object)", () => {
const mapper = new PrimitiveMapper();
// in this case it's probably intended to be an object (or an error but we can decide for the developer and we can broke the code)
// TODO: for the major version, we can return undefined or throw an error?
const value = mapper.serialize({"1": "1"} as any, {type: Number} as any);
expect(value).toEqual({"1": "1"});
});
it("should return value (null)", () => {
const mapper = new PrimitiveMapper();
const value = mapper.serialize(null as any, {type: Number} as any);
expect(value).toEqual(null);
});
it("should return value (undefined)", () => {
const mapper = new PrimitiveMapper();
const value = mapper.serialize(undefined as any, {type: Number} as any);
expect(value).toEqual(undefined);
});
});
});
import {catchError} from "@tsed/core";
import {PrimitiveMapper} from "./PrimitiveMapper.js";
describe("PrimitiveMapper", () => {
describe("deserialize()", () => {
it("should return value (number => string)", () => {
const mapper = new PrimitiveMapper();
const data = 1;
const ctx = {
type: String,
collectionType: undefined,
next: vi.fn()
};
const value = mapper.deserialize(data, ctx as never);
expect(value).toEqual("1");
});
it("should return value (string => string)", () => {
const mapper = new PrimitiveMapper();
const data = "1";
const ctx = {
type: String,
collectionType: undefined,
next: vi.fn()
};
const value = mapper.deserialize(data, ctx as never);
expect(value).toEqual("1");
});
it("should return value (null => number)", () => {
const mapper = new PrimitiveMapper();
const data = null;
const ctx = {
type: Number,
collectionType: undefined,
next: vi.fn()
};
const value = mapper.deserialize(data, ctx as never);
expect(value).toEqual(null);
});
it("should return value ('null' => number)", () => {
const mapper = new PrimitiveMapper();
const data = "null";
const ctx = {
type: Number,
collectionType: undefined,
next: vi.fn()
};
const value = mapper.deserialize(data, ctx as never);
expect(value).toEqual(null);
});
it("should return value (string => number)", () => {
const mapper = new PrimitiveMapper();
const data = "1";
const ctx = {
type: Number,
collectionType: undefined,
next: vi.fn()
};
const value = mapper.deserialize(data, ctx as never);
expect(value).toEqual(1);
});
it("should return value (number => number)", () => {
const mapper = new PrimitiveMapper();
const data = 1;
const ctx = {
type: Number,
collectionType: undefined,
next: vi.fn()
};
const value = mapper.deserialize(data, ctx as never);
expect(value).toEqual(1);
});
it("should return value (wrong number => number)", () => {
const mapper = new PrimitiveMapper();
const data = "t1";
const ctx = {
type: Number,
collectionType: undefined,
next: vi.fn()
};
let actualError: any = catchError(() => mapper.deserialize(data, ctx as never));
expect(actualError.message).toEqual("Cast error. Expression value is not a number.");
});
it("should return value (truthy => boolean)", () => {
const mapper = new PrimitiveMapper();
const ctx: any = {
type: Boolean,
collectionType: undefined,
next: vi.fn()
};
expect(mapper.deserialize(1, ctx)).toEqual(true);
expect(mapper.deserialize("1", ctx)).toEqual(true);
expect(mapper.deserialize("true", ctx)).toEqual(true);
expect(mapper.deserialize(true, ctx)).toEqual(true);
});
it("should return value (falsy => boolean)", () => {
const mapper = new PrimitiveMapper();
const ctx: any = {
type: Boolean,
collectionType: undefined,
next: vi.fn()
};
expect(mapper.deserialize(0, ctx)).toEqual(false);
expect(mapper.deserialize("0", ctx)).toEqual(false);
expect(mapper.deserialize("", ctx)).toEqual(false);
expect(mapper.deserialize("false", ctx)).toEqual(false);
expect(mapper.deserialize(false, ctx)).toEqual(false);
expect(mapper.deserialize(undefined, ctx)).toBeUndefined();
});
it("should return value (null => boolean)", () => {
const mapper = new PrimitiveMapper();
const ctx: any = {
type: Boolean,
collectionType: undefined,
next: vi.fn()
};
expect(mapper.deserialize(null, ctx)).toEqual(null);
expect(mapper.deserialize("null", ctx)).toEqual(null);
});
});
describe("serialize()", () => {
it("should return value (string to string)", () => {
const mapper = new PrimitiveMapper();
const value = mapper.serialize("1", {type: String} as any);
expect(value).toEqual("1");
});
it("should return value (string to number)", () => {
const mapper = new PrimitiveMapper();
const value = mapper.serialize("1", {type: Number} as any);
expect(value).toEqual(1);
});
it("should return value (object)", () => {
const mapper = new PrimitiveMapper();
// in this case it's probably intended to be an object (or an error but we can decide for the developer and we can broke the code)
// TODO: for the major version, we can return undefined or throw an error?
const value = mapper.serialize({"1": "1"} as any, {type: Number} as any);
expect(value).toEqual({"1": "1"});
});
it("should return value (null)", () => {
const mapper = new PrimitiveMapper();
const value = mapper.serialize(null as any, {type: Number} as any);
expect(value).toEqual(null);
});
it("should return value (undefined)", () => {
const mapper = new PrimitiveMapper();
const value = mapper.serialize(undefined as any, {type: Number} as any);
expect(value).toEqual(undefined);
});
});
});
Cheat sheet
Input | Type | Output |
---|---|---|
1 | String | "1" |
"1" | String | "1" |
null | Number | null |
"null" | Number | null |
"1" | Number | 1 |
1 | Number | 1 |
"to1" | Number | Throw Bad Request. This is the only case where JsonMapper throw a cast type error. |
true | Boolean | true |
"true" | Boolean | true |
"1" | Boolean | true |
1 | Boolean | true |
false | Boolean | false |
"false" | Boolean | false |
"0" | Boolean | false |
0 | Boolean | false |
"" | Boolean | false |
"null" | Boolean | null |
undefined | Boolean | undefined |
Symbol
SymbolMapper is responsible to map a String
to Symbol
or a Symbol
to a String
.
import {JsonMapper} from "../decorators/jsonMapper.js";
import {JsonMapperMethods} from "../interfaces/JsonMapperMethods.js";
/**
* Mapper for the `Symbol` type.
*
* @jsonmapper
* @component
*/
@JsonMapper(Symbol)
export class SymbolMapper implements JsonMapperMethods {
deserialize(data: string): symbol {
return Symbol.for(data);
}
serialize(object: Symbol): any {
return object.toString().replace("Symbol(", "").replace(")", "");
}
}
import {SymbolMapper} from "./SymbolMapper.js";
describe("SymbolMapper", () => {
describe("deserialize()", () => {
it("should return value", () => {
const mapper = new SymbolMapper();
const value = mapper.deserialize("SYMBOL");
expect(typeof value).toEqual("symbol");
expect(value.toString()).toEqual("Symbol(SYMBOL)");
});
});
describe("serialize()", () => {
it("should return value", () => {
const mapper = new SymbolMapper();
const value = mapper.serialize(Symbol.for("SYMBOL"));
expect(value).toEqual("SYMBOL");
});
});
});
import {SymbolMapper} from "./SymbolMapper.js";
describe("SymbolMapper", () => {
describe("deserialize()", () => {
it("should return value", () => {
const mapper = new SymbolMapper();
const value = mapper.deserialize("SYMBOL");
expect(typeof value).toEqual("symbol");
expect(value.toString()).toEqual("Symbol(SYMBOL)");
});
});
describe("serialize()", () => {
it("should return value", () => {
const mapper = new SymbolMapper();
const value = mapper.serialize(Symbol.for("SYMBOL"));
expect(value).toEqual("SYMBOL");
});
});
});
Date
DateMapper is responsible to map a Number
, String
to a Date
or a Date
to a String
.
import {isBoolean} from "@tsed/core";
import {JsonMapper} from "../decorators/jsonMapper.js";
import {JsonMapperMethods} from "../interfaces/JsonMapperMethods.js";
/**
* Mapper for `Date` type.
* @jsonmapper
* @component
*/
@JsonMapper(Date)
export class DateMapper implements JsonMapperMethods {
deserialize(data: string | number): Date;
deserialize(data: boolean | null | undefined): boolean | null | undefined;
deserialize(data: any): any {
// don't convert unexpected data. In normal case, Ajv reject unexpected data.
// But by default, we have to skip data deserialization and let user to apply
// the right mapping
if (isBoolean(data) || data === null || data === undefined) {
return data;
}
return new Date(data);
}
serialize(object: Date): any {
return object ? new Date(object).toISOString() : object;
}
}
import {DateMapper} from "./DateMapper.js";
describe("DateMapper", () => {
describe("deserialize()", () => {
it("should return a Date when the data is a string", () => {
const date = new Date();
const mapper = new DateMapper();
const value = mapper.deserialize(date.toISOString());
expect(value).toEqual(date);
});
it("should return a Date when the data is a number", () => {
const date = new Date();
const mapper = new DateMapper();
const value = mapper.deserialize(date.getTime());
expect(value).toEqual(date);
});
it("should return value when the data is a boolean/null/undefined", () => {
const date = new Date();
const mapper = new DateMapper();
expect(mapper.deserialize(false)).toEqual(false);
expect(mapper.deserialize(true)).toEqual(true);
expect(mapper.deserialize(null)).toEqual(null);
expect(mapper.deserialize(undefined)).toBeUndefined();
});
});
describe("serialize()", () => {
it("should return value", () => {
const date = new Date();
const mapper = new DateMapper();
const value = mapper.serialize(date);
expect(value).toEqual(date.toISOString());
});
});
});
import {DateMapper} from "./DateMapper.js";
describe("DateMapper", () => {
describe("deserialize()", () => {
it("should return a Date when the data is a string", () => {
const date = new Date();
const mapper = new DateMapper();
const value = mapper.deserialize(date.toISOString());
expect(value).toEqual(date);
});
it("should return a Date when the data is a number", () => {
const date = new Date();
const mapper = new DateMapper();
const value = mapper.deserialize(date.getTime());
expect(value).toEqual(date);
});
it("should return value when the data is a boolean/null/undefined", () => {
const date = new Date();
const mapper = new DateMapper();
expect(mapper.deserialize(false)).toEqual(false);
expect(mapper.deserialize(true)).toEqual(true);
expect(mapper.deserialize(null)).toEqual(null);
expect(mapper.deserialize(undefined)).toBeUndefined();
});
});
describe("serialize()", () => {
it("should return value", () => {
const date = new Date();
const mapper = new DateMapper();
const value = mapper.serialize(date);
expect(value).toEqual(date.toISOString());
});
});
});
::: warn Ts.ED doesn't transform Date to date format or hours format because it depends on each project guidelines.
But you can easily implement a Date mapper for each format with the Date API or moment:
import {isBoolean} from "@tsed/core";
import {DateFormat} from "@tsed/schema";
import {serialize, JsonMapper, JsonMapperContext, JsonMapperMethods} from "../../src/index";
@JsonMapper(Date)
export class DateMapper implements JsonMapperMethods {
deserialize(data: string | number, ctx: JsonMapperContext): Date;
deserialize(data: boolean | null | undefined, ctx: JsonMapperContext): boolean | null | undefined;
deserialize(data: any, ctx: JsonMapperContext): any {
// don't convert unexpected data. In normal case, Ajv reject unexpected data.
// But by default, we have to skip data deserialization and let user to apply
// the right mapping
if (isBoolean(data) || data === null || data === undefined) {
return data;
}
return new Date(data);
}
serialize(object: Date, ctx: JsonMapperContext): any {
const date = new Date(object);
switch (ctx.options.format) {
case "date":
const y = date.getUTCFullYear();
const m = ("0" + (date.getUTCMonth() + 1)).slice(-2);
const d = ("0" + date.getUTCDate()).slice(-2);
return `${y}-${m}-${d}`;
default:
return new Date(object).toISOString();
}
}
}
:::
Create your own type mapper
It's possible de to change add your own type mapper by using the JsonMapper decorator on a class. Just copy a mapper implementation and import the mapper in your application.
A mapper must declare the type it must work on and implement two methods: serialize and deserialize.
import {JsonMapper, JsonMapperMethods, JsonMapperCtx} from "@tsed/json-mapper";
@JsonMapper(String)
export class TheTypeMapper implements JsonMapperMethods {
deserialize(data: any, ctx: JsonMapperCtx): String {
return JSON.stringify(data) + ":deserialize";
}
serialize(data: any, ctx: JsonMapperCtx): String {
return JSON.stringify(data) + ":serialize";
}
}
Then import your new mapper in your Server.ts as following:
import {Configuration} from "@tsed/di";
import "./mappers/TheTypeMapper";
@Configuration({
mount: {
"/rest": []
}
})
export class Server {}
Moment
Moment.js is a powerful library to transform any formatted date string to a Moment instance.
You can change the Date mapper behavior to transform string to a Moment instance.
import {JsonMapper, JsonMapperMethods} from "@tsed/json-mapper";
import moment, {Moment} from "moment";
@JsonMapper(Date, "Moment")
export class MomentMapper implements JsonMapperMethods {
deserialize(data: string, ctx: JsonMapperCtx): Moment {
return moment(data, ["YYYY-MM-DD hh:mm:ss"]);
}
serialize(data: Date | Moment, ctx: JsonMapperCtx): string {
const format = ctx.options?.format;
switch (format) {
case "date":
return moment(data).format("YYYY-MM-DD");
default:
return moment(data).format("YYYY-MM-DD hh:mm:ss");
}
}
}
import {Configuration} from "@tsed/di";
import "./mappers/MomentMapper.js"; // just import mapper to be available
@Configuration({})
export class Server {}
import {Moment} from "moment";
import {Property} from "@tsed/schema";
export class Person {
@Property(Date) // or @Property(String) + @DateTime()
birthdate: Moment;
}