JohnWeisz / TypedJSON

Typed JSON parsing and serializing for TypeScript that preserves type information.
MIT License
604 stars 64 forks source link

Document how to parse enums without ReflectDecorators #98

Open gthb opened 5 years ago

gthb commented 5 years ago

I want to deserialize an enum property without ReflectDecorators:

enum Color { red="RED", green="GREEN", blue="BLUE" }

@jsonObject
class Thing {
    @jsonMember({what: "todo?"})
    public color?: Color
}

console.log(TypedJSON.parse('{"color": "green"}', Thing));
// should log Object { color: "green" }

console.log(TypedJSON.parse('{"color": "infradead"}', Thing));
// should throw error

How do I populate those @jsonMember options to achieve this? Enum types don't have a constructor.

The only solution I've found is:

function deserializeColor(value: string): Color {
    const keyValue = value as keyof typeof Color;
    if (keyValue in Color) {
        return Color[keyValue];
    } else {
        throw new SyntaxError(`${keyValue} is not a valid Color`);
    }
};

@jsonObject
class Thing {
    @jsonMember({deserializer: deserializeColor})
    public color?: Color
}

but that's pretty verbose, and requires a separate deserializer function for each enum type (or at least I haven't found a workable way to make it generic).

JohnWeisz commented 5 years ago

I understand it's perhaps not the most intuitive thing ever, but in the meantime, for your exact example, you have to think with the runtime code here, i.e. that your enum is in fact a string at runtime. As such, you should specify String as the member constructor (which is the same as the string primitive for TypedJSON):

enum Color { red="RED", green="GREEN", blue="BLUE" }

@jsonObject
class Thing {
    @jsonMember({ constructor: String })
    public color?: Color
}

That said though, this is not going to be value-checked at runtime. To do that, you can instead use jsonMember with an accessor, e.g.:

enum Color { red="RED", green="GREEN", blue="BLUE" }

@jsonObject
class Thing {
    private _color?: Color;

    @jsonMember({ constructor: String })
    public get color(): Color ...
    public set color(value: Color) ...
}

If you properly value-check and throw in your setter, the TypedJSON.parse call should throw on invalid values (assuming your error handler does as well, which is the default behavior). This can be simplified by using a custom decorator that value-checks assignments, as jsonMember can be used with other decorators.