npm package: @sm2dev/typescript-json-serializer
Description: Typescript library to serialize classes into json and deserialize json into classes.
Author: Gillian Pérard
Latest version: 6.0.1

# typescript-json-serializer

![](https://github.com/GillianPerard/typescript-json-serializer/workflows/Build/badge.svg)
![npm](https://img.shields.io/npm/dt/typescript-json-serializer)
![npm bundle size (version)](https://img.shields.io/bundlephobia/minzip/typescript-json-serializer/6.0.1)
[![Coverage Status](https://coveralls.io/repos/github/GillianPerard/typescript-json-serializer/badge.svg)](https://coveralls.io/github/GillianPerard/typescript-json-serializer)
[![Known Vulnerabilities](https://snyk.io/test/github/gillianperard/typescript-json-serializer/badge.svg?targetFile=package.json)](https://snyk.io/test/github/gillianperard/typescript-json-serializer?targetFile=package.json)

A typescript library to deserialize json into typescript classes and serialize classes into json.

## Summary

1. [Installation](#installation)
2. [Usage](#usage)
3. [API](#api)
4. [Development](#development)
5. [Thanks to](#thanks-to)

## Installation

```sh
npm install @sm2dev/typescript-json-serializer --save
# or
yarn add @sm2dev/typescript-json-serializer
```

You also need to set **experimentalDecorators** and **emitDecoratorMetadata** to true into the tsconfig.json file.

For example:

```json
{
    "compilerOptions": {
        ...
        "emitDecoratorMetadata": true,
        "experimentalDecorators": true,
        ...
    }
}
```

## Usage

```typescript
import { JsonSerializer, throwError } from '@sm2dev/typescript-json-serializer';

import { json } from '../json/data';
import { Organization } from '../models/organization';

// Instantiate a default serializer
const defaultSerializer = new JsonSerializer();

// Or you can instantiate a serializer with your custom options
const customSerializer = new JsonSerializer({
    // Throw errors instead of logging
    errorCallback: throwError,

    // Allow all nullish values
    nullishPolicy: {
        undefined: 'allow',
        null: 'allow'
    },

    // Disallow additional properties (non JsonProperty)
    additionalPropertiesPolicy: 'disallow',

    // e.g. if all the properties in the json object are prefixed by '_'
    formatPropertyName: (propertyName: string) => `_${propertyName}`,
})

// Deserialize
const organization = defaultSerializer.deserialize(json, Organization);

// Serialize
const data = defaultSerializer.serialize(organization);
```

### Examples

#### Classes

```typescript
// Import decorators from library
import { JsonObject, JsonProperty } from '@sm2dev/typescript-json-serializer';

// Enums
export enum Gender {
    Female,
    Male,
    Other
}

export enum Status {
    Alive = 'Alive',
    Sick = 'Sick',
    DeadAndAlive = 'Dead and alive',
    Dead = 'Dead'
}

// Create a JsonObject class: LivingBeing

// JsonObject decorator
@JsonObject()
export class LivingBeing {

    /** The living being id (PK) */
    @JsonProperty() id: number;
}


// Create a JsonObject class that extends LivingBeing: Human

@JsonObject()
export class Human extends LivingBeing {
    constructor(
        // This comment works
        // Override LivingBeing id property name
        // and set required to true
        @JsonProperty({name: 'humanId', required: true})
        public id: number,
        @JsonProperty() public name: string,
        @JsonProperty() public gender: Gender,
        /** This comment works */
        @JsonProperty() public readonly birthDate: Date
    ) {
        super();
        this.id = id;
    }
}


// Create a JsonObject class: PhoneNumber

@JsonObject()
export class PhoneNumber {
    @JsonProperty() countryCode: string;
    @JsonProperty() value: string;
}


// Create a JsonObject class that extends Human: Employee

@JsonObject()
export class Employee extends Human {
    /** The employee's email */
    @JsonProperty({required: true}) email: string;

    /** Predicate function to determine if the property type
      * is PhoneNumber or a primitive type */
    @JsonProperty({
        type: property => {
            if (property && property.value !== undefined) {
                return PhoneNumber;
            }
        }
    })
    phoneNumber: PhoneNumber | string;

    constructor(
        public name: string,
        // Override human id property name
        // (keep the require to true from Human id)
        @JsonProperty('employeeId') public id: number,
        public gender: Gender,
        public birthDate: Date
    ) {
        super(name, id, gender, birthDate);
    }
}


// Create a JsonObject class that extends LivingBeing: Animal

@JsonObject()
export class Animal extends LivingBeing {
    @JsonProperty() name: string | undefined;
    @JsonProperty() birthDate: Date;
    @JsonProperty() numberOfPaws: number;
    @JsonProperty() gender: Gender;

    // Enum value (string)
    @JsonProperty() status: Status;

    // Specify the property name of json property if needed
    @JsonProperty('childrenIdentifiers')
    childrenIds: Array<number>;

    constructor(name: string | undefined) {
        super();
        this.name = name;
    }

}


// Create a JsonObject class that extends Animal (which extends LivingBeing): Panther

@JsonObject()
export class Panther extends Animal {

    @JsonProperty() color: string;

    // JsonProperty directly inside the constructor
    // for property parameters
    public constructor(
        name: string | undefined,
        @JsonProperty() public isSpeckled: boolean
    ) {
        super(name);
    }

}


// Create a JsonObject class that extends Animal (which extends LivingBeing): Snake

// When the library instatiates a class during deserilization it does not know the constructor params value
// so you need to specify them.
// To ways are available
// - by passing default value to those params in the constructor
// - by passing the `constructorParams` options to `JsonObject` (see below); it allows to keep clean constructor
@JsonObject({ constructorParams: [{}] })
export class Snake extends Animal {

    @JsonProperty() isPoisonous: boolean;

    public constructor(args: { name: string | undefined; isPoisonous: boolean }) {
        super(args.name);
        this.isPoisonous = args.isPoisonous;
    }

}


// Create a JsonObject empty class that extends Animal (which extends LivingBeing): UnknownAnimal

@JsonObject()
export class UnknownAnimal extends Animal {
    public constructor(name: string) {
        super(name);
    }
}


// Create a JsonObject class: Zoo

// Function to transform coordinates into an array
const coordinatesToArray = (coordinates: {
    x: number;
    y: number;
    z: number;
}): Array<number> => {
    return Object.values(coordinates);
};

// Function to transform an array into coordinates
const arrayToCoordinates = (array: Array<number>): {
    x: number;
    y: number;
    z: number
} => {
    return {
        x: array[0],
        y: array[1],
        z: array[2]
    };
};

// A predicate function use to determine what is the
// right type of the data (Snake or Panther)
const snakeOrPanther = animal => {
    return animal && animal['isPoisonous'] !== undefined
        ? Snake
        : Panther;
};

@JsonObject()
export class Zoo {

    // Here you do not need to specify the type
    // inside the decorator
    @JsonProperty() boss: Employee;

    @JsonProperty() city: string;
    @JsonProperty() country: string;

    // Property with transform functions executed respectively
    // on serialize and on deserialize
    @JsonProperty({
        beforeDeserialize: arrayToCoordinates,
        afterSerialize: coordinatesToArray
    })
    coordinates: { x: number; y: number; z: number };

    // Possibly undefined Set of none-basic type elements
    @JsonProperty({ type: Employee, dataStructure: 'set' })
    employees: Set<Employee> | undefined;

    @JsonProperty() id: number;
    @JsonProperty() name: string;

    // Array of none-basic type elements where you need to
    // specify the name of the json property
    // and use the predicate function to cast the deserialized
    // object into the correct child class
    @JsonProperty({ name: 'Animals', type: snakeOrPanther })
    animals: Array<Animal>;

    // Property that can be Panther or Snake type
    // Use again the predicate function
    @JsonProperty({ type: snakeOrPanther })
    mascot: Panther | Snake;

    // Dictionary of empty child classes
    @JsonProperty({ dataStructure: 'dictionary', type: UnknownAnimal })
    unknownAnimals: { [id: string]: UnknownAnimal };

    // Map of array of PhoneNumber or string
    @JsonProperty({
        type: property => {
            if (property?.value !== undefined) {
                return PhoneNumber;
            }
        }
    })
    phoneBook: Map<string, Array<PhoneNumber | string>>;

    // Property which will be not serialized and deserialized
    // but event accessible and editable from Zoo class.
    public isFree: boolean = true;

    public constructor() { }
}

// Create a JsonObject generic class: Value

@JsonObject()
export class Value<R> {
    @JsonProperty() value: R;

    constructor(value: R) {
        this.value = value;
    }
}

// Create a JsonObject class: Item

@JsonObject()
export class Item {
    @JsonProperty({ name: 'name' })
    private readonly _name: string;

    @JsonProperty({ name: 'version' })
    private readonly _version: number;

    constructor(name: string, version: number) {
        this._name = name;
        this._version = version;
    }
}

// Create a JsonObject class that extends Society: Organization

@JsonObject()
export class Organization extends Society {
    @JsonProperty({ type: Zoo }) zoos: Array<Zoo>;
    @JsonProperty({ dataStructure: 'dictionary' })
    zoosName: { [id: string]: string };

    // To merge multiple properties in a single one
    // use the property `names`.
    // If you don't create your own merge with the `beforeDeserialize`
    // and `afterSerialize` function, it will just merge properties
    // in this one when using `deserialize` and split back
    // when using `serialize`
    @JsonProperty({
        name: [
            'mainShareholder',
            'secondaryShareholder',
            'thirdShareholder'
        ],
        type: Human,
        beforeDeserialize: value => Object.values(value),
        afterSerialize: value => {
            return {
                mainShareholder: value[0],
                secondaryShareholder: value[1],
                thirdShareholder: value[2]
            };
        }
    })
    shareholders: Array<Human>;
    miscellaneous: Value<Item>;
}


// Create a JsonObject class: Society

@JsonObject()
export class Society {
    // Here note the `required` option with the default value `'4'` 
    // it will not throw because the default value will be set instead of `undefined`
    @JsonProperty({ required: true }) id: string = '4';
    @JsonProperty() name: string;
}
```

#### Json data

```typescript
// data.ts
export const data: any = {
    name: 'Zoos Organization',
    zoosName: {
        '15': 'The Greatest Zoo',
        '16': 'Zoo Zoo'
    },
    zoos: [
        {
            id: 15,
            name: 'The Greatest Zoo',
            city: 'Bordeaux',
            coordinates: [1, 2, 3],
            country: 'France',
            boss: {
                employeeId: 1,
                name: 'Bob Razowsky',
                birthDate: '1984-04-03T22:00:00.000Z',
                email: 'bob.razowsky@tgzoo.fr',
                gender: 1,
                phoneNumber: '111-111-1111'
            },
            employees: [
                {
                    employeeId: 1,
                    name: 'Bob Razowsky',
                    birthDate: '1984-04-03T22:00:00.000Z',
                    email: 'bob.razowsky@tgzoo.fr',
                    gender: 1,
                    phoneNumber: '111-111-1111'
                },
                {
                    employeeId: 2,
                    name: 'Mikasa Ackerman',
                    birthDate: '1984-01-11T22:00:00.000Z',
                    email: 'mikasa.ackerman@tgzoo.fr',
                    gender: 0,
                    phoneNumber: '222-222-2222'
                },
                {
                    employeeId: 3,
                    name: 'Red Redington',
                    birthDate: '1970-12-04T22:00:00.000Z',
                    email: 'red.redington@tgzoo.fr',
                    gender: 1,
                    phoneNumber: '333-333-3333'
                },
                {
                    employeeId: 4,
                    name: 'Fried Richter',
                    birthDate: '1994-04-01T22:00:00.000Z',
                    email: 'fried.richter@tgzoo.fr',
                    gender: 1
                }
            ],
            Animals: [
                {
                    id: 1,
                    name: 'Bagheera',
                    birthDate: '2010-01-11T22:00:00.000Z',
                    numberOfPaws: 4,
                    gender: 1,
                    childrenIdentifiers: [2, 3],
                    color: 'black',
                    isSpeckled: false,
                    status: 'Sick'
                },
                {
                    id: 2,
                    birthDate: '2017-03-10T22:00:00.000Z',
                    numberOfPaws: 4,
                    gender: 0,
                    color: 'blond',
                    isSpeckled: true,
                    status: 'Alive'
                },
                {
                    id: 3,
                    name: 'Ka',
                    birthDate: '2018-09-09T00:00:00.000Z',
                    numberOfPaws: 0,
                    gender: 1,
                    isPoisonous: true
                },
                {
                    id: 4,
                    name: 'Schrodinger',
                    numberOfPaws: 4,
                    gender: 1,
                    color: 'brown',
                    isSpeckled: false,
                    status: 'Dead and alive'
                }
            ],
            mascot: {
                id: 1,
                name: 'Bagheera',
                birthDate: '2010-01-11T22:00:00.000Z',
                numberOfPaws: 4,
                gender: 1,
                childrenIdentifiers: [2, 3],
                color: 'black',
                isSpeckled: false,
                status: 'Sick'
            },
            unknownAnimals: {
                '1': {
                    name: null
                }
            },
            phoneBook: {
                '1': [
                    { value: '111-111-1111' },
                    { value: '444-444-4444' }
                ],
                '2': [{ value: '222-222-2222' }],
                '3': ['333-333-3333']
            }
        },
        {
            id: 16,
            name: 'Zoo Zoo',
            city: 'Paris',
            coordinates: [4, 2, 3],
            country: 'France',
            boss: {
                employeeId: 2,
                name: 'Sully',
                birthDate: '1984-08-03T22:00:00.000Z',
                email: 'sully.razowsky@tgzoo.fr',
                gender: 1,
                phoneNumber: {
                    countryCode: '33',
                    value: '0111111111'
                }
            },
            employees: [],
            Animals: [],
            mascot: null,
            unknownAnimals: {}
        }
    ],
    mainShareholder: {
        humanId: 100,
        name: 'Elon Musk',
        birthDate: '1971-06-28T22:00:00.000Z',
        gender: 1
    },
    secondaryShareholder: null,
    miscellaneous: {
        value: {
            name: 'Item A',
            version: 1
        }
    }
};
```

## API

### Decorators

**@JsonObject()**   
Used to make a class serializable.

##### **Parameters**

**options**  
Type: [`JsonObjectOptions`](#jsonobjectoptions)  
Optional: `true`  
Description: The option to customize the serialization/deserialization of the target class.

##### **Example**

```typescript
@JsonObject(options)
class MyClass {}
```

**@JsonProperty()**  
Used to make a class property serializable, property will be ignored if not set.

##### **Parameters**

**options**  
Type: `string` | [`JsonPropertyOptions`](#jsonpropertyoptions)  
Optional: `true`  
Description: The option to customize the serialization/deserialization of the target property.

##### **Example**

```typescript
@JsonProperty(options) myProperty: string;
```

### JsonSerializer

#### Constructor

```typescript
constructor(options?: Partial<JsonSerializerOptions>) {}
```

##### **Parameters**

**options**  
Type: <code>Partial<[JsonSerializerOptions](#jsonserializeroptions)></code>  
Optional: `true`  
Description: The options to customize the serializer.

#### Properties

**options**  
Type: <code>Partial<[JsonSerializerOptions](#jsonserializeroptions)></code>  
Optional: `false`  
Description: The options to customize the serializer.

Default value:

```typescript
{
    errorCallback: logError,
    nullishPolicy: {
        undefined: 'remove',
        null: 'allow'
    },
    additionalPropertiesPolicy: 'remove'
}
```

#### Methods

**deserialize()**  
To use when you don't know if the value to deserialize is an object or an array.

```typescript
deserialize<T extends object>(
    value: string | object | Array<object>,
    type: Type<T> | T
): T | Array<T|Nullish> | Nullish
```

##### **Parameters**

**value**  
Type: `string` | `object` | `Array<object>`  
Optional: `false`  
Description: The value to deserialize.

**type**  
Type: [`Type<T>`](#typet) | `T`  
Optional: `false`  
Description: The constructor class to deserialize into.

##### **Return**

`T` or <code>Array<T|[Nullish](#nullish)></code> or [`Nullish`](#nullish)

---

**deserializeObject()**  
To use when the value to deserialize is an object.

```typescript
deserializeObject<T extends object>(
    obj: string | object,
    type: Type<T> | T
): T | Nullish
```

##### **Parameters**

**obj**  
Type: `string` | `object`  
Optional: `false`  
Description: The object to deserialize.

**type**  
Type: [`Type<T>`](#typet) | `T`  
Optional: `false`  
Description: The constructor class to deserialize into.

##### **Return**

`T` or [`Nullish`](#nullish)

---

**deserializeObjectArray()**  
To use when the value to deserialize is an array.

```typescript
deserializeObjectArray<T extends object>(
    array: string | Array<any>,
    type: Type<T> | T
): Array<T|Nullish> | Nullish
```

##### **Parameters**

**array**  
Type: `string` | `Array<any>`  
Optional: `false`  
Description: The object to deserialize.

**type**  
Type: [`Type<T>`](#typet) | `T`  
Optional: `false`  
Description: The constructor class to deserialize into.

##### **Return**

<code>Array<T|[Nullish](#nullish)></code> or [`Nullish`](#nullish)

---

**serialize()**  
To use when you don't know if the value to serialize is an object or an array

```typescript
serialize(value: object | Array<object>): object | Array<object|Nullish> | Nullish
```

##### **Parameters**

**value**  
Type: `object` | `Array<object>`  
Optional: `false`  
Description: The object or the array of objects to serialize.

##### **Return**

`object` or <code>Array<object|[Nullish](#nullish)></code> or [`Nullish`](#nullish)

---

**serializeObject()**  
To use when the value to serialize is an object.

```typescript
serializeObject(instance: object): object | Nullish
```

##### **Parameters**

**instance**  
Type: `object`  
Optional: `false`  
Description: The object to serialize.

##### **Return**

`object` or [`Nullish`](#nullish)

---

**serializeObjectArray()**  
To use when the value to serialize is an array of objects.

```typescript
serializeObjectArray(array: Array<object>): Array<object|Nullish> | Nullish
```

##### **Parameters**

**array**  
Type: `Array<object>`  
Optional: `false`  
Description: The array of objects to serialize.

##### **Return**

<code>Array<object|[Nullish](#nullish)></code> or [`Nullish`](#nullish)

### Definitions

#### **Types**

##### **JsonObjectOptions**

```typescript
constructorParams?: Array<unknown>;
```

##### **JsonPropertyOptions**

```typescript
name?: string | Array<string>;
type?: Function | PredicateProto;
dataStructure?: DataStructure;
required?: boolean;
beforeSerialize?: IOProto;
afterSerialize?: IOProto;
beforeDeserialize?: IOProto;
afterDeserialize?: IOProto;
```

##### **JsonSerializerOptions**

```typescript
errorCallback?: ErrorCallback = logError;
nullishPolicy: NullishPolicy = {
    undefined: 'remove',
    null: 'allow'
};
additionalPropertiesPolicy: Policy = 'remove';
formatPropertyName?: FormatPropertyNameProto;
```

##### **NullishPolicy**

```typescript
undefined: Policy;
null: Policy;
```

#### **Value types**

##### **DataStructure**

```typescript
'array' | 'dictionary' | 'map' | 'set'
```

##### **Nullish**

```typescript
null | undefined
```

##### **Policy**

```typescript
'allow' | 'disallow' | 'remove'
```

#### **Function types**

##### **ErrorCallback**

```typescript
(message: string) => void
```

The library provide two built-in methods:

- `logError` that logs the error.
- `throwError` that throws the error.

##### **FormatPropertyNameProto**

```typescript
(propertyName: string) => string;
```

##### **IOProto**

```typescript
(property: any, currentInstance?: any) => any
```

##### **PredicateProto**

```typescript
(property: any, parentProperty?: any) => any
```

##### **Type\<T>**

```typescript
new (...args: Array<any>) => T;
```

Note: represent a `constructor`.

## Development

### Prerequisites

- NodeJS: [https://nodejs.org](https://nodejs.org/en/)
- Yarn: [https://yarnpkg.com](https://yarnpkg.com/)

### Install dependencies

```sh
yarn
```

### Run build

```sh
yarn build
```

### Run linter

```sh
yarn lint
```

### Run tests

```sh
yarn test
```

## Thanks to

### Author

Gillian Pérard - [@GillianPerard](https://github.com/GillianPerard)

### Contributors

* Hyeonsoo David Lee - [@civilizeddev](https://github.com/civilizeddev)