Limits of JSON Round‑Trip in p5.js: Preserving Complex Object Behavior
0 reputation · 23 Mar 2026, 13:40 UTC
0 reputation · 23 Mar 2026, 13:40 UTC
To reliably export sketch state with saveJSON() and restore it with loadJSON() while keeping methods, prototypes, and p5.Vector behavior intact.
loadJSON() is asynchronous; using its return value synchronously can lead to undefined data.saveJSON()/loadJSON() cycle?30775 reputation · 23 Mar 2026, 20:37 UTC
p5.js does not provide a built-in mechanism to preserve class methods, prototypes, or p5.Vector behavior across a saveJSON()/loadJSON() cycle, and that is not a bug — it is a property of JSON itself. The documented, reliable pattern is: serialize only plain data (with a type marker), load it asynchronously, then explicitly rebuild your objects with createVector(), constructors, or Object.setPrototypeOf().
Confirmed by the JSON specification: JSON.stringify keeps only strings, numbers, booleans, null, arrays, and plain objects. Functions, undefined, class identity, and prototype links are silently dropped or converted. A p5.Vector is not special to JSON — after a round trip you get a plain object with x/y/z-like properties, and calls like restored.add() or restored.mag() will throw because those methods live on the prototype that was never saved.
Likely explanation for "undefined data" symptoms: loadJSON() is asynchronous outside preload(). If you read its result synchronously, you are reading before the file has loaded. Use it in preload(), or pass a success callback and run all restore logic inside it.
Give each serializable class a toJSON() that emits minimal fields plus a type tag, and a static fromJSON() that rebuilds it:
class Particle {
constructor(pos, vel) { this.pos = pos; this.vel = vel; }
speed() { return this.vel.mag(); }
toJSON() {
return { __type: 'Particle',
pos: { x: this.pos.x, y: this.pos.y },
vel: { x: this.vel.x, y: this.vel.y } };
}
static fromJSON(d) {
return new Particle(createVector(d.pos.x, d.pos.y),
createVector(d.vel.x, d.vel.y));
}
}
const registry = { Particle };
function revive(key, value) {
if (value && registry[value.__type]) {
return registry[value.__type].fromJSON(value);
}
return value;
}
// inside preload() or the loadJSON callback:
const state = JSON.parse(JSON.stringify(raw), revive);
Revivers run bottom-up, so nested children are already plain data when the parent's reviver sees them — rebuild children first or let the registry handle each level. Note that Object.assign({}, saved) copies data only, never behavior; structuredClone has the same limitation for custom class prototypes.
Separate durable state from runtime resources. Save positions, velocities (as numbers), seeds, and configuration. Recreate renderers, images, sounds, fonts, and DOM elements after load — they are not meaningfully serializable and can produce bloated or broken exports.
Do not compare JSON.stringify output of original vs. restored — key order, dropped fields, and NaN/Infinity handling make that unreliable. Instead check semantics:
restored.pos instanceof p5.Vector — expect false right after plain load, true after rehydration.restored.pos.add(1, 2) and restored.speed() should work and return expected values.Object.getPrototypeOf(), vector components with float tolerance, and reports the first differing path.preload() and callback) and confirm nothing reads the data before the callback fires.General-purpose serialization libraries exist, but none integrate specially with p5.js — the explicit toJSON/fromJSON contract above is the standard recommendation and keeps you in control of what is durable. One caveat: exact loadJSON() signatures and p5.Vector internals vary across p5.js releases, so pin your p5.js version and rerun a small round-trip fixture after upgrades rather than assuming older examples transfer unchanged.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.