o2s2o
v1.0.0
Published
o2s2o: Object → Storage (JSON) → back to Object. Safe, readable de/serializer with class revival and built-ins.
Readme
o2s2o
Object → Storage (JSON) → back to Object.
A tiny, readable de/serializer that dehydrates class instances into JSON-safe data and hydrates them back to real instances — with support for built-ins (Date, URL, RegExp (flags kept), Map, Set, Error, BigInt).
Why?
- Keep your class methods after persisting to
localStorage, DB, or sending over the wire. - No magic: uses explicit envelopes (
handlerId,ctorName,data) for clarity. - Deterministic revival across ESM/CJS/minified builds using a registry or a
ctorMap.
Install
npm i o2s2o
# or
yarn add o2s2o
# or
pnpm add o2s2oQuick start
import Serializer from 'o2s2o';
import type { AutoHandler } from 'o2s2o';
const S = new Serializer();
class Point {
constructor(public x: number, public y: number) {}
len() { return Math.hypot(this.x, this.y); }
}
// Register your classes (version ids recommended)
S.register<Point>({ id: 'Point@1', ctor: Point });
const state = { p: new Point(3,4), when: new Date('2024-01-02T03:04:05Z') };
// Save
const json = S.stringify(state);
localStorage.setItem('app', json);
// Restore
const restored = S.parse<typeof state>(localStorage.getItem('app')!, {
ctorMap: { Point } // deterministic mapping by constructor name
});
restored.p instanceof Point; // true
restored.p.len(); // 5
restored.when instanceof Date; // trueAPI
new Serializer()
register<T>(handler: AutoHandler<T>)
Registers a class-type for auto dehydration/hydration.
id: stable string, versioned like"Point@1"ctor: the class constructorkeys?:(instance) => string[]— which own keys to persist (default:Object.keys(instance))construct?:(plain) => T— custom builder on hydration (default: create a blank object with the prototype andObject.assignthe hydrated fields)
dehydrateAny(value: any): any
Recursively converts any value to a JSON-safe shape. Class instances become envelopes:
{ handlerId: 'Point@1', data: { x: 3, y: 4 } }Non-registered classes become:
{ ctorName: 'ClassName', data: { ... } }Built-ins are handled out of the box.
hydrateAny(value: any, opts?: HydrationOptions): any
Rebuilds values back to live instances. Options:
type HydrationOptions = {
ctorMap?: Record<string, Constructor>; // deterministic ctor name -> ctor
coerceScalars?: boolean; // turns "true","42","null" -> proper types (off by default)
allowGlobalLookup?: boolean; // last-ditch globalThis lookup (off by default)
};stringify(obj: any): string / parse<T>(text: string, opts?: HydrationOptions): T
Friendly helpers around JSON.stringify/JSON.parse + (de)hydrate.
Built-ins supported
Date↔ ISO stringURL↔ stringRegExp↔{source, flags}Map↔ array of[key, value](both sides recursively processed)Set↔ array of valuesError↔{name, message, stack}BigInt↔ string
Patterns
Version your handlers
S.register<Point>({ id: 'Point@2', ctor: Point, construct: (p) => new Point(p.x, p.y) });Enforce invariants
class Money { constructor(public cents: number, public currency: 'USD'|'EUR') { if (!Number.isInteger(cents)) throw new Error('int'); } }
S.register<Money>({ id: 'Money@1', ctor: Money, keys: m => ['cents','currency'], construct: p => new Money(p.cents, p.currency) });Deterministic hydration across bundles
const restored = S.parse(json, { ctorMap: { Point, Money } });FAQ
Q: Does it support circular references?
No. JSON doesn’t either. You can layer an ID-based cycle encoder if needed.
Q: What about functions / symbols / undefined?
Same as JSON: they are dropped.
Q: Do I have to register built-ins?
No. They are included out of the box.
License
MIT © Alireza Tabatabaeian
