o2s2o
Version:
o2s2o: Object → Storage (JSON) → back to Object. Safe, readable de/serializer with class revival and built-ins.
130 lines (98 loc) • 3.99 kB
Markdown
# 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
```bash
npm i o2s2o
# or
yarn add o2s2o
# or
pnpm add o2s2o
```
## Quick start
```ts
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; // true
```
## API
### `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 constructor
- `keys?`: `(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 and `Object.assign` the hydrated fields)
### `dehydrateAny(value: any): any`
Recursively converts any value to a JSON-safe shape. Class instances become envelopes:
```ts
{ handlerId: 'Point@1', data: { x: 3, y: 4 } }
```
Non-registered classes become:
```ts
{ ctorName: 'ClassName', data: { ... } }
```
Built-ins are handled out of the box.
### `hydrateAny(value: any, opts?: HydrationOptions): any`
Rebuilds values back to live instances. Options:
```ts
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 string
- `URL` ↔ string
- `RegExp` ↔ `{source, flags}`
- `Map` ↔ array of `[key, value]` (both sides recursively processed)
- `Set` ↔ array of values
- `Error` ↔ `{name, message, stack}`
- `BigInt` ↔ string
## Patterns
### Version your handlers
```ts
S.register<Point>({ id: 'Point@2', ctor: Point, construct: (p) => new Point(p.x, p.y) });
```
### Enforce invariants
```ts
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
```ts
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](https://github.com/Alireza-Tabatabaeian)