Skip to content

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

typescript
@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:

typescript
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:

ts
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[];
}
ts
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"]
    });
  });
});
ts
import {deserialize, serialize} from "@tsed/json-mapper";
import {expect} from "chai";
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).to.be.instanceof(Person);
    expect(result).to.deep.eq({
      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.to.be.instance.of(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:

ts
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.

ts
import {Property} from "@tsed/schema";

export class User {
  _id: string;

  @Property()
  firstName: string;

  @Property()
  lastName: string;

  password: string;
}
ts
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"
    });
  });
});
ts
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.

ts
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;
}
ts
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"
    });
  });
});
ts
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"
    });
  });
});

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.

typescript
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:

PropTypeDescription
endpointbooleanIt's an endpoint context
mongoosebooleanIt's a mongoose context

Additional properties

AdditionalProperties decorator can be used to accept any additional properties on a specific model.

ts
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;
}
ts
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"
    });
  });
});
ts
import {deserialize, serialize} from "@tsed/json-mapper";
import {expect} from "chai";
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).to.be.instanceof(Person);
    expect(result).to.deep.eq({
      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.

ts
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.

typescript
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.

typescript
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:

TypeMapper
PrimitivesPrimitiveMapper,
SymbolSymbolMapper,
ObjectsDateMapper,

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.

ts
import {nameOf} from "@tsed/core";
import {JsonMapper} from "../decorators/jsonMapper";
import {JsonMapperCtx, JsonMapperMethods} from "../interfaces/JsonMapperMethods";

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): string | number | boolean | BigInt {
    return 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;

    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);
  }
}
ts
import {catchError} from "@tsed/core";
import Sinon from "sinon";
import {PrimitiveMapper} from "./PrimitiveMapper";

describe("PrimitiveMapper", () => {
  describe("deserialize()", () => {
    it("should return value (number => string)", () => {
      const mapper = new PrimitiveMapper();
      const data = 1;
      const ctx = {
        type: String,
        collectionType: undefined,
        next: Sinon.stub()
      };

      const value = mapper.deserialize(data, ctx);

      expect(value).toEqual("1");
    });
    it("should return value (string => string)", () => {
      const mapper = new PrimitiveMapper();
      const data = "1";
      const ctx = {
        type: String,
        collectionType: undefined,
        next: Sinon.stub()
      };

      const value = mapper.deserialize(data, ctx);

      expect(value).toEqual("1");
    });
    it("should return value (null => number)", () => {
      const mapper = new PrimitiveMapper();
      const data = null;
      const ctx = {
        type: Number,
        collectionType: undefined,
        next: Sinon.stub()
      };

      const value = mapper.deserialize(data, ctx);

      expect(value).toEqual(null);
    });
    it("should return value ('null' => number)", () => {
      const mapper = new PrimitiveMapper();
      const data = "null";
      const ctx = {
        type: Number,
        collectionType: undefined,
        next: Sinon.stub()
      };

      const value = mapper.deserialize(data, ctx);

      expect(value).toEqual(null);
    });
    it("should return value (string => number)", () => {
      const mapper = new PrimitiveMapper();
      const data = "1";
      const ctx = {
        type: Number,
        collectionType: undefined,
        next: Sinon.stub()
      };

      const value = mapper.deserialize(data, ctx);

      expect(value).toEqual(1);
    });
    it("should return value (number => number)", () => {
      const mapper = new PrimitiveMapper();
      const data = 1;
      const ctx = {
        type: Number,
        collectionType: undefined,
        next: Sinon.stub()
      };

      const value = mapper.deserialize(data, ctx);

      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: Sinon.stub()
      };

      let actualError: any = catchError(() => mapper.deserialize(data, ctx));

      expect(actualError.message).toEqual("Cast error. Expression value is not a number.");
    });
    it("should return value (truthy => boolean)", () => {
      const mapper = new PrimitiveMapper();
      const ctx = {
        type: Boolean,
        collectionType: undefined,
        next: Sinon.stub()
      };

      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 = {
        type: Boolean,
        collectionType: undefined,
        next: Sinon.stub()
      };

      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 = {
        type: Boolean,
        collectionType: undefined,
        next: Sinon.stub()
      };

      expect(mapper.deserialize(null, ctx)).toEqual(null);
      expect(mapper.deserialize("null", ctx)).toEqual(null);
    });
  });
  describe("serialize()", () => {
    it("should return value", () => {
      const mapper = new PrimitiveMapper();

      const value = mapper.serialize("1");

      expect(value).toEqual("1");
    });
  });
});

Cheat sheet

InputTypeOutput
1String"1"
"1"String"1"
nullNumbernull
"null"Numbernull
"1"Number1
1Number1
"to1"NumberThrow Bad Request. This is the only case where JsonMapper throw a cast type error.
trueBooleantrue
"true"Booleantrue
"1"Booleantrue
1Booleantrue
falseBooleanfalse
"false"Booleanfalse
"0"Booleanfalse
0Booleanfalse
""Booleanfalse
"null"Booleannull
undefinedBooleanundefined

Symbol

SymbolMapper is responsible to map a String to Symbol or a Symbol to a String.

ts
import {JsonMapper} from "../decorators/jsonMapper";
import {JsonMapperMethods} from "../interfaces/JsonMapperMethods";

/**
 * 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(")", "");
  }
}
ts
import {SymbolMapper} from "./SymbolMapper";

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.

ts
import {isBoolean} from "@tsed/core";
import {JsonMapper} from "../decorators/jsonMapper";
import {JsonMapperMethods} from "../interfaces/JsonMapperMethods";

/**
 * 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;
  }
}
ts
import {DateMapper} from "./DateMapper";

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:

typescript
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.

typescript
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:

typescript
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.

ts
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");
    }
  }
}
ts
import {Configuration} from "@tsed/di";
import "./mappers/MomentMapper"; // just import mapper to be available

@Configuration({})
export class Server {}
typescript
import {Moment} from "moment";
import {Property} from "@tsed/schema";

export class Person {
  @Property(Date) // or @Property(String) + @DateTime()
  birthdate: Moment;
}

Released under the MIT License.