Custom Types
The Crous tagged type system lets you register custom serializers and decoders for your own classes and objects. This enables type-preserving round-trips for application-specific types.
How It Works
Crous uses numeric tags (integers ≥ 100) to identify custom types in the binary stream. You register a serializer that converts your object to a Crous-native type, and a decoder that reconstructs it.
Registering a Serializer
const crous = require('crous');
class Vector3 {
constructor(x, y, z) {
this.x = x;
this.y = y;
this.z = z;
}
}
// Register a serializer for Vector3
// The tag is auto-assigned starting from 100
crous.registerSerializer(
Vector3, // constructor to match
(vec) => [vec.x, vec.y, vec.z] // serialize to array
);
// Now Vector3 instances can be serialized!
const data = { position: new Vector3(1, 2, 3) };
const buffer = crous.dumps(data);
console.log(`Encoded: ${buffer.length} bytes`);Auto-Tag Assignment
Registering a Decoder
const crous = require('crous');
class Vector3 {
constructor(x, y, z) {
this.x = x;
this.y = y;
this.z = z;
}
magnitude() {
return Math.sqrt(this.x ** 2 + this.y ** 2 + this.z ** 2);
}
}
// Register decoder for tag 100 (the first custom type)
crous.registerDecoder(
100, // tag number
(arr) => new Vector3(arr[0], arr[1], arr[2]) // reconstruct from array
);
// Full round-trip
crous.registerSerializer(Vector3, (v) => [v.x, v.y, v.z]);
const original = new Vector3(3, 4, 0);
const buffer = crous.dumps(original);
const restored = crous.loads(buffer);
console.log(restored instanceof Vector3); // true
console.log(restored.magnitude()); // 5Complete Round-Trip Example
const crous = require('crous');
// ─── Define custom types ─────────────────────────
class Color {
constructor(r, g, b, a = 1.0) {
this.r = r; this.g = g;
this.b = b; this.a = a;
}
toHex() {
const hex = (v) => Math.round(v * 255).toString(16).padStart(2, '0');
return `#${hex(this.r)}${hex(this.g)}${hex(this.b)}`;
}
}
class Transform {
constructor(position, rotation, scale) {
this.position = position;
this.rotation = rotation;
this.scale = scale;
}
}
// ─── Register serializers ────────────────────────
crous.registerSerializer(Color, (c) => ({
r: c.r, g: c.g, b: c.b, a: c.a
}));
crous.registerSerializer(Transform, (t) => ({
pos: t.position,
rot: t.rotation,
scl: t.scale
}));
// ─── Register decoders ──────────────────────────
crous.registerDecoder(100, (d) =>
new Color(d.r, d.g, d.b, d.a)
);
crous.registerDecoder(101, (d) =>
new Transform(d.pos, d.rot, d.scl)
);
// ─── Use them! ──────────────────────────────────
const scene = {
background: new Color(0.1, 0.1, 0.2),
player: {
color: new Color(1, 0, 0),
transform: new Transform(
[0, 1, 0], // position
[0, 0, 0, 1], // rotation (quaternion)
[1, 1, 1] // scale
)
}
};
const buffer = crous.dumps(scene);
const loaded = crous.loads(buffer);
console.log(loaded.background.toHex()); // #1a1a33
console.log(loaded.player.color instanceof Color); // true
console.log(loaded.player.transform.position); // [0, 1, 0]Unregistering Types
const crous = require('crous');
class Temporary {
constructor(val) { this.val = val; }
}
// Register
crous.registerSerializer(Temporary, (t) => t.val);
crous.registerDecoder(100, (v) => new Temporary(v));
// Later, unregister
crous.unregisterSerializer(Temporary);
crous.unregisterDecoder(100);
// Now Temporary objects will throw CrousEncodeError
// and tag 100 will throw CrousDecodeErrorConstructor-Based Lookup
Serializer registration uses the object's constructor for matching. This means subclasses need their own registration unless you want them to use the parent's serializer.
const crous = require('crous');
class Shape {
constructor(type) { this.type = type; }
}
class Circle extends Shape {
constructor(radius) {
super('circle');
this.radius = radius;
}
}
// Only registering Shape
crous.registerSerializer(Shape, (s) => ({ type: s.type }));
// Circle won't match — it has a different constructor!
try {
crous.dumps(new Circle(5));
} catch (e) {
console.log(e.message); // encode error
}
// Register Circle separately
crous.registerSerializer(Circle, (c) => ({
type: c.type, radius: c.radius
}));
crous.dumps(new Circle(5)); // ✓ works nowNo MRO Lookup
Tag Numbering
| Tag Range | Purpose |
|---|---|
| 0–89 | Reserved — core Crous types |
| 90 | Set (built-in tagged type) |
| 91–99 | Reserved for future built-in types |
| 100+ | User-defined custom types |
Tag Consistency
Cross-SDK Custom Types
Custom types are fully interoperable between the Python and Node.js SDKs. As long as both sides register the same tag number with compatible serialization formats, data flows seamlessly.
// Node.js side — reading data written by Python
const crous = require('crous');
// Python registered: crous.register_serializer(
// datetime, 100, lambda dt: dt.isoformat()
// )
// Node.js decoder for the same tag
crous.registerDecoder(100, (isoString) => new Date(isoString));
// Load a file written by Python
const data = crous.load('timestamps.crous');
console.log(data.created instanceof Date); // true