Skip to content

Proposal: Add registerSerializableClass for classes with toSuperJSON() + static fromSuperJSON() #350

@ZiadTaha62

Description

@ZiadTaha62

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions