Skip to content

Custom serialized with nested fields that should also be serialized #330

@pie6k

Description

@pie6k

TLDR: With a custom class serializer, nested fields are not serialized but copied as-is.

I have a custom class Foo that should be serialized. The class can look like this:

class Foo {
  bar: any;

  constructor(bar) {
    this.bar = bar;
  }
}

foo.bar can be anything, including a nested Map that can even contain nested instances of Foo somewhere deep.

If I define a custom transformer like this:

  serializer.registerCustom(
    {
      isApplicable(v: unknown): v is Foo {
        return v instanceof Foo;
      },
      serialize(foo) {
        return { bar: foo.bar };
      },
      deserialize(data) {
         return new Foo(data);
      },
    },
   "Foo",
  );

It doesn't work as expected, as the serializer 'stops' at bar and simply passes it without performing nested serialization.

I am not sure how to tell SuperJson to keep going into foo and serialize everything using all existing rules, adding all nested fields to the result meta for nested types or circular references.

Is it possible?

The expected result would be:

const json = new SuperJson();
// register my custom rule (serializer.registerCustom(...))
const nestedFoo = new Foo(0);
const someMap = new Map();
someMap.set("nested", nestedFoo);
const foo2 = new Foo(someMap);

const serialized = json.serialize(foo2);
const parsed = json.parse(serialized);

const nestedFooClone = parsed.bar.get("nested");
nestedFooClone.foo === 0; // true - they are equal
nestedFooClone === nestedFoo; // false - they are not the same instance, i.e., they are cloned

Note 2: I've tried with serializer.registerClass - the result is the same (I did check the source code of SuperJson):

    const allowedProps = superJson.classRegistry.getAllowedProps(clazz.constructor);
    if (!allowedProps) {
        return { ...clazz };
    }
    const result = {};
    allowedProps.forEach(prop => {
        result[prop] = clazz[prop];
    });

As seen above, class properties are shallowly passed over without nested serialization.

Note 3: If in the case above foo would include some circular references, the serialize result would also have them, and trying to stringify will throw an error.


Possible solution:

Possible API could look like:

  serializer.registerCustom(
    {
      isApplicable(v: unknown): v is Foo {
        return v instanceof Foo;
      },
      serialize(foo, serialize) {
        return { bar: serialize(foo.bar) }; // <-------- Here! We perform nested serialization. It would keep serializing and adding to `meta` inside superjson result
      },
      deserialize(data, deserialize) {
         return new Foo(deserialize(data)); // <----- Also here, before we create the instance, we de-serialize first (possibly this could be done automatically)
      },
    },
   "Foo",
  );

Or:
serializer.registerCustom would have another option isNested: boolean. If enabled, serialization output is treated as deep and traversed

Some workaround or solution would be very welcome. I am creating a generic coder/decoder that can have arbitral, possibly nested and circular data structures built using custom classes. Not being able to serialize nested properties is quite a blocker for it to work

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