Cómo inicializo un objeto TypeScript con un objeto JSON


Recibo un objeto JSON de una llamada AJAX a un servidor REST. Este objeto tiene nombres de propiedad que coinciden con mi clase TypeScript (esto es una continuación de esta pregunta).

¿Cuál es la mejor manera de inicializarlo? No creo que esto funcione porque la clase (objeto& JSON) tiene miembros que son listas de objetos y miembros que son clases, y esas clases tienen miembros que son listas y/o clases.

Pero preferiría un enfoque que busque al miembro los nombra y los asigna, creando listas e instanciando clases según sea necesario, por lo que no tengo que escribir código explícito para cada miembro en cada clase (¡hay mucho!)

Author: wonea, 2014-04-05

12 answers

Estas son algunas tomas rápidas para mostrar algunas maneras diferentes. No son de ninguna manera "completos" y como descargo de responsabilidad, no creo que sea una buena idea hacerlo así. También el código no es demasiado limpio ya que acabo de escribir juntos bastante rápido.

También como nota: Por supuesto las clases deserializables necesitan tener constructores por defecto como es el caso en todos los otros lenguajes donde estoy consciente de deserialización de cualquier tipo. Por supuesto, Javascript no se quejará si llama a un no predeterminado constructor sin argumentos, pero es mejor que la clase esté preparada para ello (además, no sería realmente la "forma typescripty").

Opción # 1: No hay información de tiempo de ejecución en absoluto

El problema con este enfoque es principalmente que el nombre de cualquier miembro debe coincidir con su clase. Lo que automáticamente le limita a un miembro del mismo tipo por clase y rompe varias reglas de buenas prácticas. Aconsejo fuertemente en contra de esto, pero solo liste aquí porque fue el primer "borrador" cuando escribí esta respuesta (que es también por qué los nombres son "Foo" etc.).

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);

Opción # 2: El nombre propiedad

Para deshacerse del problema en la opción #1, necesitamos tener algún tipo de información de qué tipo es un nodo en el objeto JSON. El problema es que en Typescript, estas cosas son construcciones en tiempo de compilación y las necesitamos en tiempo de ejecución, pero los objetos en tiempo de ejecución simplemente no tienen conocimiento de sus propiedades hasta que se establecen.

Una forma de hacerlo es haciendo que las clases sean conscientes de sus nombres. Pero también necesitas esta propiedad en el JSON. En realidad, solo lo necesitas en el 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);

Opción # 3: Indicando explícitamente los tipos de miembros

Como se indicó anteriormente, la información de tipo de los miembros de la clase no está disponible en tiempo de ejecución, es decir, a menos que la hagamos disponible. Solo necesitamos hacer esto para los miembros no primitivos y estamos listos para ir:

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);

Opción # 4: La forma detallada, pero ordenada

Actualización 01/03/2016: As @GameAlchemist señaló en los comentarios, a partir de Typescript 1.7, la solución descrita a continuación se puede escribir de una mejor manera utilizando decoradores de clase / propiedad.

La serialización siempre es un problema y en mi opinión, la mejor manera es una manera que simplemente no es la más corta. De todas las opciones, esto es lo que preferiría porque el autor de la clase tiene control total sobre el estado de los objetos deserializados. Si tuviera que adivinar, diría que todas las demás opciones, tarde o temprano, te harán entrar problemas (a menos que Javascript encuentre una forma nativa de lidiar con esto).

Realmente, el siguiente ejemplo no hace justicia a la flexibilidad. Realmente solo copia la estructura de la clase. La diferencia que hay que tener en cuenta aquí, sin embargo, es que la clase tiene control total para usar cualquier tipo de JSON que quiera controlar el estado de toda la clase (se podrían calcular cosas, etc.).

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);
 157
Author: Ingo Bürk,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-03-01 22:43:49

TLDR: TypedJSON (prueba de concepto de trabajo)


La raíz de la complejidad de este problema es que necesitamos deserializar JSON en tiempo de ejecución usando información de tipo que solo existe en tiempo de compilación. Esto requiere que la información de tipo esté disponible de alguna manera en tiempo de ejecución.

Afortunadamente, esto se puede resolver de una manera muy elegante y robusta con decoradores y Reflectdecoradores :

  1. Use decoradores de propiedades en propiedades que están sujetas a serialización, para registrar información de metadatos y almacenar esa información en algún lugar, por ejemplo en el prototipo de clase
  2. Alimentar esta información de metadatos a un inicializador recursivo (deserializer)

 

Tipo de registro-Información

Con una combinación de ReflectDecorators y decoradores de propiedades, escriba información puede ser fácilmente registrado sobre una propiedad. Una aplicación rudimentaria de este enfoque sería:

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);
}

Para cualquier propiedad dada, el fragmento de código anterior agregará una referencia de la función constructor de la propiedad a la propiedad oculta __propertyTypes__ en el prototipo de clase. Por ejemplo:

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

    @JsonMember// Number
    level: number;
}

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

    @JsonMember// Language
    language: Language;
}

Y eso es todo, tenemos la información de tipo requerida en tiempo de ejecución, que ahora se puede procesar.

 

Tipo de procesamiento-Información

Primero necesitamos obtener un Object instancia usando JSON.parse after después de eso, podemos iterar sobre las entires en __propertyTypes__ (recopilado anteriormente) e instanciar las propiedades requeridas en consecuencia. Se debe especificar el tipo del objeto raíz, para que el deserializador tenga un punto de partida.

Una vez más, una simple implementación muerta de este enfoque sería:

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);

La idea anterior tiene una gran ventaja de deserializar por tipos esperados (para valores complejos / objeto), en lugar de lo que está presente en el JSON. Si se espera un Person, entonces se crea una instancia Person. Con algunas medidas de seguridad adicionales en su lugar para tipos primitivos y matrices, este enfoque se puede hacer seguro, que resiste cualquier JSON malicioso.

 

Casos extremos

Sin embargo, si ahora estás contento de que la solución es que simple, tengo algunas malas noticias: hay un vasto número de casos extremos que necesitan ser atendidos. Solo algunos de los cuales son:

  • Arrays y elementos de array (especialmente en arrays anidados)
  • Polimorfismo
  • Clases abstractas e interfaces
  • ...

Si no quieres jugar con todos estos (apuesto a que no), me encantaría recomendar una versión experimental de trabajo de una prueba de concepto utilizando este enfoque, TypedJSON which que creé para abordar este problema exacto, un problema que me enfrento a diario.

Debido a cómo decoradores todavía se consideran experimentales, no recomendaría usarlo para uso de producción, pero hasta ahora me ha servido bien.

 29
Author: John Weisz,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-01-02 13:09:46

Puede usar Object.assign No se cuando se agregó esto, actualmente estoy usando Typescript 2.0.2, y esto parece ser una característica de ES6.

client.fetch( '' ).then( response => {
        return response.json();
    } ).then( json => {
        let hal : HalJson = Object.assign( new HalJson(), json );
        log.debug( "json", hal );

Aquí está HalJson

export class HalJson {
    _links: HalLinks;
}

export class HalLinks implements Links {
}

export interface Links {
    readonly [text: string]: Link;
}

export interface Link {
    readonly href: URL;
}

Esto es lo que Chrome dice que es

HalJson {_links: Object}
_links
:
Object
public
:
Object
href
:
"http://localhost:9000/v0/public

Así que puedes ver que no hace la asignación recursiva

 25
Author: xenoterracide,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-01-05 02:54:22

He estado usando a este tipo para hacer el trabajo: https://github.com/weichx/cerialize

Es muy simple pero potente. Soporta:

  • Serialización y deserialización de todo un árbol de objetos.
  • Propiedades persistentes y transitorias en el mismo objeto.
  • Ganchos para personalizar la lógica de (des)serialización.
  • Puede (de) serializarse en una instancia existente (ideal para Angular) o generar una nueva instancia.
  • etc.

Ejemplo:

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);
 10
Author: André,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-02-18 13:07:27

Opción #5: Usando constructores de Typescript y jQuery.extender

Este parece ser el método más mantenible: agregue un constructor que tome como parámetro la estructura json y extienda el objeto json. De esta manera puede analizar una estructura json en todo el modelo de aplicación.

No es necesario crear interfaces, o listar propiedades en constructor.

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;
    }
}

En su devolución de llamada ajax donde recibe una empresa para calcular los salarios:

onReceiveCompany( jsonCompany : any ) 
{
   let newCompany = new Company( jsonCompany );

   // call the methods on your newCompany object ...
   newCompany.calculateSalaries()
}
 2
Author: Anthony Brenelière,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-02-06 08:47:11

La 4a opción descrita anteriormente es una manera simple y agradable de hacerlo, que debe combinarse con la 2a opción en el caso en el que tenga que manejar una jerarquía de clases como por ejemplo una lista de miembros que es cualquiera de las ocurrencias de subclases de una superclase Miembro, por ejemplo, Director extiende Miembro o Estudiante extiende Miembro. En ese caso, debe dar el tipo de subclase en el formato json

 1
Author: Xavier Méhaut,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-12-02 03:29:52

He creado una herramienta que genera interfaces TypeScript y un "mapa de tipos" de tiempo de ejecución para realizar la comprobación de tipos en tiempo de ejecución contra los resultados de JSON.parse: ts.quicktype.io

Por ejemplo, dado este JSON:

{
  "name": "David",
  "pets": [
    {
      "name": "Smoochie",
      "species": "rhino"
    }
  ]
}

Quicktype produce la siguiente interfaz TypeScript y mapa de tipos:

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",
    },
};

Luego comparamos el resultado de JSON.parse con el mapa de tipos:

export function fromJson(json: string): Person {
    return cast(JSON.parse(json), object("Person"));
}

He omitido algún código, pero puedes probar quicktype para los detalles.

 1
Author: David Siegel,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-09-09 01:14:11

Tal vez no real, pero la solución simple:

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

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

Trabajar para dependencias difíciles también!!!

 0
Author: Михайло Пилип,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-07-28 09:30:14

JQuery .extend hace esto por usted:

var mytsobject = new mytsobject();

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

$.extend(mytsobject, newObj); //mytsobject will now contain a & b
 0
Author: Daniel,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-08-12 14:55:10

Otra opción usando fábricas

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

Use así

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);
  1. mantiene sus clases simples
  2. inyección disponible para las fábricas por flexibilidad
 0
Author: Anthony Johnston,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-09-13 21:50:50

Puedes hacer lo siguiente

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

Y

var instance: Instance = <Instance>({
      id: null,
      name: '',
      type: ''
    });
 -1
Author: Md Ayub Ali Sarker,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-10-19 16:10:04
**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;
    }
}
 -1
Author: user8390810,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-12-27 04:15:43