blitz-js / superjson

Safely serialize JavaScript expressions to a superset of JSON, which includes Dates, BigInts, and more.
MIT License
3.88k stars 83 forks source link

Improve docs for non-default data types #259

Open jasongerbes opened 9 months ago

jasongerbes commented 9 months ago

The superjson README provides a recipe for registering a custom transformer for a non-default data type:

import { Decimal } from "decimal.js"

SuperJSON.registerCustom<Decimal, string>(
  {
    isApplicable: (v): v is Decimal => Decimal.isDecimal(v),
    serialize: v => v.toJSON(),
    deserialize: v => new Decimal(v),
  },
  'decimal.js'
);

However, the docs don't include any details about the registerClass and registerSymbol methods, nor do they include details about how a custom transformer can leverage the built-in transformers.

For example, given a CustomMap class the extends the built-in Map class and adds a customProperty and overridden get method, how can I use registerClass or registerCustom in a way that leverages the built-in serialization of the Map's entires?

class CustomMap<K, V> extends Map<K, V> {
  public customProperty: string;

  public constructor(
    customProperty: string,
    entries?: ReadonlyArray<readonly [K, V]> | null
  ) {
    super(entries);
    this.customProperty = customProperty;
  }

  public override get(key: K): V {
    const value = super.get(key);
    if (!value) {
      throw new Error(`Value with key ${key} does not exist.`);
    }
    return value;
  }
}

Using registerClass(CustomMap) doesn't serialize the CustomMap entries:

import SuperJSON from "superjson";
SuperJSON.registerClass(CustomMap);

const map = new CustomMap([
  ["a", 1],
  ["b", 2],
]);

const { json, meta } = SuperJSON.serialize(map);

/*
json = {
  customProperty: "My Map"
};
meta = {
  values: [
    ["class", "CustomMap"]
  ]
};
*/

Using registerCustom, it's unclear how you can leverage SuperJson's built-in Map transformer to serialize and deserialize the CustomMap class:

SuperJSON.registerCustom<CustomMap<any, any>, string>(
  {
    isApplicable: (v): v is CustomMap<any, any> => v instanceof CustomMap,
    serialize: (v) => "", // TODO
    deserialize: (v) => new CustomMap(/* TODO */),
  },
  "CustomMap"
);
Skn0tt commented 9 months ago

Hey @jasongerbes! registerClass and registerCustom aren't documented well, I agree. If you want to open a PR that adds docs for that, i'd be happy to review it!

Regarding the CustomMap case, I don't see an easy way of leveraging SuperJSON's built-in Map transformer, either. registerCustom is probably the best choice. You could try to make a Map out of your CustomMap and then recursively call SuperJSON.serialize(v) inside of serialize. Would that help?