recorrer - ¿Cómo inicializo un objeto de TypeScript con un objeto JSON?
string to json javascript (12)
Opción n. ° 5: uso de constructores de TypeScript y jQuery.extend
Este parece ser el método más fácil de mantener: agregue un constructor que tome como parámetro la estructura json y extienda el objeto json. De esta forma, puede analizar una estructura json en todo el modelo de la aplicación.
No es necesario crear interfaces, o listar propiedades en el 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 compañía para calcular los salarios:
onReceiveCompany( jsonCompany : any )
{
let newCompany = new Company( jsonCompany );
// call the methods on your newCompany object ...
newCompany.calculateSalaries()
}
Recibo un objeto JSON de una llamada AJAX a un servidor REST. Este objeto tiene nombres de propiedad que coinciden con mi clase de TypeScript (este es un seguimiento de esta pregunta ).
¿Cuál es la mejor manera de inicializarlo? No creo que this funcione porque la clase (y el objeto JSON) tienen 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 los nombres de los miembros y los asigne, creando listas y creando instancias de clases según sea necesario, para que no tenga que escribir código explícito para cada miembro de cada clase (¡hay MUCHO!)
Creé una herramienta que genera interfaces de TypeScript y un "mapa de tipos" en tiempo de ejecución para realizar la comprobación de tipos de tiempo de ejecución frente a 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 el tipo de mapa:
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 verificamos el resultado de JSON.parse
relación 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 de forma rápida los detalles.
Estas son algunas tomas rápidas para mostrar algunas formas diferentes. De ninguna manera son "completos" y como un descargo de responsabilidad, no creo que sea una buena idea hacerlo así. Además, el código no está muy limpio, ya que lo escribí bastante rápido.
También como una nota: por supuesto, las clases deserializables deben tener constructores por defecto, como es el caso en todos los demás idiomas, donde conozco la deserialización de cualquier tipo. Por supuesto, Javascript no se quejará si llamas a un constructor no predeterminado sin argumentos, pero la clase mejor estará preparada para eso (además, no sería realmente la "manera tipográfica").
Opción 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 te limita a un miembro del mismo tipo por clase y rompe varias reglas de buenas prácticas. Recomiendo enfáticamente que no lo haga, pero simplemente enumérelo aquí porque fue el primer "borrador" cuando escribí esta respuesta (que también es el motivo por el que 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 n. ° 2: la propiedad del nombre
Para deshacerse del problema en la opción n. ° 1, necesitamos tener algún tipo de información sobre 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 configuran.
Una forma de hacerlo es haciendo que las clases conozcan sus nombres. Sin embargo, 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 n. ° 3: establecer explícitamente 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 disponibles. Solo tenemos que hacer esto para los miembros no primitivos y estamos listos para continuar:
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 n. ° 4: la forma prolija pero clara
Actualización 01/01/2016 : Como señaló @GameAlchemist en los comentarios, a partir de Typescript 1.7, la solución que se describe a continuación se puede escribir de una mejor manera usando decoradores de clase / propiedad.
La serialización siempre es un problema y, en mi opinión, la mejor manera es una que simplemente no sea la más corta. De todas las opciones, esto es lo que prefiero 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 otras opciones, tarde o temprano, te meterán en problemas (a menos que Javascript tenga 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 debe tener en cuenta aquí, sin embargo, es que la clase tiene control total para usar cualquier clase de JSON, quiere controlar el estado de toda la clase (puede 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);
He estado usando este chico para hacer el trabajo: https://github.com/weichx/cerialize
Es muy simple pero poderoso. Es compatible con:
- 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) serialización.
- Puede (de) serializarse en una instancia existente (ideal para Angular) o generar nuevas instancias.
- 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);
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
La cuarta opción descrita anteriormente es una forma simple y agradable de hacerlo, que tiene que combinarse con la segunda opción en el caso de que tenga que manejar una jerarquía de clases como, por ejemplo, una lista de miembros que es una de las ocurrencias de subclases de una superclase miembro, p. ej., el director extiende miembro o el miembro extiende estudiante. En ese caso, debe dar el tipo de subclase en el formato json
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
usar 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);
- mantiene tus clases simples
- inyección disponible para las fábricas para la flexibilidad
Tal vez no sea una solución real, sino simple:
interface Bar{
x:number;
y?:string;
}
var baz:Bar = JSON.parse(jsonString);
alert(baz.y);
¡trabaja para dependencias difíciles también!
puede usar Object.assign
. No sé cuándo se agregó, actualmente estoy usando Typescript 2.0.2, y esta 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 dice Chrome es
HalJson {_links: Object}
_links
:
Object
public
:
Object
href
:
"http://localhost:9000/v0/public
para que pueda ver que no hace la asignación recursiva
puedes hacer lo siguiente
export interface Instance {
id?:string;
name?:string;
type:string;
}
y
var instance: Instance = <Instance>({
id: null,
name: '''',
type: ''''
});
TLDR: TypedJSON (prueba de concepto en funcionamiento)
La raíz de la complejidad de este problema es que necesitamos deserializar JSON en tiempo de ejecución utilizando información de tipo que solo existe en tiempo de compilación . Esto requiere que la información de tipo esté de alguna manera disponible en tiempo de ejecución.
Afortunadamente, esto se puede resolver de una manera muy elegante y robusta con decorators y ReflectDecorators :
- Utilice 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
- Alimente esta información de metadatos a un inicializador recursivo (deserializador)
Tipo de información de grabación
Con una combinación de ReflectDecorators y decoradores de propiedades, la información del tipo se puede registrar fácilmente sobre una propiedad. Una implementació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 arriba agregará una referencia de la función de constructor de la propiedad a la propiedad __propertyTypes__
oculta 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.
Información de tipo de procesamiento
Primero necesitamos obtener una instancia de Object
usando JSON.parse
- después de eso, podemos iterar sobre las entidades en __propertyTypes__
(recopilados arriba) e instanciar las propiedades requeridas en consecuencia. El tipo del objeto raíz debe especificarse para que el deserializador tenga un punto de partida.
Una vez más, una implementación simple y 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 deserialización por tipos esperados (para valores complejos / objetos), en lugar de lo que está presente en el JSON. Si se espera una Person
, entonces se crea una instancia de Person
. Con algunas medidas de seguridad adicionales implementadas para tipos y matrices primitivos, este enfoque puede hacerse seguro y resiste cualquier JSON malicioso.
Casos de borde
Sin embargo, si ahora está contento de que la solución sea así de simple, tengo algunas malas noticias: hay una gran cantidad de casos extremos que deben ser atendidos. Solo algunos de los cuales son:
- Matrices y elementos de matriz (especialmente en matrices anidadas)
- Polimorfismo
- Clases e interfaces abstractas
- ...
Si no quieres jugar con todo esto (apuesto a que no), con mucho gusto recomendaría una versión experimental de prueba de concepto utilizando este enfoque, TypedJSON , que creé para abordar este problema exacto, un problema que me enfrento a diario.
Debido a la forma en que los decoradores todavía se consideran experimentales, no recomendaría su uso para producción, pero hasta ahora me ha ido bien.
**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;
}
}