Как инициализировать объект TypeScript с помощью объекта JSON
Я получаю объект JSON из AJAX-вызов к серверу и остальные. Этот объект имеет имена свойств, соответствующие моему классу TypeScript (это продолжение этот вопрос).
Что является лучшим способом, чтобы инициализировать его? Я не думаю этой будет работать, потому что класс (& JSON object) имеет члены, которые являются списками объектов и членов, которые являются классами, и эти классы имеют члены, которые являются списками и/или классами.
но я бы предпочел подход, который ищет имена членов и назначает их поперек, создавая списки и создавая экземпляры классов по мере необходимости, поэтому мне не нужно писать явный код для каждого члена в каждом классе (есть много!)
12 ответов:
это несколько быстрых снимков, чтобы показать несколько разных способов. Они ни в коем случае "полной" и как отказ, я не думаю, что это хорошая идея, чтобы сделать так. Также код не слишком чистый, так как я просто набрал его вместе довольно быстро.
также как Примечание: конечно, десериализуемые классы должны иметь конструкторы по умолчанию, как и во всех других языках, где я знаю о десериализации любого рода. Конечно, Javascript не будет жаловаться, если вы позвоните конструктор не по умолчанию без аргументов, но класс лучше быть готовым к нему тогда (плюс, это не было бы действительно "typescripty way").
Вариант №1: никакой информации о времени выполнения вообще
проблема с этим подходом заключается в основном в том, что имя члена должны соответствовать своему классу. Что автоматически ограничивает вас одним членом одного типа на класс и нарушает несколько правил хорошей практики. Я настоятельно советую против этого, но просто перечислить его здесь, потому что это был первый "черновик", когда я написал этот ответ (именно поэтому имена" Foo " и т. д.).
module Environment { export class Sub { id: number; } export class Foo { baz: number; Sub: Sub; } } function deserialize(json, environment, clazz) { var instance = new clazz(); for(var prop in json) { if(!json.hasOwnProperty(prop)) { continue; } if(typeof json[prop] === 'object') { instance[prop] = deserialize(json[prop], environment, environment[prop]); } else { instance[prop] = json[prop]; } } return instance; } var json = { baz: 42, Sub: { id: 1337 } }; var instance = deserialize(json, Environment, Environment.Foo); console.log(instance);Вариант №2:имя свойства
чтобы избавиться от проблемы в варианте № 1, Нам нужно иметь какую-то информацию о том, какого типа узел в объекте JSON. Проблема в том, что в Typescript эти вещи являются конструкциями времени компиляции, и мы нуждаемся в них во время выполнения, но объекты времени выполнения просто не знают об их свойствах, пока они не будут набор.
один из способов сделать это-сделать классы осведомленными об их именах. Однако вам также нужно это свойство в JSON. Вообще-то, ты только нужно это в json:
module Environment { export class Member { private __name__ = "Member"; id: number; } export class ExampleClass { private __name__ = "ExampleClass"; mainId: number; firstMember: Member; secondMember: Member; } } function deserialize(json, environment) { var instance = new environment[json.__name__](); for(var prop in json) { if(!json.hasOwnProperty(prop)) { continue; } if(typeof json[prop] === 'object') { instance[prop] = deserialize(json[prop], environment); } else { instance[prop] = json[prop]; } } return instance; } var json = { __name__: "ExampleClass", mainId: 42, firstMember: { __name__: "Member", id: 1337 }, secondMember: { __name__: "Member", id: -1 } }; var instance = deserialize(json, Environment); console.log(instance);Вариант #3: явное указание типов членов
как указано выше, информация о типе членов класса недоступна во время выполнения-то есть, если мы не сделаем ее доступной. Нам нужно только сделать это для непримитивных членов, и мы хорошо вперед:
interface Deserializable { getTypes(): Object; } class Member implements Deserializable { id: number; getTypes() { // since the only member, id, is primitive, we don't need to // return anything here return {}; } } class ExampleClass implements Deserializable { mainId: number; firstMember: Member; secondMember: Member; getTypes() { return { // this is the duplication so that we have // run-time type information :/ firstMember: Member, secondMember: Member }; } } function deserialize(json, clazz) { var instance = new clazz(), types = instance.getTypes(); for(var prop in json) { if(!json.hasOwnProperty(prop)) { continue; } if(typeof json[prop] === 'object') { instance[prop] = deserialize(json[prop], types[prop]); } else { instance[prop] = json[prop]; } } return instance; } var json = { mainId: 42, firstMember: { id: 1337 }, secondMember: { id: -1 } }; var instance = deserialize(json, ExampleClass); console.log(instance);Вариант №4: многословный, но аккуратный способ
01/03/2016 обновления: как указал @GameAlchemist в комментариях, начиная с Typescript 1.7, решение, описанное ниже, может быть написано лучше с помощью декораторов классов/свойств.
сериализация-это всегда проблема, и на мой взгляд, лучший способ-это способ, который просто не самый короткий. Из всех вариантов, это то, что я бы предпочел, потому что автор класса имеет полный контроль состояния десериализованных объектов. Если бы мне пришлось угадать, я бы сказал, что все остальные варианты рано или поздно приведут вас к неприятностям (если Javascript не придумает собственный способ борьбы с этим).
действительно, следующий пример не гибкости правосудия. Он действительно просто копирует структуру класса. Разница, которую вы должны иметь в виду здесь, заключается в том, что класс имеет полный контроль над использованием любого типа JSON, который он хочет контролировать состояние всего класс (можно вычислить вещей и т. д.).
interface Serializable<T> { deserialize(input: Object): T; } class Member implements Serializable<Member> { id: number; deserialize(input) { this.id = input.id; return this; } } class ExampleClass implements Serializable<ExampleClass> { mainId: number; firstMember: Member; secondMember: Member; deserialize(input) { this.mainId = input.mainId; this.firstMember = new Member().deserialize(input.firstMember); this.secondMember = new Member().deserialize(input.secondMember); return this; } } var json = { mainId: 42, firstMember: { id: 1337 }, secondMember: { id: -1 } }; var instance = new ExampleClass().deserialize(json); console.log(instance);
TLDR: TypedJSON (рабочее доказательство концепции)
корень сложности этой проблемы заключается в том, что нам нужно десериализовать JSON в runtime используя информацию о типе, которая существует только в время компиляции. Для этого требуется, чтобы информация о типе каким-то образом была доступна во время выполнения.
к счастью, это может быть решена в очень элегантный и надежный способ с декораторы и ReflectDecorators:
- использовать декораторы собственность на свойства, которые подлежат сериализации, записывать метаданные и хранить эту информацию где-то, например на прототипе класса
- кормить эту информацию метаданных для рекурсивной инициализации (десериализатор)
Тип Записи-Информация
С комбинацией ReflectDecorators и декораторы свойств, информация о типе может быть легко записана о свойстве. Элементарной реализацией этого подхода было бы:
function JsonMember(target: any, propertyKey: string) { var metadataFieldKey = "__propertyTypes__"; // Get the already recorded type-information from target, or create // empty object if this is the first property. var propertyTypes = target[metadataFieldKey] || (target[metadataFieldKey] = {}); // Get the constructor reference of the current property. // This is provided by TypeScript, built-in (make sure to enable emit // decorator metadata). propertyTypes[propertyKey] = Reflect.getMetadata("design:type", target, propertyKey); }для любого заданного свойства приведенный выше фрагмент добавит ссылку на функцию конструктора свойства в hidden
__propertyTypes__свойства в прототипе класса. Например:class Language { @JsonMember // String name: string; @JsonMember// Number level: number; } class Person { @JsonMember // String name: string; @JsonMember// Language language: Language; }и это все, у нас есть необходимая информация типа во время выполнения, которая теперь может быть обработанный.
Тип-Обработки Информации
сначала нам нужно получить
Objectэкземпляр с помощьюJSON.parse-- после этого мы можем перебирать все в__propertyTypes__(собранный выше) и создать экземпляр необходимых свойств соответственно. Тип корневого объекта должен быть указан, чтобы десериализатор имел начальную точку.опять же, мертвая простая реализация этого подхода будет быть:
function deserialize<T>(jsonObject: any, Constructor: { new (): T }): T { if (!Constructor || !Constructor.prototype.__propertyTypes__ || !jsonObject || typeof jsonObject !== "object") { // No root-type with usable type-information is available. return jsonObject; } // Create an instance of root-type. var instance: any = new Constructor(); // For each property marked with @JsonMember, do... Object.keys(Constructor.prototype.__propertyTypes__).forEach(propertyKey => { var PropertyType = Constructor.prototype.__propertyTypes__[propertyKey]; // Deserialize recursively, treat property type as root-type. instance[propertyKey] = deserialize(jsonObject[propertyKey], PropertyType); }); return instance; }var json = '{ "name": "John Doe", "language": { "name": "en", "level": 5 } }'; var person: Person = deserialize(JSON.parse(json), Person);вышеуказанная идея имеет большое преимущество десериализации по ожидается типы (для сложных/объектных значений), а не то, что присутствует в JSON. Если a , то это
Personэкземпляр, созданный. С некоторыми дополнительными мерами безопасности для примитивных типов и массивов этот подход может быть защищен, что сопротивляется любой вредоносный JSON.края Дела
однако, если вы теперь счастливы, что решение это просто, у меня есть некоторые плохие новости: есть vast количество крайних случаев, о которых необходимо позаботиться. Только некоторые из них:
- массивы и элементы массива (особенно во вложенных массивах)
- полиморфизм
- абстрактные классы и интерфейсы
- ...
если вы не хотите возиться со всеми из них (я уверен, что вы этого не сделаете), я был бы рад порекомендовать рабочую экспериментальную версию доказательства концепции, использующую этот подход, TypedJSON -- который я создал, чтобы решить эту точную проблему, проблему, с которой я сталкиваюсь ежедневно.
из-за того, как декораторы все еще считаются экспериментальными, я бы не рекомендовал использовать его для производственного использования, но до сих пор он служил мне хорошо.
можно использовать
Object.assignЯ не знаю, когда это было добавлено, в настоящее время я использую Typescript 2.0.2, и это, похоже, функция ES6.client.fetch( '' ).then( response => { return response.json(); } ).then( json => { let hal : HalJson = Object.assign( new HalJson(), json ); log.debug( "json", hal );здесь
HalJsonexport class HalJson { _links: HalLinks; } export class HalLinks implements Links { } export interface Links { readonly [text: string]: Link; } export interface Link { readonly href: URL; }вот что хром говорит, что это
HalJson {_links: Object} _links : Object public : Object href : "http://localhost:9000/v0/publicтак что вы можете видеть, что он не делает назначение рекурсивно
я использовал этого парня, чтобы сделать работу: https://github.com/weichx/cerialize
это очень просто, но мощный. Он поддерживает:
- сериализация и десериализация всего дерева объектов.
- постоянные и переходные свойства одного и того же объекта.
- крючки для настройки логики сериализации (de).
- он может (de)сериализовать в существующий экземпляр (отлично подходит для углового) или создать новый экземпляры.
- etc.
пример:
class Tree { @deserialize public species : string; @deserializeAs(Leaf) public leafs : Array<Leaf>; //arrays do not need extra specifications, just a type. @deserializeAs(Bark, 'barkType') public bark : Bark; //using custom type and custom key name @deserializeIndexable(Leaf) public leafMap : {[idx : string] : Leaf}; //use an object as a map } class Leaf { @deserialize public color : string; @deserialize public blooming : boolean; @deserializeAs(Date) public bloomedAt : Date; } class Bark { @deserialize roughness : number; } var json = { species: 'Oak', barkType: { roughness: 1 }, leafs: [ {color: 'red', blooming: false, bloomedAt: 'Mon Dec 07 2015 11:48:20 GMT-0500 (EST)' } ], leafMap: { type1: { some leaf data }, type2: { some leaf data } } } var tree: Tree = Deserialize(json, Tree);
Вариант #5: Использование конструкторов Typescript и jQuery.расширить
это, кажется, самый поддерживаемый Метод: Добавьте конструктор, который принимает в качестве параметра структуру json и расширяет объект json. Таким образом, вы можете проанализировать структуру json во всей модели приложения.
нет необходимости создавать интерфейсы или перечислять свойства в конструкторе.
export class Company { Employees : Employee[]; constructor( jsonData: any ) { jQuery.extend( this, jsonData); // apply the same principle to linked objects: if ( jsonData.Employees ) this.Employees = jQuery.map( jsonData.Employees , (emp) => { return new Employee ( emp ); }); } calculateSalaries() : void { .... } } export class Employee { name: string; salary: number; city: string; constructor( jsonData: any ) { jQuery.extend( this, jsonData); // case where your object's property does not match the json's: this.city = jsonData.town; } }в вашем ajax обратного вызова, где вы получаете компанию для расчета зарплаты:
onReceiveCompany( jsonCompany : any ) { let newCompany = new Company( jsonCompany ); // call the methods on your newCompany object ... newCompany.calculateSalaries() }
4-й вариант, описанный выше, является простым и приятным способом сделать это, который должен быть объединен со 2-м вариантом в случае, когда вам нужно обрабатывать иерархию классов, например, список членов, который является любым из вхождений подклассов суперкласса-члена, например, директор расширяет член или студент расширяет член. В этом случае вы должны дать тип подкласса в формате json
Я создал инструмент, который генерирует интерфейсы TypeScript и runtime "Type map" для выполнения проверки типов во время выполнения по результатам
JSON.parse: ts.quicktype.ioнапример, учитывая этот JSON:
{ "name": "David", "pets": [ { "name": "Smoochie", "species": "rhino" } ] }quicktype создает следующий интерфейс TypeScript и тип карты:
export interface Person { name: string; pets: Pet[]; } export interface Pet { name: string; species: string; } const typeMap: any = { Person: { name: "string", pets: array(object("Pet")), }, Pet: { name: "string", species: "string", }, };затем мы проверяем результат
JSON.parseна карте типа:export function fromJson(json: string): Person { return cast(JSON.parse(json), object("Person")); }я пропустил какой-то код, но вы можете попробовать quicktype за подробностями.
может быть не актуален, но простое решение:
interface Bar{ x:number; y?:string; } var baz:Bar = JSON.parse(jsonString); alert(baz.y);Работа за сложных зависимостей!!!
JQuery .extend делает это для вас:
var mytsobject = new mytsobject(); var newObj = {a:1,b:2}; $.extend(mytsobject, newObj); //mytsobject will now contain a & b
другой вариант с использованием фабрик
export class A { id: number; date: Date; bId: number; readonly b: B; } export class B { id: number; } export class AFactory { constructor( private readonly createB: BFactory ) { } create(data: any): A { const createB = this.createB.create; return Object.assign(new A(), data, { get b(): B { return createB({ id: data.bId }); }, date: new Date(data.date) }); } } export class BFactory { create(data: any): B { return Object.assign(new B(), data); } }https://github.com/MrAntix/ts-deserialize
использовать такой
import { A, B, AFactory, BFactory } from "./deserialize"; // create a factory, simplified by DI const aFactory = new AFactory(new BFactory()); // get an anon js object like you'd get from the http call const data = { bId: 1, date: '2017-1-1' }; // create a real model from the anon js object const a = aFactory.create(data); // confirm instances e.g. dates are Dates console.log('a.date is instanceof Date', a.date instanceof Date); console.log('a.b is instanceof B', a.b instanceof B);
- сохраняет ваши классы простыми
- впрыска доступная к фабрикам для гибкости
вы можете сделать как ниже
export interface Instance { id?:string; name?:string; type:string; }и
var instance: Instance = <Instance>({ id: null, name: '', type: '' });
**model.ts** export class Item { private key: JSON; constructor(jsonItem: any) { this.key = jsonItem; } } **service.ts** import { Item } from '../model/items'; export class ItemService { items: Item; constructor() { this.items = new Item({ 'logo': 'Logo', 'home': 'Home', 'about': 'About', 'contact': 'Contact', }); } getItems(): Item { return this.items; } }
Comments