Custom Serialization
Sometimes a type should have its own wire format instead of the generated object form — a compact string, a backward-compatible layout, or a custom encoding for a built-in container. Add a @serializer / @deserializer pair to take over both directions.
The contract
- The serializer receives the instance and must return a valid JSON string.
- The deserializer receives the raw JSON string and must return a fresh instance (never mutate or assume a reused one).
- The optional decorator argument declares the JSON shape the type maps to. It defaults to
"any"; a narrower hint ("string","number", …) lets the transform generate tighter surrounding code.
Shape hints: "any", "string", "number", "boolean", "object", "array", "null", and nullable forms like "string | null".
Example: a point as "x,y"
import { JSON } from "json-as";
@json
class Point {
x: f64 = 0;
y: f64 = 0;
constructor(x: f64 = 0, y: f64 = 0) {
this.x = x;
this.y = y;
}
// Serialize the whole instance to a single JSON string.
@serializer("string")
serializer(self: Point): string {
return JSON.stringify(`${self.x},${self.y}`);
}
// ...and back. Always return a new instance.
@deserializer("string")
deserializer(data: string): Point {
const raw = JSON.parse<string>(data);
const c = raw.indexOf(",");
return new Point(f64.parse(raw.slice(0, c)), f64.parse(raw.slice(c + 1)));
}
}
JSON.stringify(new Point(3.5, -9.2)); // '"3.5,-9.2"'
const p = JSON.parse<Point>('"3.5,-9.2"');
p.x; // 3.5A type with a custom format works anywhere a normal @json type does — including as a field of another class:
@json
class Shape {
name: string = "";
origin: Point = new Point();
}
JSON.stringify(JSON.parse<Shape>('{"name":"s","origin":"1,2"}'));
// '{"name":"s","origin":"1.0,2.0"}'Calling parse / stringify inside
Notice the serializer above calls JSON.stringify(...) and the deserializer calls JSON.parse<string>(...). That's fine and encouraged — the transform rewrites those nested calls to JSON.internal.stringify / JSON.internal.parse so they don't clobber the shared buffer state of the outer (de)serialize that invoked them.
Built-in container subclasses
The most common use is giving a subclass of a built-in container a compact representation — e.g. a Uint8Array as a hex string, an Array<i32> as CSV, or a Map/Set as tagged text. See Built-in Subclasses for how decorating a subclass works.
Not combinable with lazy fields
A class with a custom @serializer/@deserializer can't also use lazy fields — the custom methods replace the generated (de)serializer that the lazy slots rely on, so the transform reports an error. (A field whose type has a custom serializer is fine.)
