json-registry
v0.2.1
Published
A JSON registry for custom types
Readme
JSONRegistry
⚠️ highly experimental and discussed as ECMAScript proposal
A registry for teaching JSON.stringify() and JSON.parse() how to
round-trip application-specific values.
JSON already supports replacers and revivers, but coordinating them across a
project quickly becomes repetitive: every custom type needs a stable tag, a
predicate, a serializer, and a parser. json-registry keeps those rules in one
place and applies them consistently.
Why
Plain JSON has no representation for values such as typed arrays, class
instances, bigint, or symbol. Those values are either lost, rejected, or
turned into plain objects unless the caller provides custom logic.
JSONRegistry solves that by encoding registered values as tagged JSON payloads
and restoring them during parsing:
import JSONRegistry from 'json-registry';
class Test {
constructor(value) {
this.value = value;
}
}
const { parse, stringify } = new JSONRegistry([
['Uint8Array', {
is: value => value instanceof Uint8Array,
to: value => Array.from(value),
from: value => Uint8Array.from(value),
}],
['Test', {
is: value => value instanceof Test,
to: value => ({ value: value.value }),
from: value => new Test(value.value),
}],
]);
const text = stringify({
bytes: new TextEncoder().encode('hello'),
test: new Test('ok'),
});
parse(text).bytes instanceof Uint8Array; // true
parse(text).test instanceof Test; // trueEncoding Strategy
Registered values are encoded as single-key objects whose key is the registry name:
stringify(123n); // {"bigint":"123"} when bigint is registeredPlain non-array objects are also wrapped, using the reserved object key:
stringify({ a: [] }); // {"object":{"a":[]}}Arrays are not wrapped. They remain regular JSON arrays and are used only as containers for values that may themselves be registered or plain object wrappers. This keeps array traversal native to the underlying JSON implementation:
stringify([{}, []]); // [{"object":{}},[]]During parsing, wrappers are restored through the native bottom-up reviver flow.
A single-key object is treated as a wrapper only when that key is object or a
registered name and the surrounding parse context confirms it is a payload
carrier. This lets plain data such as { object: [1, 2, 3] } or
{ bigint: "123" } round-trip without being mistaken for tagged values.
Each registry entry is a tuple of [name, record], where record contains:
is(value): returnstruewhen the value should use this record.to(value): converts the value into JSON-compatible data.from(value): restores the value from the JSON-compatible data.
Registry names must be unique strings.
API
const registry = new JSONRegistry(entries);The constructor can also be called as a function. It returns an object with:
register(name, record): add another record after creation.stringify(value, replacer, space): likeJSON.stringify(), with registry support.parse(text, reviver): likeJSON.parse(), with registry support.recursive(json): adapt another JSON-compatible implementation, such as flatted, so registered values and circular references can be handled together.
Recursive JSON
recursive({ parse, stringify }) accepts an object with parse(text, reviver) and
stringify(value, replacer, space) methods:
import * as flatted from 'flatted';
import JSONRegistry from 'json-registry';
class Item {
constructor(value) {
this.value = value;
}
}
const registry = new JSONRegistry([
['Item', {
is: value => value instanceof Item,
to: value => value.value,
from: value => new Item(value),
}],
]);
const recursive = registry.recursive(flatted);
const value = { item: new Item('ok') };
value.self = value;
value.list = [value, value.item];
const text = recursive.stringify(value);
const copy = recursive.parse(text);
copy.self === copy; // true
copy.list[0] === copy; // true
copy.item instanceof Item; // true
copy.list[1] instanceof Item; // trueAbout JSON limitations
toJSON()runs before replacers. If a value definestoJSON(),JSON.stringify()passes the result oftoJSON()to the registry, not the original value. Remove or account fortoJSON()when a registered type must be preserved exactly. For built-ins such asDate, shadow the method with a non-callable value so the registry replacer can see the instance:class RegistryDate extends Date { toJSON = undefined; } const registry = new JSONRegistry([ ['Date', { is: value => value instanceof RegistryDate, to: value => value.toISOString(), from: value => new RegistryDate(value), }], ]);Returning
undefinedfrom atoJSON()method is not equivalent: it tells nativeJSON.stringify()to omit that value before the registry can handle it.Symbol keys are ignored by
JSON.stringify()itself, so they never reach the replacer and cannot be preserved as object keys. Symbols can still be registered and transformed when they appear as values.Registered
from(value)functions must return a non-nullish value. Returningnullorundefinedis treated as an invalid tagged value and throws aTypeError.Registry tags are stored in the JSON payload. When parsing data from outside your application, keep
from(value)functions strict: validate the payload shape and throw when it does not match the expected type.objectis reserved for plain object wrappers and cannot be used as a registry name. Arrays are never registry wrappers.
