typescript-object-serializer
Typescript library to convert javascript object to typescript class and vice versa
CHANGELOG
Useful snippets
Migration Guide
Installation and configuration
> npm install typescript-object-serializer
Required configure tsconfig.json:
{
"compilerOptions": {
"experimentalDecorators": true
}
}
And it is ready to use!
If necessary enable auto-detection types of serializable properties: - required additional configuration:
- Install
reflect-metadata dependency:
> npm install reflect-metadata
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
- Import
reflect-metadata in polyfills.ts or in top of index.ts file
import 'reflect-metadata';
Usage
Basic usage
import {
deserialize,
serialize,
property,
SnakeCaseExtractor,
} from 'typescript-object-serializer';
class Person {
@property()
public declare name: string;
@property(SnakeCaseExtractor)
public declare lastName: string;
}
const person = deserialize(Person, {
name: 'John',
last_name: 'Doe',
});
console.log(person instanceof Person);
console.log(person.name);
console.log(person.lastName);
console.log(serialize(person))
Deep serializable property
import {
deserialize,
property,
SnakeCaseExtractor,
} from 'typescript-object-serializer';
class Person {
@property()
public declare name: string;
@property(SnakeCaseExtractor)
public declare lastName: string;
}
class Employee {
@property()
public declare id: number;
@property()
@propertyType(Person)
public declare person: Person;
}
const employee = deserialize(Employee, {
id: 1,
person: {
name: 'John',
last_name: 'Doe',
},
});
console.log(employee.person);
Extend serializable class
import {
deserialize,
property,
} from 'typescript-object-serializer';
class Person {
@property()
public declare name: string;
}
class Employee extends Person {
@property()
public declare id: number;
}
const employee = deserialize(Employee, {
id: 1,
name: 'John',
});
console.log(employee);
Auto-detect property types
import {
deserialize,
property,
} from 'typescript-object-serializer';
class Person {
@property()
public declare name: string;
}
class Employee {
@property()
public declare id: number;
@property()
public declare person: Person;
}
const employee = deserialize(Employee, {
id: 1,
person: {
name: 'John',
},
});
console.log(employee);
Handle arrays of data
import {
deserialize,
property,
SnakeCaseExtractor,
propertyType,
} from 'typescript-object-serializer';
class Person {
@property()
public declare name: string;
@property(SnakeCaseExtractor)
public declare lastName: string;
}
class Employee {
@property()
public declare id: number;
@property()
@propertyType(Person)
public declare person: Person;
}
class Department {
@property()
public declare title: string;
@property()
@propertyType(Employee)
public declare employees: Employee[];
}
const employees = [
{
id: 1,
person: {
name: 'John',
last_name: 'Doe',
},
},
{
id: 2,
person: {
name: 'Jane',
last_name: 'Doe',
},
},
].map(e => deserialize(Employee, e));
console.log(employees.length);
console.log(employees[0]);
const department = deserialize(Department, {
title: 'Department title',
employees: [
{
id: 1,
person: {
name: 'John',
last_name: 'Doe',
},
},
{
id: 2,
person: {
name: 'Jane',
last_name: 'Doe',
},
},
],
});
console.dir(department, { depth: 3 });
console.dir(serialize(department), { depth: 3 });
Extracts property with same name
import {
deserialize,
property,
StraightExtractor,
} from 'typescript-object-serializer';
class Person {
@property()
public declare name: string;
@property(StraightExtractor)
public declare lastName: string;
}
const person = deserialize(Person, {
name: 'John',
lastName: 'Doe',
});
console.log(person);
Extracts property by name transformed from camelCase to snake_case
import {
deserialize,
property,
SnakeCaseExtractor,
} from 'typescript-object-serializer';
class Person {
@property()
public declare name: string;
@property(SnakeCaseExtractor)
public declare lastName: string;
}
const person = deserialize(Person, {
name: 'John',
last_name: 'Doe',
});
console.log(person);
Extracts property by name passed to use static method
import {
deserialize,
property,
OverrideNameExtractor,
} from 'typescript-object-serializer';
class Department {
@property(OverrideNameExtractor.use('department_id'))
public declare id: string;
}
const department = deserialize(Department, {
department_id: '123',
});
console.log(department);
Property type
Declares type for property. Required if not possible to detect type from property declaration (for example array of data)
Property type basic
import {
property,
propertyType,
} from 'typescript-object-serializer';
class Person {
@property()
public declare name: string;
@property(SnakeCaseExtractor)
public declare lastName: string;
}
class Employee {
@property()
declare id: number;
@property()
@propertyType(Person)
public declare person: Person;
}
class Department {
@property()
@propertyType(Employee)
public declare employees: Employee[];
}
Conditional property type
import {
deserialize,
property,
propertyType,
} from 'typescript-object-serializer';
class SuccessResult {
@property()
public declare data: Record<string, unknown>;
}
class FailedResult {
@property()
public declare error: string;
}
class UnmatchedResult {
}
class Results {
@property()
@propertyType(SuccessResult, (value: any) => value?.state === 'SUCCESS')
@propertyType(FailedResult, (value: any) => value?.state === 'FAIL')
@propertyType(UnmatchedResult)
public declare results: Array<SuccessResult | FailedResult | UnmatchedResult>;
}
const results = deserialize(Results, {
results: [
{
state: 'SUCCESS',
data: {
some_data: 'data',
},
},
{
state: 'UNKNOWN',
},
{
state: 'FAIL',
error: 'result error',
},
],
});
console.log(results.results[0]);
console.log(results.results[1]);
console.log(results.results[2]);
class ResultsWithStrictTypeCheck {
@property()
@propertyType(SuccessResult, (value: unknown) => typeof value === 'object'
&& value !== null
&& 'state' in value
&& value.state === 'SUCCESS',
)
@propertyType(FailedResult, (value: unknown) => typeof value === 'object'
&& value !== null
&& 'state' in value
&& value.state === 'FAIL',
)
@propertyType(UnmatchedResult)
public declare results: Array<SuccessResult | FailedResult | UnmatchedResult>;
}
Create serializable object
import {
create,
property,
} from 'typescript-object-serializer';
class Person {
@property()
public declare lastName: string;
@property()
public declare firstName: string;
}
const person = create(Person, {
firstName: 'John',
lastName: 'Doe',
});
console.log(person);
const partialPerson = createPartial(Person);
console.log(partialPerson);
Clone serializable object
import {
create,
clone,
property,
} from 'typescript-object-serializer';
class Person {
@property()
public declare lastName: string;
@property()
public declare firstName: string;
}
const person = create(Person, {
firstName: 'John',
lastName: 'Doe',
});
const personClone = clone(person);
console.log(personClone);
console.log(person === personClone);
Serialize serializable object
Serialize object and all nested serializable objects to simple javascript object
import {
create,
serialize,
property,
SnakeCaseExtractor,
} from 'typescript-object-serializer';
class Person {
@property(SnakeCaseExtractor)
public declare lastName: string;
@property(SnakeCaseExtractor)
public declare firstName: string;
}
const person = create(Person, {
firstName: 'John',
lastName: 'Doe',
});
console.log(serialize(person));
Modify property value
In case
- Property value has type mismatch (
string or null when expected number)
import {
deserialize,
modifier,
Modifier,
property,
serialize,
} from 'typescript-object-serializer';
class StringAgeModifier extends Modifier {
public override onSerialize(data: number): string {
return String(data);
}
public override onDeserialize(data: string): number {
return Number(data);
}
}
class Person {
@property()
@modifier(StringAgeModifier)
public declare age: number;
}
const person = deserialize(Person, {
age: '25',
});
console.log(person);
console.log(typeof person.age);
console.log(serialize(person));
- Modify property value format
import {
serialize,
deserialize,
property,
StraightExtractor,
} from 'typescript-object-serializer';
class BirthDateModifier extends Modifier {
public override onSerialize(value: string): string {
return new Date(value).toISOString();
}
}
class Person {
@property()
@modifier(BirthDateModifier)
public declare birthDate: string;
}
const person = create(Person, {
birthDate: '2000-05-06',
});
console.log(person);
console.log(serialize(person));
- Modification also handful to serialize non-json values like Date, or custom non-serializable classes
Advanced usage
It is possible to develop your own extractor according to your needs
Example 1: PrivateSnakeCaseExtractor. Extracts snake_case property to camelCase property with leading _
import {
deserialize,
property,
SnakeCaseExtractor,
} from 'typescript-object-serializer';
class PrivateSnakeCaseExtractor extends SnakeCaseExtractor {
constructor(
key: string,
modifier?: Modifier,
) {
super(
key.replace(/^_/, ''),
modifier,
);
}
}
class Department {
@property(PrivateSnakeCaseExtractor)
private declare _departmentId: string;
}
const department = deserialize(Department, {
department_id: '123',
});
console.log(department);
Example 2: DeepExtractor. Extracts value from deep object
import {
deserialize,
serialize,
property,
Extractor,
ExtractionResult,
} from 'typescript-object-serializer';
class DeepExtractor extends Extractor {
public static byPath<C extends typeof DeepExtractor>(path: string): C {
return class extends DeepExtractor {
constructor(_: string, modifier?: Modifier) {
super(path, modifier);
}
} as any;
}
private static getObjectByPath(dataObject: any, keys: string[]): any {
let extracted: any = dataObject;
keys.forEach(key => {
if (!extracted) {
return undefined;
}
extracted = (extracted as any)[key];
});
return extracted;
}
private static getOrCreateObjectByPath(dataObject: any, keys: string[]): any {
let currentObject = dataObject;
keys.forEach(key => {
if (!currentObject.hasOwnProperty(key)) {
currentObject[key] = {};
}
currentObject = currentObject[key];
});
return currentObject;
}
constructor(
key: string,
modifier?: Modifier,
) {
super(key, modifier);
}
public extract(data: any): ExtractionResult {
if (typeof data !== 'object' || data === null) {
return {
data: undefined,
path: this.key,
};
}
return {
data: this.modifier.onDeserialize(
DeepExtractor.getObjectByPath(data, this.key.split('.')),
),
path: this.key,
};
}
public apply(applyObject: any, value: unknown): void {
const keys = this.key.split('.');
const dataObject = DeepExtractor.getOrCreateObjectByPath(applyObject, keys.slice(0, -1));
dataObject[keys[keys.length - 1]] = this.modifier.onSerialize(value);
}
}
class AgeModifier extends Modifier {
public override onDeserialize(value: string): number {
return Number(value);
}
public override onSerialize(value: number): string {
return String(value);
}
}
class Person {
@property()
public declare id: number;
@property(DeepExtractor.byPath('data.person.age'))
@modifier(AgeModifier)
public declare age: number;
@property(DeepExtractor.byPath('data.person.last_name'))
public declare lastName: string;
@property(DeepExtractor.byPath('data.person.first_name'))
public declare firstName: string;
}
const person = deserialize(Person, {
id: 123,
data: {
person: {
age: '25',
last_name: 'John',
first_name: 'Doe',
},
},
});
console.log(person);
console.log(serialize(person));
Only deserializable property
Example 1: Using custom extractor
import {
deserialize,
serialize,
property,
StraightExtractor,
} from 'typescript-object-serializer';
class OnlyDeserializeStraightExtractor extends StraightExtractor {
public apply(applyObject: any, value: unknown): void {
}
}
class Department {
@property(OnlyDeserializeStraightExtractor)
public declare id: number;
@property()
public declare title: string;
}
const department = deserialize(Department, {
id: 123,
title: 'Department title',
});
console.log(department);
console.log(serialize(department));
Example 2: Using modificator (Recommended)
import {
deserialize,
modifier,
Modifier,
serialize,
property,
} from 'typescript-object-serializer';
class OnlyDeserializableModifier extends Modifier {
public override onSerialize(value: unknown): undefined {
return undefined;
}
public override onDeserialize(data: unknown): unknown {
return data;
}
}
class Department {
@property()
@modifier(OnlyDeserializableModifier)
public declare id: number;
@property()
public declare title: string;
}
const department = deserialize(Department, {
id: 123,
title: 'Department title',
});
console.log(department);
console.log(serialize(department));
Getters and setters
It is possible to serialize getter and deserialize setter property
import {
deserialize,
serialize,
property,
} from 'typescript-object-serializer';
class PersonWithGetter {
constructor(
public firstName: string,
public lastName: string,
) {
}
@property()
public get fullName(): string {
return this.firstName + ' ' + this.lastName;
}
}
const personWithGetter = new PersonWithGetter('John', 'Doe');
console.log(serialize(personWithGetter));
class PersonWithSetter {
public declare firstName: string;
public declare lastName: string;
@property()
public set fullName(value: string) {
const [firstName, lastName] = value.split(' ');
this.firstName = firstName;
this.lastName = lastName;
}
}
const deserialized = deserialize(PersonWithSetter, {
fullName: 'John Doe',
});
console.log(deserialized);
Syntactic sugar: SerializableObject
Class SerializebleObject for easy access to serializer methods like serialize, deserialize, create, clone, deserializeArray. it makes possible to import 'typescript-object-serializer' only at class declaration file but not to import it where serialization/deserialization used.
import {
SerializableObject,
property,
} from 'typescript-object-serializer';
class Item extends SerializableObject {
@property()
public id: number;
@property()
public title: string;
}
const items = Item.deserializeArray([
{
id: 1,
title: 'First item',
},
{
id: 2,
title: 'Second item',
},
]);
console.log(items);
const firstItem = items[0];
const firstItemClone = firstItem.clone();
console.log(firstItemClone);
console.log(firstItemClone === firstItem);
console.log(firstItemClone.serialize());
const newItem = Item.create({
id: 3,
title: 'New item',
});
console.log(newItem);
Data validation
It is possible to validate data before deserialization. Method validate from module typescript-object-serializer/validators returns Array of validation errors. It returns empty array if data is valid.
Basic validation
import { property } from 'typescript-object-serializer';
import {
propertyValidators,
RequiredValidator,
StringLengthValidator,
validate,
} from 'typescript-object-serializer/validators';
class Person {
@property()
@propertyValidators([RequiredValidator, StringLengthValidator.with({ min: 1 })])
public name: string;
}
const resultRequired = validate(Person, {});
console.log(resultRequired);
const resultEmpty = validate(Person, {
name: '',
});
console.log(resultEmpty);
Deep validation
import {
property,
propertyType,
SnakeCaseExtractor,
} from 'typescript-object-serializer';
import {
propertyValidators,
RequiredValidator,
validate,
} from 'typescript-object-serializer/validators';
class Address {
@property()
@propertyValidators([RequiredValidator])
public declare city: string;
}
class Employee {
@property()
@propertyValidators([RequiredValidator])
public declare name: string;
@property()
@propertyValidators([RequiredValidator])
@propertyType(Address)
public declare address: Address;
}
class Department {
@property(OverrideNameExtractor.use('department_employees'))
@propertyType(Employee)
public declare employees: Employee[];
}
class Organization {
@property()
@propertyType(Department)
public declare departments: Department[];
}
const data = {
departments: [
{
department_employees: [
{
name: 'John Doe',
address: {
city: 'New York',
},
},
{
address: {
city: 'London',
},
},
],
},
{
department_employees: [
{
name: 'Jane Doe',
address: {
},
},
{
name: 'Jane Smith',
address: {
city: 'Berlin',
},
},
],
},
],
};
const validationResult = validate(Organization, data);
console.log(validationResult);
Custom validator
import { property } from 'typescript-object-serializer';
import {
propertyValidators,
validate,
ValidationError,
Validator,
} from 'typescript-object-serializer/validators';
class VINValidator extends Validator {
public validate(value: unknown, path: string): ValidationError | undefined {
if (typeof value !== 'string') {
return;
}
if (!/^[A-HJ-NPR-Z0-9]{17}$/i.test(value)) {
return new ValidationError('Invalid VIN', path);
}
}
}
class Vehicle {
@property()
@propertyValidators([VINValidator])
public declare vin: string;
}
const validationResult = validate(Vehicle, { vin: '345435' });
console.log(validationResult);
Custom validation error
It is possible to implement own validation errors at custom validators. It allows to
- Add some logic on validation result: filter critical and non-critical errors
- Add error class with predefined message (no need to write error message at
validate() method)
import { property } from 'typescript-object-serializer';
import {
propertyValidators,
validate,
ValidationError,
Validator,
} from 'typescript-object-serializer/validators';
class PasswordCriticalValidationError extends ValidationError {
constructor(
path: string,
) {
super('Password too short', path);
}
}
class PasswordWarnValidationError extends ValidationError {
constructor(
path: string,
) {
super('Password is weak', path);
}
}
class PasswordValidator extends Validator {
public validate(value: unknown, path: string): ValidationError | undefined {
if (typeof value !== 'string') {
return;
}
if (value.length < 4) {
return new PasswordCriticalValidationError(path);
}
if (value.length < 6) {
return new PasswordWarnValidationError(path);
}
}
}
class LoginCredentials {
@property()
@propertyValidators([PasswordValidator])
public declare password: string;
}
const shortPasswordResult = validate(LoginCredentials, { password: '123' });
console.log(shortPasswordResult);
const weakPasswordResult = validate(LoginCredentials, { password: '12345' });
console.log(weakPasswordResult);
const criticalErrors = weakPasswordResult.filter(error => !(error instanceof PasswordWarnValidationError));
console.log(criticalErrors);