typescript - Analysera komplexa JSON-objekt med arv
Jag bygger en batchprocess som inkluderar ett antal steg av olika typer.
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;
}
Jag vill kunna ha en array med vilken kombination/antal som helst av dessa steg och kunna spara dem till och läsa dem från lokal lagring.
const key = 'notgunnawork';
localStorage.setItem(key, JSON.stringify(this.steps));
const s = JSON.parse(key) as IStep[];
Jag visste att det fanns en minimal chans att detta skulle parse korrekt, uppenbarligen vet inte parsern vilka steg som tillhör vilka klasser i slutändan. Jag undrade bara om det fanns ett enkelt sätt att få min array att se ut på samma sätt när den kom ut som när den gick in. Jag kommer så småningom att posta den här listan till servern och skulle vilja att min .Net Core-kod också kan parse detta JSON utan att jag behöver göra en anpassad parser.
REDIGERA
Lade till hela klasserna av det jag försöker serialisera, för mer detaljer. Felet jag får varje gång jag försöker serialisera och sedan deserialisera är: "Unexpected token o in JSON at position 1"
Svar #1
Så, jag kommer att svara på vad jag tror att ditt problem är, och om jag har fel får du gärna ignorera mig 🙂
Ditt problem är att du har en massa klasser med metoder men när du serialiserar instanser av dessa till JSON och sedan deserialiserar dem tillbaka, får du vanliga JavaScript-objekt och inte instanser av dina klasser. Ett sätt att hantera detta är att använda en anpassad deserialiserare som känner till dina klasser och kan "hydrera" eller "återuppliva" de vanliga JavaScript-objekten till genuina klassinstanser. Funktionen JSON.parse() tillåter dig att ange en callback-parameter som kallas reviver som kan användas för att just göra detta.
Först behöver vi sätta upp ett system genom vilket återupplivaren kommer att känna till dina serialiserbara klasser. Jag kommer att använda en klassdekoration som lägger till varje klasskonstruktor i ett register som återupplivaren kan använda. Vi kommer att kräva att en serialiserbar klasskonstruktor kan tilldelas en typ som vi kan kalla Serializable: den behöver ha en konstruktör utan argument och de objekt den konstruerar behöver ha en className-egenskap:
// 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;
}
Nu, när du vill deserialisera något JSON, kan du kontrollera om det serialiserade objektet har en className-egenskap som är en nyckel i registret. Om så är fallet använder du konstruktorn för det klassnamnet i registret och kopierar egenskaper till det via 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);
}
Okej, nu när vi har det, låt oss skapa några klasser. (Jag använder dina ursprungliga definitioner här, innan dina ändringar.) Observera att vi är tvungna att lägga till en className-egenskap och vi måste ha en konstruktör utan argument (detta händer automatiskt om du inte specificerar en konstruktör, eftersom standardkonstruktören är utan argument):
// 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"
}
Nu låt oss testa det. Skapa några objekt som du vanligtvis skulle göra, och lägg dem i en array:
// 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];
Och låt oss ha en funktion som kommer att undersöka elementen i en array och kontrollera om de är instanser av dina klasser:
// 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");
}
Låt oss se till att det fungerar på 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
Sedan serialiserar vi det:
// serialize to JSON
const json = JSON.stringify(arr);
För att visa ditt ursprungliga problem, låt oss se vad som händer om vi bara använder JSON.parse() utan en 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 ???
Som du kan se har objekten i badParsedArr id- och name-egenskaperna (och de andra klassspecifika instansegenskaperna som prop3 om du kontrollerade) men de är inte instanser av dina klasser.
Nu kan vi se om problemet är fixat genom att använda vår anpassade deserializer:
// 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
Ja, det fungerar!
Ovanstående metod är bra, men det finns förbehåll. Huvudförbehållet: det fungerar om dina serialiserbara klasser innehåller egenskaper som själva är serialiserbara, så länge som ditt objektgraf är ett träd, där varje objekt förekommer exakt en gång. Men om du har ett objektgraf med någon form av cykel i sig (vilket betyder att samma objekt förekommer flera gånger om du traverserar grafen på flera sätt) då kommer du få oväntade resultat. Till exempel:
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
I fallet ovan visas samma objekt flera gånger. När du serialiserar och deserialiserar arrayen innehåller din nya array två olika objekt med samma egenskapsvärden. Om du behöver säkerställa att du endast deserialiserar ett visst objekt exakt en gång, behöver du en mer komplicerad deserialize()-funktion som håller reda på någon unik egenskap (som id) och returnerar befintliga objekt istället för att skapa nya.
Andra varningar: detta förutsätter att dina serialiserbara klasser har instans-egenskaper som endast består av andra serialiserbara klasser samt JSON-vänliga värden som strängar, siffror, arrayer, vanliga objekt och null. Om du använder andra saker, som Date-objekt, måste du hantera det faktum att dessa serialiseras till strängar.
Exakt hur komplicerad serialisering/deserialisering är för dig beror i hög grad på ditt användningsfall.
Okej, hoppas att det hjälper. Lycka till!
