Restoring objects from Json - jackdarker/TwineTest GitHub Wiki

The problem

As you now know we can use JSON.stringify and JSON.parse to extract data from a object and use the created json-string to restore an object with that data. But the restored object will just be of type 'object' and not be of the original class-type. You can proof this with the following code:
var before = new Inventory();
var a= before.getItemId();
var str = JSON.stringify(before);
console.log(str); //
var after = JSON.parse(str);
var b= after.getItemId(); //<- error: unknown function

'after' will be of type 'object' and thus missing all methods from 'Inventory'.
The reason is that the JSON operations just extract simple-data from objects, but not the type of the data. F.e. for a string-variable, you will only find the name and the value of the variable, but not that its of type string.

I tried to bypass this problem by design of my data-structures:

  • all data that has to be saved need to be a plain object, a simple datatype, an array of those or an even more complex structure of this.
    But no custom-class type or objects with appended functions!
  • all operations need either be static methods because I cannot save and rebuild custom class-objects...
  • ...or I need to bind the save-data to a newly cosntructed class-object after load (f.e. construct a Inventory and conenct it with the inventory-array in the save data)

This works well until your nested datastructures get more complex.
F.e. the list entry of the inventory contains just the item-name and the item-count. With the item-name I can lookup in a table that holds the items-description. The item-description is in from of custom-classobjects with static methods to call.
This means I can store the content of the Inventory but not the state of the Items. I cannot save the charge state of a battery-item.

Using Reviver

After some more googling I found a possible way to save and restore complete custom objects:

  • we use the Reviver-parameter of the JSON.parse to customize the parsing process and handle special data in a certain way
  • each class needs to override toJson to add special data to the Json-collection, referencing the objects constructor
  • each class needs to override static fromJson to call the class constructor if there is one set
  • each class needs to register its constructor in a global registry so that fromJson can pull it from there; the constructur should work without parameters
  • we have to be careful that objects are not refering to each other in a way that forms a cyclic reference; this would cause the Json.stringify to fail.

Here is an example how adapt a class. Those modifications should be enough to make it work with the save-implementation win window.storage: class Character{
constructor() {
window.storage.registerConstructor(Character); //registers the class-constructor in the registry
}
toJSON() {return window.storage.Generic_toJSON("Character", this); };
static fromJSON(value) { return window.storage.Generic_fromJSON(Character, value.data);};
...
}
Most of the other required funtionality is implemented in window.storage:

  • window.storage.registerConstructor : used to register Constructors
  • window.storage.Reviver : Reviver-function supplied to Json.parse; checks data for presence of 'ctor'
  • window.storage.Generic_fromJSON : you can implement your own fromJson or just call this; has limited support for assigning variables via setter
  • window.storage.Generic_toJSON : you can implement your own toJson or just call this; adds 'ctor' to data

Cyclic references

In my code you will see that classes are composed from other classes and therefor refereing in a tree like manner:
Character->Inventory->Item
No problem so far.
But sometimes subcomponents need to know their containers, f.e. the Item needs to know who is using them. Therefore they might refer backwards: Item->Inventory->Character

class Character{
constructor(){ this.Inv = new Inventory(this);}
}
class Inventory{
constructor(owner){ this.parent = owner;}
}
And now we have a cyclic reference and get error from Json.stringify. Even more, Inventory-constructor now also requires parameter.

My actual workaround is to not have a variable parent refering to object but a getter that calls a funtion estimating the parent. So we dont have a variable that Json needs take care of. class Inventory{
constructor(){ }
get parent() {return this._parent();}
} class Character{
constructor(){ this.Inv = new Inventory();
this.Inv._parent = (function(me){ return function(){return me;}}(this)); //append a function to detect parent
} }

When the Json is parsed, the constructor of Character is called, setting up the Inventory properly. And then the Reviver is merging the previously recreated inventory data into this object.

But there is a footangle again - we need to be aware of the order of constructor calls. The object hierarchy is recreated from inside out. For my composition of objects Character->Inventory->Item, the parsing process is like following:

  • call fromJson for each element of Item
  • call fromJson for whole Item, this will call Item-constructor and merge already extracted data into the object
  • since Item._parent was a function, its not contained in json and will NOT be recreated !
  • call fromJson for each element of Inventory (to restore the list)
  • call from Json for Inventory; execute constructor and merge the recreated list of items and other data into it
  • again Inventory._parent is missing
  • call fromJson for each element of Character
  • call from Json for Character; execute constructor and merge the recreated objects into it
  • now the object hierarchy is recreated except the parent-functions

My idea was to adapt fromJson to reestablish _parent:
static fromJSON(value) {
var _x = window.storage.Generic_fromJSON(Inventory, value.data);
for(var i=0; i<_x.list.length;i++) {
_x.list[i].item._parent = (function(me){ return function(){return me;}}(this));
}
return(_x);
};
But this DOESNT work: _x is not the final inventory-object here because Character will call Inventory-constructor again and will merge the data into the new object !
Instead I have to fix all subcomponents after rebuild of Character:
static fromJSON(value) {
var _x = window.storage.Generic_fromJSON(Character, value.data);
_x.Effects._relinkItems(); //_relinkItems assigns _parent of the collection-items
_x.Stats._relinkItems();
_x.Inv._relinkItems();
return(_x);
};
If character is a component of another supercomponent, we would have again to pull this relinking into the supercomponent :(

Keep in mind

If using this way of saving/loading, you should verify:

  • avoid cyclic references or use solution from above
  • minimize data stored in objects because they will bloat the save-file; instead of a property, you can declare data in a function
    get foo(){return "bar"; }
  • make sure no data-item is set to null because the reviver cannot restore null; initialize data in the constructor but not to null