通过继承解析复杂的JSON对象

时间:2019-01-29 18:12:07

标签: json typescript local-storage

我正在建立一个批处理过程,其中包括许多不同类型的步骤。

export interface IStep {
    id: number;
    icon: string;
    name: string;
    selected: boolean;
}

export class InitStep implements IStep {
    id: number;
    icon: string;
    name: string;
    selected = false;
}

export class InputStep implements IStep {
    id: number;
    icon: string;
    name: string;
    selected = false;
    primaryKey: string;
    file: File;
}

export class QueryStep implements IStep {
    constructor () {
        this.filters = [];
        this.output_fields = [];
        this.table_fields = [];
        const filter = new Filter;
        this.filters.push(filter);
    }

    get input_ids(): number[] {
        return this.filters.map(filter => filter.input_id);
    }

    id: number;
    icon: string;
    name: string;
    selected = false;
    table: string;
    table_fields: string[];
    filters: Filter[];
    output_fields: string[];
}

export class OutputStep implements IStep {

    constructor() {
        this.fields = [];
    }

    id: number;
    icon: string;
    name: string;
    selected = false;
    fields: string[];
}

export class DeliveryStep implements IStep {

    constructor() {
        this.output_ids = [];
    }

    id: number;
    icon: string;
    name: string;
    selected = false;
    output_ids: number[];
    format: BatchOutputType;
    frequency: BatchFrequencyType;
    email: string;
    password: string;
}

我希望能够拥有这些步骤的任意组合/数量的数组,并能够将它们保存到本地存储并从中读取。

const key = 'notgunnawork';
localStorage.setItem(key, JSON.stringify(this.steps));
const s = JSON.parse(key) as IStep[];

我知道在地狱中有一个滚雪球的机会,这将可以正确地进行解析,显然,解析器不知道哪些步骤最终属于哪些类。我只是想知道是否有一种简单的方法可以使数组出来的样子与输入数组相同。我最终会将这个列表发布到服务器,并希望我的.Net Core代码也能够解析此JSON,而无需创建自定义解析器。

编辑

添加了Im尝试序列化的完整类,以获取更多详细信息。每当我尝试序列化然后反序列化时遇到的错误是:“ JSON在位置1处出现意外令牌o”

1 个答案:

答案 0 :(得分:1)

所以,我要回答我认为的问题,如果我错了,请随时忽略我

您的问题是您有一堆带有方法的类,但是当将它们的实例序列化为JSON然后反序列化它们时,最终得到的是普通的JavaScript对象,而不是类的实例。一种解决方法是使用自定义解串器,该解串器了解您的类,并且可以将普通的JavaScript对象“混合”或“还原”为真正的类实例。使用JSON.parse()函数,您可以指定一个名为reviver的回调参数。

首先,我们需要建立一个系统,齐磊者将通过该系统了解您的可序列化类。我将使用一个class decorator,它将把每个类构造函数添加到reviver可以使用的注册表中。我们将要求可序列化的类构造函数可分配给我们可以称为Serializable的类型:它需要有一个无参数的构造函数,而它所构造的事物需要有一个className属性:

// a Serializable class has a no-arg constructor and an instance property
// named className
type Serializable = new () => { readonly className: string }

// store a registry of Serializable classes
const registry: Record<string, Serializable> = {};

// a decorator that adds classes to the registry
function serializable<T extends Serializable>(constructor: T) {
  registry[(new constructor()).className] = constructor;
  return constructor;
}

现在,当您想反序列化某些JSON时,可以检查序列化的事物是否具有className属性,该属性是注册表中的键。如果是这样,您可以在注册表中使用该类名称的构造函数,然后通过Object.assign()将属性复制到其中:

// a custom JSON parser... if the parsed value has a className property
// and is in the registry, create a new instance of the class and copy
// the properties of the value into the new instance.
const reviver = (k: string, v: any) =>
  ((typeof v === "object") && ("className" in v) && (v.className in registry)) ?
    Object.assign(new registry[v.className](), v) : v;

// use this to deserialize JSON instead of plain JSON.parse        
function deserializeJSON(json: string) {
  return JSON.parse(json, reviver);
}

好了,现在我们有了,让我们做一些课程。 (在编辑之前,我在这里使用的是原始定义。)请注意,我们需要添加一个className属性,并且必须有一个no-arg构造函数(如果您不指定构造函数,因为default constructor是无参数):

// mark each class as serializable, which requires a className and a no-arg constructor
@serializable
class StepType1 implements IStep {
  id: number = 0;
  name: string = "";
  prop1: string = "";
  readonly className = "StepType1"
}

@serializable // error, property className is missing
class OopsNoClassName {

}

@serializable // error, no no-arg constructor
class OopsConstructorRequiresArguments {
  readonly className = "OopsConstructorRequiresArguments"
  constructor(arg: any) {

  }
}

@serializable
class StepType2 implements IStep {
  id: number = 0;
  name: string = "";
  prop2: string = "";
  prop3: string = "";
  prop4: string = "";
  readonly className = "StepType2"
}

@serializable
class StepType3 implements IStep {
  id: number = 0;
  name: string = "";
  prop5: string = "";
  prop6: string = "";
  readonly className = "StepType3"
}

现在让我们对其进行测试。像平常一样制作一些对象,并将它们放在数组中:

// create some objects of our classes
const stepType1 = new StepType1();
stepType1.id = 1;
stepType1.name = "Alice";
stepType1.prop1 = "apples";

const stepType2 = new StepType2();
stepType2.id = 2;
stepType2.name = "Bob";
stepType2.prop2 = "bananas";
stepType2.prop3 = "blueberries";
stepType2.prop4 = "boysenberries";

const stepType3 = new StepType3();
stepType3.id = 3;
stepType3.name = "Carol";
stepType3.prop5 = "cherries";
stepType3.prop6 = "cantaloupes";

// make an array of IStep[]
const arr = [stepType1, stepType2, stepType3];

让我们有一个函数,它将检查数组的元素并检查它们是否是您的类的实例:

// verify that an array of IStep[] contains class instances
function verifyArray(arr: IStep[]) {
  console.log("Array contents:\n" + arr.map(a => {
    const constructorName = (a instanceof StepType1) ? "StepType1" :
      (a instanceof StepType2) ? "StepType2" :
        (a instanceof StepType3) ? "StepType3" : "???"
    return ("id=" + a.id + ", name=" + a.name + ", instanceof " + constructorName)
  }).join("\n") + "\n");
}

让我们确保它可以在arr上运行:

// before serialization, everything is fine
verifyArray(arr);
// Array contents:
// id=1, name=Alice, instanceof StepType1
// id=2, name=Bob, instanceof StepType2
// id=3, name=Carol, instanceof StepType3

然后我们将其序列化:

// serialize to JSON
const json = JSON.stringify(arr);

为演示您的原始问题,让我们看看如果仅使用JSON.parse()而没有齐磊的话会发生什么情况:

// try to deserialize with just JSON.parse
const badParsedArr = JSON.parse(json) as IStep[];

// uh oh, none of the deserialized objects are actually class instances
verifyArray(badParsedArr);
// Array contents:
// id=1, name=Alice, instanceof ???
// id=2, name=Bob, instanceof ???
// id=3, name=Carol, instanceof ???

如您所见,badParsedArr中的对象确实具有idname属性(如果选中,则还有其他特定于类的实例属性,例如prop3)但它们不是您的类的实例。

现在我们可以使用自定义解串器查看问题是否已解决:

// do the deserialization with our custom deserializer
const goodParsedArr = deserializeJSON(json) as IStep[];

// now everything is fine again
verifyArray(goodParsedArr);
// Array contents:
// id=1, name=Alice, instanceof StepType1
// id=2, name=Bob, instanceof StepType2
// id=3, name=Carol, instanceof StepType3

是的,它有效!


上面的方法很好,但是有一些警告。最主要的一点是:如果您的可序列化类包含本身可序列化的属性,它将起作用,只要您的对象图是tree,其中每个对象恰好出现一次。但是,如果其中包含带有任何类型的cycle的对象图(这意味着如果您以多种方式遍历该图,则同一对象会出现多次),那么您将得到意外的结果。例如:

const badArr = [stepType1, stepType1];
console.log(badArr[0] === badArr[1]); // true, same object twice
const badArrParsed = deserializeJSON(JSON.stringify(badArr));
console.log(badArrParsed[0] === baddArrParsed[1]); // false, two different objects

在上述情况下,同一对象出现多次。在对数组进行序列化和反序列化时,新数组包含两个具有相同属性值的 different 对象。如果需要确保只对某个特定对象反序列化一次,那么您需要一个更复杂的deserialize()函数,该函数跟踪某些唯一属性(如id)并返回现有对象而不是创建新的。

其他警告:假设您的可序列化类具有仅由其他可序列化类以及JSON友好值(例如字符串,数字,数组,纯对象和null)组成的实例属性。如果您使用其他东西,例如Date,则必须处理将它们序列化为字符串的事实。

对您而言,序列化/反序列化的复杂程度在很大程度上取决于您的用例。


好的,希望能有所帮助。祝你好运!