Как инициализировать объект TypeScript с помощью объекта JSON

Я получаю объект JSON от вызова AJAX на сервер REST. Этот объект имеет имена свойств, которые соответствуют моему классу TypeScript (это продолжение этого вопроса).

Каков наилучший способ его инициализации? Я не думаю, что this будет работать, потому что класс (& JSON object) имеет членов, которые представляют собой списки объектов и членов, которые являются классами, и эти классы имеют членов, которые являются списками и/или классами.

Но я бы предпочел подход, который ищет имена участников и назначает их, создавая списки и создавая классы по мере необходимости, поэтому мне не нужно писать явный код для каждого члена в каждом классе (там LOT! )

Ответ 1

Вот несколько быстрых снимков, чтобы показать несколько разных способов. Они ни в коем случае не "полны" и как отказ от ответственности, я не думаю, что это хорошая идея сделать это так. Также код не слишком чист, так как я просто набрал его вместе довольно быстро.

Также как примечание: Конечно, десериализуемые классы должны иметь конструкторы по умолчанию, как это имеет место на всех других языках, где я знаю о десериализации любого рода. Конечно, Javascript не будет жаловаться, если вы вызываете конструктор не по умолчанию без аргументов, но класс лучше подготовиться к нему (плюс, на самом деле это не будет "способ создания скриптов" ).

Вариант №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, нам нужно иметь некоторую информацию о том, какой тип a node в объекте 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);

Ответ 2

TL;DR: TypedJSON (рабочее доказательство концепции)


Корень сложности этой проблемы заключается в том, что нам нужно десериализовать JSON во время выполнения, используя информацию о типе, которая существует только во время компиляции. Это требует, чтобы информация о типе каким-то образом была доступна во время выполнения.

К счастью, это можно решить очень элегантным и надежным способом с помощью decorators и 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);
}

Для любого заданного свойства вышеприведенный фрагмент добавит ссылку функции конструктора свойства в скрытое свойство __propertyTypes__ в прототипе класса. Например:

class Language {
    @JsonMember // String
    name: string;

    @JsonMember// Number
    level: number;
}

class Person {
    @JsonMember // String
    name: string;

    @JsonMember// Language
    language: Language;
}

И что он, у нас есть требуемая информация о типе во время выполнения, которая теперь может быть обработана.

 

Тип обработки информации

Сначала нам нужно получить экземпляр Object, используя JSON.parse - после этого мы можем выполнить итерацию по entires в __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. Если ожидается Person, то создается экземпляр Person. С некоторыми дополнительными мерами безопасности для примитивных типов и массивов этот подход можно сделать безопасным, что противостоит любому вредоносному JSON.

 

Пограничные случаи

Однако, если вы теперь счастливы, что решение настолько простое, у меня есть плохие новости: есть огромное количество крайних случаев, о которых нужно позаботиться. Только некоторые из них:

  • Массивы и элементы массива (особенно во вложенных массивах)
  • Полиморфизм
  • Абстрактные классы и интерфейсы
  • ...

Если вы не хотите возиться со всеми этими (я уверен, вы этого не сделаете), я был бы рад рекомендовать рабочую экспериментальную версию доказательной концепции, использующей этот подход, TypedJSON - который я создал для решения этой точной проблемы, проблемы, с которой я сталкиваюсь ежедневно.

Из-за того, что декораторы все еще считаются экспериментальными, я бы не рекомендовал использовать его для использования в производстве, но до сих пор он хорошо меня обслуживал.

Ответ 3

вы можете использовать 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 );

здесь HalJson

export 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

чтобы вы могли видеть, что это не рекурсивно присваивается

Ответ 4

Я использовал этого парня, чтобы выполнить эту работу: https://github.com/weichx/cerialize

Это очень простой, но мощный. Он поддерживает:

  • Сериализация и десериализация всего дерева объектов.
  • Стойкие и переходные свойства на одном и том же объекте.
  • Крючки для настройки логики сериализации (de).
  • Он может (де) сериализоваться в существующий экземпляр (отлично подходит для Angular) или генерировать новые экземпляры.
  • и др.

Пример:

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

4-й вариант, описанный выше, - это простой и приятный способ сделать это, который должен быть объединен со вторым вариантом в случае, когда вам нужно обрабатывать иерархию классов, например, список членов, который является любым из случаев подклассов суперкласса члена, например, директор продлевает член или учащийся. В этом случае вы должны указать тип подкласса в формате json

Ответ 6

Возможно, это не фактическое, а простое решение:

interface Bar{
x:number;
y?:string; 
}

var baz:Bar = JSON.parse(jsonString);
alert(baz.y);

работать и для сложных зависимостей!!!

Ответ 7

Вариант № 5: Использование конструкторов Typescript и jQuery.extend

Это, по-видимому, самый поддерживаемый метод: добавьте конструктор, который принимает в качестве параметра структуру 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 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()
}

Ответ 8

JQuery.extend делает это для вас:

var mytsobject = new mytsobject();

var newObj = {a:1,b:2};

$.extend(mytsobject, newObj); //mytsobject will now contain a & b

Ответ 9

Другая опция, использующая заводы

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);
  • поддерживает простые классы.
  • инъекция, доступная для заводов для гибкости

Ответ 10

Я создал инструмент, который генерирует интерфейсы TypeScript и карту типа времени выполнения для выполнения проверки типа времени выполнения по результатам 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 для деталей.

Ответ 11

вы можете сделать, как показано ниже

export interface Instance {
  id?:string;
  name?:string;
  type:string;
}

и

var instance: Instance = <Instance>({
      id: null,
      name: '',
      type: ''
    });