首页 > 解决方案 > 使用继承解析复杂的 JSON 对象

问题描述

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

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 无需我制作自定义解析器。

编辑

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

标签: jsontypescriptlocal-storage

解决方案


所以,我将回答我认为您的问题,如果我错了,请随时无视我

您的问题是您有一堆带有方法的类,但是当您将这些实例序列化为 JSON 然后将它们反序列化时,您最终会得到普通的 JavaScript 对象,而不是类的实例。处理这个问题的一种方法是使用一个自定义的反序列化器,它知道你的类并且可以“水合”或“恢复”普通的 JavaScript 对象到真正的类实例中。该JSON.parse()函数允许您指定一个调用的回调参数reviver,该参数可用于执行此操作。

首先,我们需要建立一个系统,reviver 将通过该系统了解您的可序列化类。我将使用一个类装饰器,它将每个类构造函数添加到 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属性并且我们必须有一个无参数构造函数(如果您不指定构造函数,这将免费发生,因为默认构造函数是无参数的):

// 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()没有 reviver 会发生什么:

// 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确实具有idandname属性(以及其他特定于类的实例属性,如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

是的,它有效!


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

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

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

其他注意事项:这假设您的可序列化类具有仅由其他可序列化类以及 JSON 友好值(如字符串、数字、数组、普通对象和null. 如果你使用其他东西,比如Dates,你将不得不处理那些序列化为字符串的事实。

序列化/反序列化对您来说究竟有多复杂,很大程度上取决于您的用例。


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


推荐阅读