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

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions