JSON
elements man json Read as markdownElements ships with its own json serializer. The output is valid json so
anything that speaks json can read it, but the format also carries class
identity, cycles, and repeated references. Standard json doesn't handle
any of those. A class like User arrives on the browser as a plain
object, without instanceof User or any methods. A graph containing a
cycle throws. Shared subgraphs serialize their inner objects over and
over.
Elements json handles all three. A class marked with the @json
decorator round-trips with its identity intact: the same User instance
leaves the server and arrives on the browser as a User instance.
Repeated references serialize once and back-reference on every subsequent
appearance. Cycles round-trip without infinite recursion. Date, Map,
Set, LiveTable, Listener, and File all have built-in handlers
that preserve their type.
Every data boundary in Elements uses this serializer automatically. You
don't call stringify() yourself to send data across the wire; Elements
does it wherever data crosses a boundary:
@rpcarguments and return values.- Route handler returns from
route()to the html page. - Plain objects and arrays returned from api route handlers.
Channel.notify(...)payloads and listener messages.LiveTablebroadcasts.
Mark a custom class @json and its instances flow through any of those
paths with class identity restored on the other side. You can also call
the helpers directly when you need a string in hand.
import { stringify, parse } from "@elements/app";
let s = stringify(value); // EJSON string
let v = parse(s); // value with class identity restored
At a Glance
import { json, stringify, parse } from "@elements/app";
json
class User {
id: string;
name: string;
createdAt: Date;
constructor(id: string = "", name: string = "", createdAt: Date = new Date()) {
this.id = id;
this.name = name;
this.createdAt = createdAt;
}
}
let user = new User("u1", "alice", new Date());
let s = stringify(user); // EJSON string
let u: User = parse(s); // round-trips with class identity intact
u instanceof User; // true
u.createdAt instanceof Date; // true
The @json Decorator
@json is a TypeScript decorator. Apply it to any class you want to
round-trip through Elements json with its class identity intact.
import { json } from "@elements/app";
json
class Comment {
id: string;
text: string;
author: string;
}
Without @json, a class instance serializes as a plain object on the
wire. The browser receives a plain object, not an instance of the class.
For plain data shapes that's exactly what you want, and you don't need
the decorator. Use @json when you want class identity preserved.
The decorator registers the class by name in a global registry. The
serializer emits { "$type": "<class name>", "$value": { ... } }. The
deserializer looks up the class and reconstructs the instance.
What the Serializer Handles
null,undefined,boolean,number,string: passed through.Date: emitted as{ "$type": "Date", "$value": <epoch millis> }.Map,Set: emitted with their entries preserved.Array,Object: recursive.- Repeated reference: emitted as
{ "$type": "ref", "$value": "<path>" }. The serializer emits the value once and refs it on subsequent appearances. - Cycles: resolved via
ref. A node referencing itself, directly or transitively, round-trips without infinite recursion. @json-decorated class instances: emitted as{ "$type": "<name>", "$value": { ... } }.Listener,LiveTable,File: built-in types with their own handlers.
Custom Serialization
For classes that need control over their wire shape, implement the
[ToJSON] and [FromJSON] symbols.
import { json, ToJSON, FromJSON, JSONType } from "@elements/app";
json
class Money {
cents: number;
currency: string;
constructor(cents: number = 0, currency: string = "USD") {
this.cents = cents;
this.currency = currency;
}
[ToJSON]() {
return { c: this.cents, ccy: this.currency };
}
static [FromJSON](value: { c: number; ccy: string }): Money {
return new Money(value.c, value.ccy);
}
static [JSONType]() {
return "Money";
}
}
[ToJSON](): instance method that returns the wire shape. The default copies own properties.[FromJSON](/learn/man/value): static method that reconstructs the instance. The default doesObject.assign(new Class(), value).[JSONType](): static method that returns the type name used on the wire. The default is the class name.
Once the class is registered via @json, the symbols are picked up
automatically.
Helpers
import { stringify, parse, serialize, deserialize } from "@elements/app";
let s = stringify(value); // EJSON string
let v = parse(s); // round-trip
let s2 = serialize(value, replacer, " "); // JSON.stringify-style options
let v2 = deserialize(s2);
stringify and serialize are aliases. parse and deserialize are aliases.
The replacer and space arguments mirror JSON.stringify. The
replacer runs after Elements has computed the type tag, so it sees the
typed shape.
To pass a space (indent) without a replacer, pass undefined for the
replacer, not null:
let pretty = stringify(value, undefined, " "); // indented, no replacer
Attach
After deserialization, classes that need post-init wiring implement an
[Attach]() method. The deserializer calls [Attach]() on each
instance after the entire graph has been reconstructed and references are
resolved.
Elements' built-in types (Listener, LiveTable) use this to connect
to the runtime. Application classes can implement it too if they need
the same kind of post-deserialization setup.
Related
rpc: arguments and return values are serialized with this.router: route returns to html pages and api route returns both go through this.livetable: broadcasts use this serializer.channel: notify payloads use this serializer.