如何用JSON对象初始化类型记录对象?

内容来源于 Stack Overflow,并遵循CC BY-SA 3.0许可协议进行翻译与使用

  • 回答 (2)
  • 关注 (0)
  • 查看 (90)

我从AJAX调用收到一个JSON对象到REST服务器。该对象具有与我的TypeScript类匹配的属性名称。

初始化它的最佳方法是什么?我认为这不会起作用,因为类(&JSON对象)的成员是作为类的对象和成员的列表,而这些类的成员是列表和/或类。

但我更喜欢一种查找成员名称并将它们分配给对象的方法,根据需要创建列表并实例化类,所以我不必为每个类中的每个成员编写明确的代码(有很多!)

提问于
用户回答回答于

这些是一些快速拍摄,以显示几种不同的方式。他们绝不是“完整”的,作为一个免责声明,我认为这样做并不是一个好主意。而且代码不是太干净,因为我只是很快地将它们键入在一起。

同样值得注意的是:当然反序列化类需要具有默认的构造函数,就像我知道任何类型的反序列化的所有其他语言一样。当然,如果你调用一个没有参数的非默认构造函数,Javascript就不会抱怨,但是类更好地为它做好准备(另外,它实际上不是“typescripty方式”)。

选项#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:详细,但整洁的方式

对于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);
用户回答回答于

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__(在上面收集)中的entires 并相应地实例化所需的属性。必须指定根对象的类型,以便解串器具有起点。

再一次,这种方法的简单实现将是:

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预期是a,那么它就是一个Person创建的实例。通过为原始类型和数组提供一些额外的安全措施,可以使这种方法变得安全,抵御任何恶意JSON。

边缘案例

但是,如果你现在高兴的是,解决方案简单的,我有一些坏消息:有一个广阔的需要被照顾的边缘情况数。其中只有一些是:

  • 数组和数组元素(特别是在嵌套数组中)
  • 多态性
  • 抽象类和接口

如果你不想摆弄所有这些(我打赌你不这样做),我很乐意推荐一个使用这种方法的概念证明的工作实验版本,TypedJSON--我创建的解决这个确切的问题,我每天都面临着自己的问题。

由于装饰者仍被认为是实验性的,我不推荐将它用于生产用途,但到目前为止,它对我很好。

扫码关注云+社区