【问题标题】:Parsing Complex JSON Objects with inheritance使用继承解析复杂的 JSON 对象
【发布时间】:2019-06-22 22:40:20
【问题描述】:

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

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”

【问题讨论】:

  • 我认为除非您提供更多详细信息,否则这将被否决。到目前为止,您尝试过什么?
  • 我不明白为什么这不能如你所愿!您的代码应该一切正常
  • 我试图在这里的两行之间阅读......您的问题是序列化/反序列化项目不再是您的类的实例吗?如果是这样,也许类并没有给你买太多东西,你可以只用接口来代替。或者你必须为你的 JSON 解析创建一个自定义的 reviver 来反序列化为类实例。但我认为我们需要更多信息才能为您提供帮助。
  • @jcalz 感谢您的 cmets,如果这增加了任何洞察力,我已经添加了实际的类,并且我在尝试序列化然后反序列化 JSON 时立即得到错误。
  • 你是在解析本地存储key而不是-value吗?在您担心反序列化为类实例之前,需要先解决这个问题。

标签: json typescript local-storage


【解决方案1】:

所以,我将回答我认为您的问题,如果我错了,请随意忽略我?

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

首先,我们需要建立一个系统,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 属性,并且我们必须有一个无参数构造函数(如果您不指定构造函数,这将免费发生,因为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() 而不使用 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 中的对象确实具有 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

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

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

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


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

【讨论】:

  • 天哪,没错,就是这个!非常感谢!
  • 嘿@jcalz,感谢您的详细回答。我正在研究与 ikenread 类似的问题,但我还必须处理实际继承。现在我有一个主类事件,我从中派生子类。我将如何归档您描述的相同行为?
  • 不确定...您可能需要使用minimal reproducible example 发布一个新问题并提及您尝试过的内容(以及为什么此处的代码不起作用)。我看不出问题出在哪里,除非您应该确保超类的 className 属性仍然是 string 类型并且不缩小为字符串文字?
猜你喜欢
  • 2016-05-31
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-09-05
  • 1970-01-01
  • 1970-01-01
  • 2011-06-01
  • 2013-09-02
相关资源
最近更新 更多