detect an enum at runtime and stringify as keys

Viewed 91

playground

I have a bunch of interfaces, at least 2-3 levels nested, where some of the leafs are numbers/strings, etc, but others are (numeric) enums. I don't want to change this.

Now I want to "serialize" objects that implements my interfaces as JSON. Using JSON.stringify is good for almost all cases, but the enums, that are serialized with their (numerical) value.

I know that it's possible to pass a replacer function to JSON.stringify, but I'm stuck, as I'm not sure how to write a function that detect the structure of my object and replace the enum values with the appropriate names.

example:

enum E { X = 0, Y = 1, Z = 2 }
enum D { ALPHA = 1, BETA = 2, GAMMA = 3 }
interface C { e: E; }
interface B { c?: C; d?: D; }
interface A { b?: B; }

function replacer(this: any, key: string, value: any): any {
    return value;
}

function stringify(obj: A): string {
    return JSON.stringify(obj, replacer);
}

const expected = '{"b":{"c":{"e":"Y"},"d":"ALPHA"}}';
const recieved = stringify({ b: { c: { e: E.Y }, d: D.ALPHA } });

console.log(expected);
console.log(recieved);
console.log(expected === recieved);
2 Answers

It's not possible to automatically find out which enum was assigned to a field, not even with typescript's emitDecoratorMetadata option. That option can only tell you it's a Number and it will only be emitted on class fields that have other decorators on them.

The best solution you have is to manually add you own metadata. You can do that using reflect-metadata node module.

You'd have to find all enum fields on all of your classes and add metadata saying which enum should be used for serializing that field.

import 'reflect-metadata';

enum E
{
    ALPHA = 1,
    BETA = 2,
    GAMMA = 3,
}

class C
{
    // flag what to transform during serialization
    @Reflect.metadata('serialization:type', E)
    enumField: E;

    // the rest will not be affected
    number: number;
    text: string;
}

This metadata could be added automatically if you can write an additonal step for your compiler, but that is not simple to do.

Then in your replacer you'll be able to check if the field was flagged with this matadata and if it is then you can replace the numeric value with the enum key.

const c = new C();

c.enumField= E.ALPHA;

c.number = 1;
c.text = 'Lorem ipsum';

function replacer(this: any, key: string, value: any): any
{
    const enumForSerialization = Reflect.getMetadata('serialization:type', this, key);
    return enumForSerialization ? enumForSerialization[value] ?? value : value;
}

function stringify(obj: any)
{
    return JSON.stringify(obj, replacer);
}

console.log(stringify(c)); // {"enumField":"ALPHA","number":1,"text":"Lorem ipsum"}

This only works with classes, so you will have to replace your interfaces with classes and replace your plain objects with class instances, otherwise it will not be possible for you to know which interface/class the object represents.


If that is not possible for you then I have a much less reliable solution.

You still need to list all of the enum types for all of the fields of all of your interfaces.
This part could be automated by parsing your typescript source code and extracting the enum types for those enum fields and then saving it in a json file that you can load in runtime.

Then in the replacer you can guess the interface of an object by checking what are all of the fields on the this object and if they match an interface then you can apply enum types that you have listed for that interface.

Did you want something like this? It was the best I could think without using any reflection.

enum E { X = 0, Y = 1, Z = 2 }
enum D { ALPHA = 1, BETA = 2, GAMMA = 3 }
interface C { e: E; }
interface B { c?: C; d?: D; }
interface A { b?: B; }

function replacer(this: any, key: string, value: any): any {
  switch(key) {
    case 'e':
      return E[value];
    case 'd':
      return D[value];
    default:
      return value;
  }
}

function stringify(obj: A): string {
    return JSON.stringify(obj, replacer);
}

const expected = '{"b":{"c":{"e":"Y"},"d":"ALPHA"}}';
const recieved = stringify({ b: { c: { e: E.Y }, d: D.ALPHA } });

console.log(expected);
console.log(recieved);
console.log(expected === recieved);

This solution assumes you know the structure of the object, just as you gave in the example.

Related