Не так давно в JavaScript появилась такая замечательная вещь как классы, которая значительно упростила процесс написания кода. Но к сожалению не появился функционал для десериализации JSON в эти самые классы, т.е. сериализовать класс в строку можно, а вот обратно уже своими силами. И вот для исправления этого недостатка и была написана библиотека ts-serializable которой я хочу поделиться с вами.
В чем суть проблемы показывает следующий код:
export class User {
public firstName: string = "Иван";
public lastName: string = "Петров";
public birthDate: Date = new Date();
public getFullName(): string {
return [this.firstName, this.lastName].join(' ');
}
public getAge(): number {
return new Date().getFullYear() - this.birthDate.getFullYear();
}
}
const ivan = new User();
ivan.getFullName(); // возвращает ФИО
ivan.getAge(); // возвращает возраст
ivan instanceof User; // является инстансом пользователя
const text = JSON.stringify(ivan); // сериализуем класс в текст
const newIvan = JSON.parse(text); // десериализуем обратно
newIvan.getFullName(); // ОшибкаТипа: метод getFullName отсутствует
newIvan.getAge(); // ОшибкаТипа: метод getAge отсутствует
newIvan instanceof User; // не является инстансом пользователя
В чем причина ошибок нового Ивана? Дело в том что метод JSON.parse десериализует не в класс User, а в класс Object у которого просто нету методов getFullName и getAge.
Как же моя библиотека помогает решить эту проблему и десериализовать в User, а не Object? Достаточно просто слегка модифицировать код:
import { jsonProperty, Serializable } from "ts-serializable";
export class User extends Serializable {
@jsonProperty(String)
public firstName: string = "Иван";
@jsonProperty(String)
public lastName: string = "Петров";
@jsonProperty(Date)
public birthDate: Date = new Date();
public getFullName(): string {
return [this.firstName, this.lastName].join(' ');
}
public getAge(): number {
return new Date().getFullYear() - this.birthDate.getFullYear();
}
}
const ivan = new User();
ivan.getFullName(); // возвращает ФИО
ivan.getAge(); // возвращает возраст
ivan instanceof User; // является инстансом пользователя
const text = JSON.stringify(ivan); // сериализуем класс в текст
const newIvan = new User().fromJson(JSON.parse(text)); // десериализуем обратно в User
newIvan.getFullName(); // возвращает ФИО
newIvan.getAge(); // возвращает возраст
newIvan instanceof User; // является инстансом пользователя
Все очень просто. Наследуем наш класс от класса Serializable у которого есть два метода fromJson для десериализации и toJSON для сериализации, а свойствам вешаем декоратор @jsonProperty с указанием тех типов данных которые разрешено принимать из JSON. Невалидные данные будут проигнорированы, в консоль выдано предупреждение, а в свойстве останется значение по умолчанию.
Вот вообщем то и все. Теперь на фронте можно десериализовать и сериализовать так же просто как это делается в C#, Java и других языках. За основу взято поведение Newtonsoft Json.NET.
FAQ
Для чего наследоваться от Serializable?
Для того что бы добавить в модель два метода fromJson и toJSON. Можно тоже самое реализовать и через декоратор или monkey patching. Но наследование является более правильным методом для Typescript.
Как происходит валидация данных
В декоратор необходимо назначить конструктор тех типов данных которые разрешено принимать из JSON. Объекты Boolean, String, Number выдадут соответственно boolean, string, number. Если вам нужно принять массив, то тип обрамляется скобочками массива, например @jsonProperty([String]). Если конструктор отнаследован от класса Serializable, то оно также будет десериализовано в класс, если нет то вернется объект.
Как отловить ошибки валидации?
По умолчанию библиотеку просто пишет предупреждения в консоль об ошибках валидации. Для переопределения этого поведения, например для бросания исключений или логирования на бекенд, необходимо переопределить метод onWrongType у модели.
Бонус 1. Глубокая копия.
const user1 = new Uesr();
const user2 = new User().fromJson(user1); // создаст глубокую копию
Бонус 2. Ленивые ViewModels.
Если вам необходимо создать модель с дополнительными данными, например для вьюхи, но которые не принимает бекенд, можно просто расширить модель новыми свойствами и пометить эти свойства декоратором @jsonIgnore. И тогда эти свойства не будут сериализованы.
import { jsonProperty, Serializable } from "ts-serializable";
export class User extends Serializable {
@jsonProperty(String)
public firstName: string = "Иван";
@jsonIgnore()
public isExpanded: boolean = false;
}
JSON.stringify(new User()); // вернет {"firstName":"Иван"}
Комментарии (18)
keenondrums
05.11.2018 20:16А чем не угодили class-transformer вместе с class-validator?
https://github.com/typestack/class-transformer
LabEG Автор
05.11.2018 21:56Моей реализации на самом деле уже почти 4 года, в текущем виде больше 2 лет. И на тот момент когда я ее создавал не было той библиотеки которая меня устраивала бы по функционалу и качеству. Да и сейчас кажется что моя проще.
DimPal
05.11.2018 20:55После конфускации с обезличиванием имен будет счастье…
Кстати, что будет с типизированными массивами и вложенными классами?LabEG Автор
05.11.2018 22:02Минификаторы не трогают свойства классов. А так да, проблема с обфускацией есть во всех языках с рефлексией.
Массивы валидируются. Массивы классов так же десериализуются. Вложенный класс если унаследован от Serializable так же десериализуется, если нет, то остается объектом. Огромные графы с массивами и классами так же десериализуются и валидируются.
VolCh
05.11.2018 21:06+2> Можно тоже самое реализовать и через декоратор или monkey patching. Но наследование является более правильным методом для Typescript.
Вот не согласен. Особенно учитывая, что множественного наследования нет. Если у меня User extends Person, а над Person контроля нет, как мне применить ваш Serializable?LabEG Автор
05.11.2018 22:07Такой кейс мне не встречался, т.к. я всегда принимаю данные в свои классы, но вопрос интересный.
Из того что пришло в голову это сделать доработку библиотеки что бы в User переопределить свойства, но уже с декораторами, а у Serializable сделать статичный метод Serializable.toClass(User, json): User
justboris
06.11.2018 01:35Смешная пирамида деклараций вот здесь: github.com/LabEG/Serializable/blob/master/src/models/AcceptedType.ts#L13
А зачем это?LabEG Автор
06.11.2018 21:33Это рекурсивный тип, аналог
для глубоко вложенных массивов типа [[[[1,2]]]]. Дело в том что typescript разрешает рекурсивные типы, но перестает валидировать их при использовании. А такая пирамида позволяет провалидировать до 10 уровня, а дальше уже через any.type AcceptedTypes = AcceptedType | Array<AcceptedTypes>;
justboris
06.11.2018 23:24А если сделать так:
interface RecursiveArray<T> extends Array<T|RecursiveArray<T>> {} type AcceptedType = ...; type AcceptedTypes = RecursiveArray<AcceptedType>;
Живое демо, валидируется нормально.
Подсмотрено в тайпингах Lodash.
kemsky
06.11.2018 03:50Тоже думал написать что-то похожее, но потом отказался от идеи, слишком много сил надо чтобы написать полноценное решение и будущее декораторов выглядело смутно. Самая неприятная проблема — работа с датами, но ее можно решить по-другому.
Сейчас есть интересная альтернатива — TypeScript Custom Transformers, что позволяет делать кодогенерацию во время компиляции, еще не все сделано, но уже можно пробовать. По идее можно будет обойтись без кучи декораторов. Еще один способ — кодогенерация по моделям бэкенда или свагеру.
LabEG Автор
06.11.2018 21:50Я тоже не сразу написал библиотеку. С начало это производилось вручную в методе объекта, потом была выявлена закономерность, а в тайпскрипте появились декораторы и рефлексия. И как только появилось свободное время реализовал библиотеку.
Со временем работать просто. Если нужно исключить влияние таймзоны надо оставить его строкой, если нет, то десериализовать в локальное время.
TypeScript Custom Transformers — спасибо за наводку. Кажется эта штука избавит нас от browserify/webpack и позволит компилировать в ES2015.
Про кодогенерацию моделей и репозиториев с валидацией из свагера тоже есть идея, надеюсь скоро удастся реализовать.
metalalisa
06.11.2018 12:27Если не хочется писать классы, есть github.com/gcanti/io-ts
LabEG Автор
06.11.2018 21:59Мне не нравится подход со схемами, кроме того в моделях у меня бывают методы.
boblgum
06.11.2018 22:00За саму реализацию автору однозначно плюс в карму.
Но я советую как можно быстрее уходить от такого подхода. Вы двигаетесь в сторону ActiveRecord. В данном случае вы впиливаете функционал сохранения данных в Вашу модель. Этот функционал ни каким образом не относится к смыслу модели.
Вынесите этот функционал в сервис и получите более чистый и распределенный код.LabEG Автор
06.11.2018 22:08А вы правы, в комментарии выше уже всплывала эта проблема. И в Newtonsoft десериализация тоже реализована через статический метод. Реализую как Serializable.toClass(User, json): User
keenondrums
А чем не угодили class-transformer вместе с class-validator?
https://github.com/typestack/class-transformer