[json] JSON 객체로 TypeScript 객체를 초기화하는 방법

REST 서버에 대한 AJAX 호출에서 JSON 객체를받습니다. 이 객체에는 TypeScript 클래스와 일치하는 속성 이름이 있습니다 ( 이 질문 에 대한 후속 조치입니다 ).

그것을 초기화하는 가장 좋은 방법은 무엇입니까? 클래스 (& JSON 객체)에는 객체 목록 인 멤버와 클래스 인 멤버가 있고 해당 클래스에는 목록 및 / 또는 클래스 인 멤버가 있기 때문에 이것이 효과 가 있다고 생각하지 않습니다 .

그러나 멤버 이름을 조회하고 필요에 따라 목록을 만들고 클래스를 인스턴스화하는 방법을 선호하므로 모든 클래스의 모든 멤버에 대해 명시 적 코드를 작성할 필요가 없습니다 (LOT이 있습니다!)



답변

이것들은 몇 가지 다른 방법을 보여주기 위해 이것에 대한 빠른 샷입니다. 그것들은 결코 “완전한”것이 아니며, 면책 조항으로, 이렇게하는 것이 좋은 생각이라고 생각하지 않습니다. 또한 코드를 함께 빨리 입력했기 때문에 코드가 너무 깨끗하지 않습니다.

또한 참고 : 물론 역 직렬화 가능 클래스에는 모든 종류의 역 직렬화를 알고있는 다른 모든 언어의 경우와 마찬가지로 기본 생성자가 있어야합니다. 물론, 인수없이 기본이 아닌 생성자를 호출해도 Javascript는 불만을 제기하지 않지만 클래스를 준비하는 것이 좋습니다.

옵션 # 1 : 런타임 정보가 전혀 없음

이 방법의 문제점은 대부분 멤버의 이름이 해당 클래스와 일치해야한다는 것입니다. 클래스별로 같은 유형의 멤버로 자동 제한하고 여러 가지 모범 사례 규칙을 위반합니다. 나는 이것에 대해 강력히 권고하지만,이 답변을 썼을 때 처음으로 “초안”이었기 때문에 여기에 적어 두십시오.

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 : 장황하지만 깔끔한 방법

2016 년 1 월 3 일 업데이트 : @GameAlchemist 가 Typescript 1.7 의 의견 ( idea , implementation ) 에서 지적했듯이 아래 설명 된 솔루션은 클래스 / 속성 데코레이터를 사용하여 더 나은 방법으로 작성할 수 있습니다.

직렬화는 항상 문제이며 제 생각에 가장 좋은 방법은 가장 짧지 않은 방법입니다. 모든 옵션 중에서 클래스의 작성자가 deserialized 객체의 상태를 완전히 제어 할 수 있기 때문에 이것이 내가 선호하는 것입니다. 내가 추측해야한다면, 다른 모든 옵션이 조만간 당신을 곤경에 빠뜨릴 것이라고 말하고 싶습니다 (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);


답변

당신이 사용할 수있는 Object.assign이가, 나는 현재 타이프 라이터 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

재귀 적으로 할당하지 않는 것을 볼 수 있습니다.


답변

TLDR : TypedJSON (작업 개념 증명)


이 문제의 복잡성의 근본 원인은 컴파일 타임 에만 존재하는 유형 정보를 사용하여 런타임시 JSON을 직렬화 해제해야한다는 것 입니다. 이를 위해서는 런타임에 형식 정보를 사용할 수 있어야합니다.

다행히도 데코레이터ReflectDecorators 를 사용하면 매우 우아하고 강력한 방법으로 해결할 수 있습니다 .

  1. 직렬화 대상 속성에 속성 데코레이터 를 사용 하여 메타 데이터 정보를 기록하고 해당 정보를 클래스 프로토 타입과 같은 곳에 저장
  2. 이 메타 데이터 정보를 재귀 이니셜 라이저 (디시리얼라이저)에 제공

 

녹화 유형 정보

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그 후에는 __propertyTypes__위에서 수집 한 전체를 반복 하고 필요한 속성을 인스턴스화 할 수 있습니다. deserializer에 시작점이 있도록 루트 개체의 유형을 지정해야합니다.

다시 말하지만,이 접근법의 간단한 구현은 다음과 같습니다

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 – 내가 만든 이 정확한 문제를 해결하기 위해 제가 매일 직면하는 문제입니다.

데코레이터가 여전히 실험적으로 간주되는 방식으로 인해 프로덕션 용도로 사용하는 것은 좋지 않지만 지금까지는 잘 작동했습니다.


답변

나는이 사람을 사용하여 일을 해왔다 : https://github.com/weichx/cerialize

매우 간단하지만 강력합니다. 다음을 지원합니다.

  • 전체 객체 트리의 직렬화 및 역 직렬화
  • 동일한 객체의 지속적 및 일시적 속성.
  • 직렬화 해제 논리를 사용자 정의하기위한 후크입니다.
  • 기존 인스턴스로 직렬화 (직렬화)하거나 (앵귤러에 적합) 새 인스턴스를 생성 할 수 있습니다.
  • 기타

예:

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


답변

나는 타이프 라이터 인터페이스와 결과에 대한 런타임 유형 검사를 수행하기위한 실행 “형태 맵”을 생성하는 도구를 만들었습니다 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"));
}

코드를 생략 했지만 세부 정보를 보려면 빠른 유형 을 사용해보십시오 .


답변

옵션 # 5 : 타입 스크립트 생성자와 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'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()
}


답변

간단한 객체의 경우이 방법이 좋습니다.

class Person {
  constructor(
    public id: String,
    public name: String,
    public title: String) {};

  static deserialize(input:any): Person {
    return new Person(input.id, input.name, input.title);
  }
}

var person = Person.deserialize({id: 'P123', name: 'Bob', title: 'Mr'});

생성자에서 속성을 정의하는 기능을 활용하면 간결 해집니다.

이렇게하면 입력 된 객체 (Object.assign 또는 객체를 제공하는 일부 변형을 사용하는 모든 답변)와 외부 라이브러리 또는 데코레이터가 필요하지 않습니다.