---
head:
  - - meta
    - name: description
      content: Discover how to use the @tsed/json-mapper package to map a plain object to a model and a model to a plain object. Learn how to configure the package and use decorators to describe a model and transform object depending on which operation you want to perform.
  - - meta
    - name: keywords
      content: class model decorators ts.ed express typescript node.js javascript jsonschema json mapper serialization deserialization
---
# 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 @@function:serialize@@ and @@function: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](/docs/model.md#groups-strict-mode) 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:

::: code-group

```ts [Person.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 [Jest]
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 [Vitest]
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"]
    });
  });
});
```

:::

::: tip Note
Take a look on Jest/Mocha tabs to see @@function:serialize@@ and @@function: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()]);
  }
}
```

::: tip Note
In the previous example, we can see [Returns](/ai/api/specs/schema/types/decorators/operations/decorator-returns.md) decorator usage.
In all case, Ts.ED infer the returned value and apply the correct transformation on your response.

[Returns](/ai/api/specs/schema/types/decorators/operations/decorator-returns.md) 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.

::: code-group

```ts [User.ts]
import {Property} from "@tsed/schema";

export class User {
  _id: string;

  @Property()
  firstName: string;

  @Property()
  lastName: string;

  password: string;
}
```

```ts [Jest]
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 [Mocha]
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)

::: warning deprecated
This decorator is deprecated. Use [Groups](/ai/api/specs/schema/types/decorators/common/decorator-groups.md) decorator instead of.
:::

### Usage

[Ignore](/ai/api/specs/schema/types/decorators/common/decorator-ignore.md) 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.

::: code-group

```ts [User.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 [Jest]
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 [Vitest]
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](/ai/api/specs/schema/types/decorators/common/decorator-ignore.md) 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:

| Prop     | Type    | Description              |
| -------- | ------- | ------------------------ |
| endpoint | boolean | It's an endpoint context |
| mongoose | boolean | It's a mongoose context  |

## Additional properties

[AdditionalProperties](/ai/api/specs/schema/types/decorators/common/decorator-additional-properties.md) decorator can be used to accept any additional properties on a specific model.

::: code-group

```ts [Person.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 [Jest]
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 [Vitest]
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](/ai/api/specs/schema/types/decorators/common/decorator-name.md) 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](/ai/api/specs/json-mapper/types/decorators/decorator-on-serialize.md) 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](/ai/api/specs/json-mapper/types/decorators/decorator-on-deserialize.md) 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:

| Type       | Mapper               |
| ---------- | -------------------- |
| Primitives | [PrimitiveMapper](/ai/api/specs/json-mapper/types/components/class-primitive-mapper.md), |
| Symbol     | [SymbolMapper](/ai/api/specs/json-mapper/types/components/class-symbol-mapper.md),    |
| Objects    | [DateMapper](/ai/api/specs/json-mapper/types/components/class-date-mapper.md),      |

It's possible to add your own type mapper by using the [JsonMapper](/ai/api/specs/json-mapper/types/decorators/decorator-json-mapper.md) decorator on a class. Just copy a mapper implementation
and import the mapper in your application.

### Primitives

[PrimitiveMapper](/ai/api/specs/json-mapper/types/components/class-primitive-mapper.md) is responsible to map the primitive value like `Boolean`, `Number` or `String`.

::: code-group

```ts [PrimitiveMapper]
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);
}

/**
 * Error thrown when a primitive conversion cannot be performed safely.
 */
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);
  }
}
```

```ts [Jest]
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);
    });
  });
});
```

```ts [Vitest]
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](/ai/api/specs/json-mapper/types/components/class-symbol-mapper.md) is responsible to map a `String` to `Symbol` or a `Symbol` to a `String`.

::: code-group

```ts [SymbolMapper]
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(")", "");
  }
}
```

```ts [Jest]
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");
    });
  });
});
```

```ts [Vitest]
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](/ai/api/specs/json-mapper/types/components/class-date-mapper.md) is responsible to map a `Number`, `String` to a `Date` or a `Date` to a `String`.

::: code-group

```ts [DateMapper]
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;
  }
}
```

```ts [Jest]
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());
    });
  });
});
```

```ts [Vitest]
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());
    });
  });
});
```

:::

::: warning
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](/ai/api/specs/json-mapper/types/decorators/decorator-json-mapper.md) 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](https://momentjs.com) 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.
::: code-group

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

@Configuration({})
export class Server {}
```

```typescript [Example]
import {Moment} from "moment";
import {Property} from "@tsed/schema";

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

:::
