Package Exports
- @stevef51/json-tree
This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (@stevef51/json-tree) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.
Readme
JsonTree - a circular reference aware and customizable Javscript serializer/deserializer
Note, this package is similar to the popular 'flatted' (and CircularJSON) package but handles more scenarios, 'flatted' is very likely to be faster (untested)
Installation
npm install @stevef51/json-tree
Usage
import { JsonTree } from '@stevef51/json-tree';
expect(JsonTree.parse(JsonTree.stringify('Hello world'))).toBe('Hello world');
Similarities to flatted :-
- Serializes to and from a string in same fashion as JSON.stringify/parse
- Able to serialize/deserialize simple primitives, arrays, objects and importantly handles circular references
- Flattens a hierarchy into an easy to interpret array with references to other elements
Key differences to flatted :-
- Objects create with {} and Objects created with Object.create(null) are handled (ie correct prototype can be recreated)
- Able to register "custom type translators" which handle classes
- Everything is keyed into the root array including primitives which allows for primitive compression - eg a Number is gauranteed to appear once in the root array no matter how many times it is used in the object hierarchy
- Object property names are also keyed into the root array for better "compression" of large trees (single objects will be bigger due to keying overhead)
- Able to handle "externs", objects which are not to be serialized but can be "hooked" back up during deserialization
Examples
JsonTree.stringify('Hello world')
//['Hello world']
JsonTree.stringify(123)
//[123]
JsonTree.stringify(['Hello world', 123])
//[[[1,2]],'Hello world',123]
JsonTree.stringify([123,123])
//[[[1,1]],123]
var fred = { name: 'Fred', age: 36 }
JsonTree.stringify(fred);
//[[1,2],"Object",{"3":4,"5":6},"name","Fred","age",36]
var betty = { name: 'Betty', age: 36 }
betty.brother = fred;
fred.sister = betty;
JsonTree.stringify([fred, betty])
//[[[1,10]],[2,3],"Object",{"4":5,"6":7,"8":9},"name","Fred","age",36,"sister",[2,11],{"4":12,"6":5,"brother":1},"Betty","brother"]
To handle custom types (classes), you register your type with JsonTree :-
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
}
JsonTreeTranslators.register({
ctr: Person
})
JsonTree.stringify(new Person("Fred", 36))
//[[1,2],"Person",{"3":4,"5":6},"name","Fred","age",36]
To avoid possible name clashes, use a name override when registering
JsonTreeTranslators.register({
ctr: Person,
name: 'My Person'
})
JsonTree.stringify(new Person("Fred", 36))
//[[1,2],"My Person",{"3":4,"5":6},"name","Fred","age",36]
To handle more complex types that don't necessarily have public properties that you want to iterate and serialize you provide your own flatten and fatten methods, for example to handle a Javascript Moment along with possible timezone (eg moment-timezone), simply use the following registration
JsonTreeTranslators.register({
ctr: moment().constructor, // Required since access to the actual constructor is hidden by anonymous functions
name: 'Moment',
flatten(o) {
return {
dt: o.format(),
tz: o.tz()
}
},
fatten(o, fatten, store) {
// Note, o is an object with the same properties are returned from 'flatten', however its values need 'fattening' to be used
var m = moment(fatten(o.dt));
if (o.tz != null) {
m = m.tz(fatten(o.tz));
}
return store(m); // You must 'store' the result to allow JsonTree circular referencing to work
}
})
The flatten method
This is quite simple, you return an object/primitive/array of whatever you need to properly serialize your object.
In the Moment example above we return enough information to fully restore the Moment in the fatten method
The fatten method
A little more complex due to how deserialization must handle possible circular references, your fatten method is passed an object to fatten, a funtion fatten which will fatten other objects and a store method which you must call as early as possible to register your fattened object to support circular reference.
The default Object fatten method is essentially as follows, note that it calls store right away before fattening the objects properties :-
fatten(o: any, fatten: (o: any) => any, store: (o: any) => any) {
let fatObj = store({}); // Create the Object and store it right away
let hasOwnProperty = Object.hasOwnProperty.bind(o);
// Populate the Objects properties
for (let p in o) {
if (hasOwnProperty(p)) {
fatObj[p] = fatten(o[p]); // Fatten properties
}
}
return fatObj; // Return the fully fattened object
}
If your objects have no possibility of circular references then calling store at the end will work fine (like the Moment example)
Example of custom fatten
import { JsonTree, JsonTreeTranslators, Convert } from 'json-tree';
class Person {
public brother?: Person;
public sister?: Person;
constructor(public name: string, public age: number) {
}
}
let fred = new Person('Fred', 36);
let betty = new Person('Betty', 32);
fred.sister = betty;
betty.brother = fred;
JsonTreeTranslators.register({
ctr: Person,
fatten: (o: any, fatten: Convert, store: Convert) {
// name & age are constructor required and cannot circular reference
// call _store_ with our new Person right away
let p = store(new Person(fatten(o.name), fatten(o.age)));
// set _brother_ and _sister_ which will eventually use the object
// already _stored_ to fulfill the circular reference
o.brother && p.brother = fatten(o.brother);
o.sister && p.sister = fatten(o.sister);
return p;
}
})
JsonTree.stringify([fred,betty]);
//[[[1,10]],[2,3],"Person",{"4":5,"6":7,"8":9},"name","Fred","age",36,"sister",[2,11],{"4":12,"5":9,"13":1},"Betty","brother"]
Note, there is no need for a custom flatten since all public properties are automatically handled by the default flatten method
Externs
In some cases you may have certain objects which you do not want to be serialized, this is handled by a custom JsonTree instance and setting its externs property to the list of objects you dont want serialized
Using the Person class defined earlier, if we did not want Fred to be serialized ..
let fred = new Person("Fred",36);
let jt = new JsonTree();
jt.externs = [fred];
let externFredAndBetty = jt.stringify([fred,betty])
//[[[-1,1]],[2,3],"Person",{"4":5,"6":7,"8":-1},"name","Betty","age":32,"brother"]
and to deserialise the original structure with a prepared Fred object
let fred = new Person("Fred",36);
let jt = new JsonTree();
jt.externs = [fred];
let [fred2,betty] = jt.parse(externFredAndBetty);
expect(fred2).toBe(fred);
expect(betty.brother).toBe(fred);
Provided the array of externs has the same order and length in both the stringify and parse calls then your tree will work perfectly