npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@stevef51/json-tree

v1.4.0

Published

A circular reference and type aware JSON serializer/deserializer

Downloads

2

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
  • Optionally, Object property names can be keyed into the root array for better "compression" of large trees (single objects will likely 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",{"name":3,"age":4},"Fred",36]


var betty = { name: 'Betty', age: 36 }
betty.brother = fred;
fred.sister = betty;

JsonTree.stringify([fred, betty])
//[[[1,6]],[2,3],"Object",{"name":4,"age":5,"sister":6},"Fred",36,[2,7],{"name":8,"age":5,"brother":1},"Betty"]

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",{"name":3,"age":4},"Fred",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",{"name":3,"age":4},"Fred",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,6]],[2,3],"Person",{"name":4,"age":5,"sister":9},"Fred",36,[2,11],{"name":8,"age":5,"brother":1},"Betty"]

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",{"name":4,"age":5,"brother":-1},"Betty",32]

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

Flattening Object property names

By default Object property names are not flattened, but you may get better "compression" by turning flattenPropertyNames on. With the option turned off (default) Object property names are embedded into each keyed object, eg

var fred = { name: 'Fred', age: 36 }

JsonTree.stringify(fred);
//[[1,2],"Object",{"name":3,"age":4},"Fred",36]					// 45 characters

with the option turned on, object property names are also flattened into the root ..

var fred = { name: 'Fred', age: 36 }

JsonTree.stringify(fred, {
	flattenPropertyNames: true
});
//[[1,2],"Object",{"3":4,"5":6},"name","Fred","age",36]			// 53 characters

in the above simple example, the keying overhead actually makes the resulting string longer (53 compared to 45), but with another couple objects of same type being flattened ..

var fred = { name: 'Fred', age: 36 }
var betty = { name: 'Betty', age: 32 }
var wilma = { name: 'Wilma', age: 39 }

JsonTree.stringify([fred, betty, wilma]);
//[[[1,6,10]],[2,3],"Object",{"name":4,"age":5},"Fred",36,[2,7],{"name":8,"age":9},"Betty",32,[2,11],{"name":12,"age":12},"Wilma",39]		// 131 characters

And the flattenPropertyNames option turned on ..

var fred = { name: 'Fred', age: 36 }
var betty = { name: 'Betty', age: 32 }
var wilma = { name: 'Wilma', age: 39 }

JsonTree.stringify([fred, betty, wilma], {
	flattenPropertyNames: true
});
//[[[1,8,10]],[2,3],"Object",{"4":5,"6":7},"name","Fred","age",36,[2,9],{"4":8,"6":9},"Betty",32,[2,11],{"4":12,"6":13},"Wilma",39]			// 129 characters

the resulting string is 2 characters shorter, the more similar objects flattened and the compression will get better - the obvious cost to this feature is the resulting string is far more difficult to decode for us mere humans

You should also note, that an object tree stringified with this option turned on should always be parsed with the option turned on aswell ..

var [ fred2, betty2, wilma2 ] = JsonTree.parse('[[[1,8,10]],[2,3],"Object",{"4":5,"6":7},"name","Fred","age",36,[2,9],{"4":8,"6":9},"Betty",32,[2,11],{"4":12,"6":13},"Wilma",39]', {
	flattenPropertyNames: true
})