How to add a serialization method to custom JavaScript classes
April 20, 2025 by Andrew Dawes

What is serialization?
In programming, serialization is the art of transmuting a living, breathing stateful entity into a static and statuesque facsimile of itself. It is like cryostasis for objects. Or a painted portrait of a live model.
The benefit of carbon-freezing your objects is that you can easily transport them. By turning them into simple bits and bytes, you can move them around from application to application and network to network.
What is a serialization method?
A serialization method is behavior you (or someone else) has built into a class making it possible for that class to someday be serialized. A serialization method defines the raw DNA sequencing of an object. Any code charged with surgically deconstructing your object and putting its pieces on ice will use your object’s serialization method as a guide for where to cut, what parts to extract and keep, and which pieces to throw away.
Serialization in JavaScript classes
In JavaScript, objects are most often serialized to a format called JavaScript Object Notation – or “JSON”.
You can make an object serializable by gifting it with a toJSON()
method.
The method should describe what shape the object will have once transformed into JSON. It should return all properties and values intended for inclusion in the serialized data – and anything necessary for creating a new object from this frozen body. Anything not listed by this method will be discarded once the object is serialized. This also means that any object later reconstructed would be missing those excluded properties – so be careful not to accidentally amputate anything critical!
The toJSON()
method should not be confused with the toString()
method, which is used to represent or “print” an object and is not used in the cycle of deconstructing and reconstructing objects.
Any object with a toJSON()
method can be passed as an argument to JSON.stringify()
in order to return a serialized version of itself.
An example
interface BTreeSerializableInterface<T> {
t: number;
root: BTreeNodeSerializableInterface<T>;
}
interface BTreeNodeSerializableInterface<T> {
isLeaf: boolean;
keys: T[];
children: BTreeNodeSerializableInterface<T>[];
}
interface BTreeNodeInterface<T> {
findIndex(key: T): number;
toJSON(): BTreeNodeSerializableInterface<T>;
toString(): string;
}
interface BTreeInterface<T> {
search(node: BTreeNodeInterface<T>, key: T): boolean;
contains(key: T): boolean;
insert(key: T): void;
toJSON(): BTreeSerializableInterface<T>;
toString(): string;
}
class BTreeNode<T>
implements BTreeNodeInterface<T>, BTreeNodeSerializableInterface<T>
{
public isLeaf: boolean;
public keys: T[];
public children: BTreeNode<T>[];
constructor(isLeaf = false) {
this.isLeaf = isLeaf;
this.keys = [];
this.children = [];
}
findIndex(key: T) {
let i = 0;
while (i < this.keys.length && key > this.keys[i]) {
i++;
}
return i;
}
toJSON(): BTreeNodeSerializableInterface<T> {
return {
isLeaf: this.isLeaf,
keys: this.keys,
children: this.children.map((child) => child.toJSON()),
};
}
toString() {
return 'BTreeNode here - I am overridding the toString() method!';
}
static fromJSON<T>(json: BTreeNodeSerializableInterface<T>) {
const node = new BTreeNode<T>(json.isLeaf);
node.keys = json.keys;
node.children = json.children.map(BTreeNode.fromJSON<T>);
return node;
}
}
class BTree<T> implements BTreeInterface<T>, BTreeSerializableInterface<T> {
public t: number;
public root: BTreeNode<T>;
constructor(t = 2) {
this.t = t;
this.root = new BTreeNode<T>(true);
}
search(node: BTreeNode<T>, key: T): boolean {
let i = node.findIndex(key);
if (i < node.keys.length && node.keys[i] === key) {
return true;
}
if (node.isLeaf) {
return false;
}
return this.search(node.children[i], key);
}
contains(key: T) {
return this.search(this.root, key);
}
insert(key: T) {
const root = this.root;
if (root.keys.length === 2 * this.t - 1) {
const newRoot = new BTreeNode<T>(false);
newRoot.children.push(root);
this.splitChild(newRoot, 0);
this.insertNonFull(newRoot, key);
this.root = newRoot;
} else {
this.insertNonFull(root, key);
}
}
private insertNonFull(node: BTreeNode<T>, key: T) {
let i = node.keys.length - 1;
if (node.isLeaf) {
while (i >= 0 && key < node.keys[i]) {
i--;
}
node.keys.splice(i + 1, 0, key);
} else {
while (i >= 0 && key < node.keys[i]) {
i--;
}
i++;
if (node.children[i].keys.length === 2 * this.t - 1) {
this.splitChild(node, i);
if (key > node.keys[i]) {
i++;
}
}
this.insertNonFull(node.children[i], key);
}
}
private splitChild(parent: BTreeNode<T>, i: number) {
const fullChild = parent.children[i];
const newChild = new BTreeNode<T>(fullChild.isLeaf);
const t = this.t;
newChild.keys = fullChild.keys.splice(t);
const midKey = fullChild.keys.pop();
if (undefined === midKey) {
return;
}
if (!fullChild.isLeaf) {
newChild.children = fullChild.children.splice(t);
}
parent.children.splice(i + 1, 0, newChild);
parent.keys.splice(i, 0, midKey);
}
toJSON() {
return {
t: this.t,
root: this.root.toJSON(),
};
}
toString() {
return 'BTree here - I am overridding the toString() method!';
}
static fromJSON<T>(json: BTreeSerializableInterface<T>) {
const tree = new BTree<T>(json.t);
tree.root = BTreeNode.fromJSON<T>(json.root);
return tree;
}
}
// Instantiate the object
const btree = new BTree(3);
// Give the object breath and life - aka, "state"
[5, 26, 12, 883, 1124, 64, 12, 92349, 123, 5233, 775, 23].forEach((item) =>
btree.insert(item)
);
// Serialize the object
console.log(JSON.stringify(btree));
// Prints: {"t":3,"root":{"isLeaf":false,"keys":[26,883],"children":[{"isLeaf":true,"keys":[5,12,12,23],"children":[]},{"isLeaf":true,"keys":[64,123,775],"children":[]},{"isLeaf":true,"keys":[1124,5233,92349],"children":[]}]}}
// Print the object
console.log(new String(btree));
// Prints: [String: 'BTree here - I am overridding the toString() method!']