Summary
This proposal adds a dedicated, lightweight API that mirrors the existing registerClass pattern but uses class’s defined toSuperJSON/fromSuperJSON methods.
Motivation
current implementation of class registry ask for allowedProps to include them in serialization:
import { registerClass } from 'superjson';
class User {
constructor(public name: string, public createdOn: Date) {}
}
registerClass(User, {
identifier: 'User',
allowProps: ['name', 'createdOn'],
});
then re-create instance using Object.assign. This approach works in most cases but has some limitations, as discussed in issue #331, also if class needs to run constructor to initialize some properties or if we want to add fields in our json that are not present as a property in the class.
I think that allow passing explicit to/from methods will solve these problems entirely and add flexible control of serialization, so earlier code becomes:
import { registerSerializableClass, SuperJSONValue } from 'superjson';
class User {
constructor(public name: string, public createdOn: Date) {}
static fromSuperJSON(json: SuperJSONValue) {
return new User(json.name, json.createdOn);
}
toSuperJSON(): SuperJSONValue {
return { name: this.name, createdOn: this.createdOn };
}
}
registerSerializableClass(User, 'User');
More advanced usage (constructor side-effects, computed fields, json data wrappers):
class User {
ctorInitializedProp: unknown;
constructor(public name: string, public createdOn: Date) {
// Initialize property
ctorInitializedProp = 'some value';
// Some side effect on creation
sideEffect();
}
static fromSuperJSON(json: SuperJSONValue) {
const { data } = json;
return new User(data.name, data.createdOn);
}
toSuperJSON(): SuperJSONValue {
return {
label: 'USER',
date: new Date(),
data: { name: this.name, createdOn: this.createdOn },
};
}
}
As class registry it works for nested classes as well:
class Admin {
constructor(public adminId: string, user: User) {} // Our earlier serializable class
static fromSuperJSON(json: SuperJSONValue) {
return new Admin(json.adminId, json.user);
}
toSuperJSON() {
return { adminId: this.adminId, user: this.user };
}
}
This completely optional for devs who need more control, as change is additive only (extra single composite Rule).
Proposed API
interface SerializableClass {
static fromSuperJSON(json: SuperJSONValue): InstanceType<this>;
new (...args: any[]): { toSuperJSON(): SuperJSONValue };
}
SuperJSON.registerSerializableClass(Class);
SuperJSON.registerSerializableClass(Class, 'id'); // or with custom identifier
(Exact same style as the existing registerClass and registerSymbol.)
Example
import { SuperJSON } from 'superjson';
// Following convention so our classes can be serialized by other external libs if needed
class JsonSerializable {
static fromJSON(json: any) {
return SuperJSON.deserialize(json);
}
toJSON() {
return SuperJSON.serialize(this);
}
}
class User extends JsonSerializable {
constructor(public name: string, public createdOn: Date) {
super();
}
static fromSuperJSON(json: any) {
return new User(json.name, json.createdOn);
}
toSuperJSON() {
return { name: this.name, createdOn: this.createdOn };
}
}
SuperJSON.registerSerializableClass(User);
const user = new User('user-123', new Date());
const superJsonstringifiedUser = SuperJSON.stringify(user);
// → {"json":{"name":"user-123","createdOn":"2026-03-16T01:36:57.217Z"},"meta":{"values":[["serializable-class","User"],{"createdOn":["Date"]}],"v":1}}
const superJsonrevivedUser = SuperJSON.parse(superJsonstringifiedUser);
// → User instance
const jsonStringifiedUser = JSON.stringify(user);
// → {"json":{"name":"user-123","createdOn":"2026-03-16T01:36:57.217Z"},"meta":{"values":[["serializable-class","User"],{"createdOn":["Date"]}],"v":1}}
const jsonRevivedUser = User.fromJSON(JSON.parse(jsonStringifiedUser));
// → User instance
Implementation notes
I already have a minimal, production-ready patch:
- New file:
serializable-class-registry.ts (modeled exactly after class-registry.ts)
- New
registerSerializableClass in index.ts.
- New
serializable-class annotation + rule in transformer.ts (identical structure to classRule)
- One-line update in
plainer.ts for deep traversal
- 5 new tests (
works for serializable class, works for nested serializable class, constructor side-effects & initialization, external json props in serializable classes and throw if non-serializable class is passed to registerSerializableClass)
The change is purely additive, does not touch any existing behavior, and reuses all existing patterns, types, and error messages.
I will create the PR after this discussion 😄
Note
Initially toSuperJSON/fromSuperJSON were named toJSON/fromJSON, but to avoid any confusion with plain JSON serialization i renamed them and suggested pattern of using JsonSerializable in Example.
This allows full compatibility and safe serialization/deserialization even if plain JSON.stringify/JSON.parse are used but introduced two additional methods to our class, i would love to hear your opinion about naming and whether using toSuperJSON/fromSuperJSON is better or toJSON/fromJSON
Summary
This proposal adds a dedicated, lightweight API that mirrors the existing
registerClasspattern but uses class’s definedtoSuperJSON/fromSuperJSONmethods.Motivation
current implementation of
class registryask forallowedPropsto include them in serialization:then re-create instance using
Object.assign. This approach works in most cases but has some limitations, as discussed in issue #331, also if class needs to run constructor to initialize some properties or if we want to add fields in our json that are not present as a property in the class.I think that allow passing explicit
to/frommethods will solve these problems entirely and add flexible control of serialization, so earlier code becomes:More advanced usage (constructor side-effects, computed fields, json data wrappers):
As
class registryit works for nested classes as well:This completely optional for devs who need more control, as change is additive only (extra single composite Rule).
Proposed API
(Exact same style as the existing
registerClassandregisterSymbol.)Example
Implementation notes
I already have a minimal, production-ready patch:
serializable-class-registry.ts(modeled exactly afterclass-registry.ts)registerSerializableClassinindex.ts.serializable-classannotation + rule intransformer.ts(identical structure to classRule)plainer.tsfor deep traversalworks for serializable class,works for nested serializable class,constructor side-effects & initialization,external json props in serializable classesandthrow if non-serializable class is passed to registerSerializableClass)The change is purely additive, does not touch any existing behavior, and reuses all existing patterns, types, and error messages.
I will create the PR after this discussion 😄
Note
Initially
toSuperJSON/fromSuperJSONwere namedtoJSON/fromJSON, but to avoid any confusion with plain JSON serialization i renamed them and suggested pattern of usingJsonSerializablein Example.This allows full compatibility and safe serialization/deserialization even if plain
JSON.stringify/JSON.parseare used but introduced two additional methods to our class, i would love to hear your opinion about naming and whether usingtoSuperJSON/fromSuperJSONis better ortoJSON/fromJSON