Open-source project
WebReflection/flatted avatar
WebReflection/flatted

flatted: a 0.5K circular JSON parser that flattens objects into an index table

A fast and minimal circular JSON parser.

1,160 stars101 forksJavaScriptISC

At a glance

What is it?
flatted is a JavaScript library that serialises and deserialises circular JSON structures, using a flat index table in place of back-references. It ships as CommonJS, ESM, an unpkg build, a PHP port, a Python port and a Go port, and it deliberately refuses to be compatible with plain JSON.parse and JSON.stringify.
Who is it for?
Adopt flatted when you have a genuinely circular object graph and you need it to survive a round trip through a text format, because the index-table design handles repeated and self-referencing nodes without the recursion problems of a naive implementation, and the PHP, Python and Go ports let a service and a browser agree on the format.
Can I use it commercially?
Yes. ISC is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 6 days ago.
What is it written in?
Mainly JavaScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The problem: an object graph that JSON.stringify cannot express

JSON has no way to say that an object contains itself. The moment you build a structure where a node points back to an ancestor, `JSON.stringify` throws, and the usual workaround, a replacer function that rewrites the back-reference as an id string, spreads a custom id scheme through your own code and still breaks on anything with more than one reference cycle.

flatted is the library that automates that rewrite. Its description calls it a fast and minimal circular JSON parser, and the README says it comes directly from the creator of CircularJSON, with a PHP, a Python and a Go version available as well. The size claim is in the README: 0.5K, against a 0.5K-scale footprint rather than a library with a dependency tree.

The scope is deliberately narrow, and the README is unusually direct about where the boundary lies. flatted serialises and deserialises only data that is compatible with JSON, so anything with internal classes of its own, sockets for instance, will not survive the round trip. The README points to the Structured Clone polyfill for the case where you need more than plain JSON values, and calls that the standard approach for recursion and additional data types.

Who it is for is therefore a developer with a circular structure and a text transport. The most common case is a tree of objects that a UI library mutates in place, where a child can hold a reference to the parent, and the state has to be persisted, sent over a socket or written to a file.

How the index table replaces back-references

The mechanism is a flat list of values plus a table of indices. While stringifying, every object, and every array, and every string, is flattened out and replaced by its unique index, and the string form of that index is used so that it cannot be confused with a number in the data. Once parsed, the indices are resolved back through the flattened collection.

The README works through a small example to make the layout clear.

js
// logic example
var a = [{one: 1}, {two: '2'}];
a[0].a = a;
// a is the main object, will be at index '0'
// {one: 1} is the second object, index '1'
// {two: '2'} the third, in '2', and it has a string
// which will be found at index '3'

Flatted.stringify(a);
// [["1","2"],{"one":1,"a":"0"},{"two":"3"},"2"]
// a[one,two]    {one: 1, a}    {two: '2'}  '2'

Reading the output, the first element is the index table and the rest is the flattened collection. The main object sits at index `0`, and the array of two children records that they live at `1` and `2`. Inside the first child, the `a` property points at `"0"`, which is the main object again, and that is where the cycle is closed. The second child holds a string rather than an object, so the string itself is lifted into the collection and referenced by index `3`.

The consequence worth understanding is that the output is not JSON-shaped data. It is a table and a bag of values, which is exactly why the format is cheap to produce and cheap to consume, and exactly why it cannot be read by a standard parser.

Installing it and taking the first round trip

The package is on npm and the install is the usual one.

bash
npm i flatted

It is also usable through a CDN, according to the README, or as a regular module, and both module systems are covered by named exports.

js
// ESM
import {parse, stringify, toJSON, fromJSON} from 'flatted';

// CJS
const {parse, stringify, toJSON, fromJSON} = require('flatted');

const a = [{}];
a[0].a = a;
a.push(a);

stringify(a); // [["1","0"],{"a":"0"}]

The example is the smallest possible demonstration of the point. An array holds an object, the object points back at the array, and the array contains itself. The output is a two element array: the index table and one flattened object whose `a` property is the string `"0"`, pointing at the root. Round tripping it with `parse(stringify(a))` gives back the same circular structure.

The `parse` and `stringify` signatures match the JSON ones since version 1. The README lists the additions: `.parse(string, reviver)` takes a reviver so you can revive your own objects, and `.stringify(object, replacer, space)` takes a replacer and a space parameter for parity with the JSON signature. The `space` argument in particular means the output can be pretty printed, which is handy when you are trying to read a stored payload by hand.

Compatibility is stated in one line: any engine that supports `Map`, `Set`, `Object.keys` and `Array.prototype.reduce` will work, even if those are polyfilled.

toJSON and fromJSON for classes that hold their own state

Two helper functions exist for the case where the circular structure lives inside a class rather than in a plain object. `toJSON` prepares a value for an implicit JSON serialisation, and `fromJSON` reverses it, so a class can participate in `JSON.stringify` and `JSON.parse` without the caller knowing anything about the format.

The README shows the pattern with a `Map` subclass that holds a circular reference.

js
import {toJSON, fromJSON} from 'flatted';

class RecursiveMap extends Map {
  static fromJSON(any) {
    return new this(fromJSON(any));
  }
  toJSON() {
    return toJSON([...this.entries()]);
  }
}

const recursive = new RecursiveMap;
const same = {};
same.same = same;
recursive.set('same', same);

const asString = JSON.stringify(recursive);
const asMap = RecursiveMap.fromJSON(JSON.parse(asString));
asMap.get('same') === asMap.get('same').same;
// true

The mechanism is that `toJSON` is a method the JSON serialiser already knows how to call, so `JSON.stringify(recursive)` picks it up and produces a flatted payload. On the way back, `JSON.parse` yields the flatted structure and `fromJSON` rebuilds the graph, after which the class constructor runs again through `static fromJSON`.

The final assertion is the point of the whole exercise. After the round trip, `asMap.get('same')` is the same object as `asMap.get('same').same`, so the cycle is intact and the identity relationship survived, not merely a copy of it. For a class holding a back-reference to itself, that is the property you actually need and the reason a plain JSON round trip would not do.

The compatibility rule that will bite you if you skip it

The README spends a short section on something that looks like a detail and is in fact the main operational hazard: you should never call `JSON.parse` on the result of `Flatted.stringify(data)`, and you should never call `Flatted.parse` on the result of `JSON.stringify(data)`. The only combination that works is `Flatted.parse(Flatted.stringify(data))`.

The reason given is that this is true of every specialised format capable of serialising and deserialising circular data, and of CircularJSON and any other such format, and that otherwise there is no guaranteed data integrity. That is a structural claim rather than a caution. The flatted output is a table of indices and a bag of values, so feeding it to a standard parser produces either an error or a plausible-looking but wrong object graph, and the second case is the dangerous one.

The same section repeats the scope limit about JSON-only values, mentioning sockets and anything else with internal classes different from those the JSON standard allows. A class instance with methods is not a JSON value, and flatted will not carry it across a boundary in a way that preserves its behaviour.

If you are integrating this into an existing codebase, that gives you a concrete checklist. Every read path must use `parse` from flatted and every write path must use `stringify` from flatted, and a mixed pipeline, where one service writes with flatted and another reads with `JSON.parse`, will fail quietly rather than loudly.

One format, four languages, and a published specification

The repository goes beyond a JavaScript package. The README links a PHP version, a Python version and a Go version, and the top level listing shows the `php/`, `python/` and `golang/` directories alongside the JavaScript sources, with the published `files` list in `package.json` including `php/flatted.php`, `python/flatted.py` and `golang/pkg/flatted/flatted.go`. The `package.json` also has a `test:php` script running `php php/test.php` and a `test:py` script running `python python/test.py`, so the ports are exercised by the same project rather than left as untested ports.

That matters if your circular data crosses a service boundary. A Node process writing with flatted and a Go service reading it with the Go port agree on the format, and the same for PHP and Python. A format that only one language can read is a private encoding, and this one is deliberately not private.

The repository also contains a `SPECS.md` file, which is where the format description belongs if you want to implement or audit the format yourself rather than trust one of the ports, and a `SECURITY.md` for the disclosure process. There is a `jsr.json` at the top level, so the package is prepared for the JSR registry as well as npm, and a `types/` directory with TypeScript definitions plus a `ts` script that runs `tsc -p .`.

The build is more involved than the runtime. The `build` script chains a CommonJS conversion with `ascjs`, three Rollup bundles for the ES, ESM and Babel targets, a Terser pass producing `min.js`, and then the tests and a size measurement. The `size` script is a curiosity worth noting: it counts bytes for `index.js` and `min.js`, then gzips the minified file and brotli-compares several bundles, which is how the size claim in the README is kept honest.

Maintenance state and the licence

The licence is ISC, which is short and permissive, and the README links the ISC text. The `files` array in `package.json` publishes the JavaScript builds, the PHP, Python and Go ports, the type definitions and the licence, so the ports travel with the npm package rather than needing a separate fetch.

On maintenance, the facts are modest. The last push to the repository was on 2026-07-30, and the repository is not archived. There are no GitHub releases, so the version history lives in npm, where `package.json` records `3.4.4`. The project has been shipping for long enough that the version number carries real weight, but the absence of GitHub releases means you cannot read a changelog in the repository to see what changed in a given version.

The ecosystem requirement is the other practical constraint. The compatibility statement puts the floor at any engine with `Map`, `Set`, `Object.keys` and `Array.prototype.reduce`, polyfills permitted, which covers current browsers and servers comfortably. The `min.js` file is what the unpkg field points at, so a script tag consumer gets the minified build, and the size script measures exactly that file.

Editorial conclusion

Adopt flatted when you have a genuinely circular object graph and you need it to survive a round trip through a text format, because the index-table design handles repeated and self-referencing nodes without the recursion problems of a naive implementation, and the PHP, Python and Go ports let a service and a browser agree on the format. Do not adopt it as a general JSON replacement, since the README is explicit that its output must not be fed to JSON.parse and that it only handles values JSON itself understands. Before you depend on it, check that your graph contains no internal class instances such as sockets, because the README warns those will not survive serialisation, and pin a version, since the repository publishes no GitHub releases and the npm version in `package.json` is the only version marker you get.

Frequently asked questions

How do I install flatted and stringify a circular object?

Run `npm i flatted`, then import `stringify` and call it on the structure. The README example creates an array, points a member back at it and pushes the array into itself, and `stringify(a)` returns `[["1","0"],{"a":"0"}]`.

Can I read flatted output with JSON.parse?

No. The README says you should never call `JSON.parse` on the result of `Flatted.stringify(data)`, and the only combination with guaranteed data integrity is `Flatted.parse(Flatted.stringify(data))`, which is the same rule that applies to CircularJSON and other specialised circular formats.

Does flatted work with class instances and Maps?

The `toJSON` and `fromJSON` helpers exist for that case. The README shows a `Map` subclass whose `toJSON` returns `toJSON([...this.entries()])` and whose `static fromJSON` rebuilds the instance, so `JSON.stringify` and `JSON.parse` can carry a circular structure held inside a class.

Which languages have a flatted port?

The README links PHP, Python and Go versions in addition to the JavaScript package, and `package.json` publishes `php/flatted.php`, `python/flatted.py` and `golang/pkg/flatted/flatted.go`, with `test:php` and `test:py` scripts to run their test suites.

What data can flatted not serialise?

Only data compatible with JSON. The README warns that sockets, or anything else with internal classes different from those the JSON standard allows, will not be serialised and deserialised as expected, and points to the Structured Clone polyfill for recursion and extra data types.

Which JavaScript engines can run flatted?

The README states that all ECMAScript engines compatible with `Map`, `Set`, `Object.keys` and `Array.prototype.reduce` will work, even if those features are polyfilled. The package also ships an ESM build, a CommonJS build and a minified file used as the unpkg entry.

Official sources

  1. Issues
  2. License: ISC
  3. README
  4. WebReflection/flatted on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/webreflection-flatted.svg)](https://hysenlabs.com/projects/webreflection-flatted)

Was this useful?