Discriminated named union Serialization
Issues #1283
Introduction
Currently when defining named union if wanting to serialize it to JSON with a discriminator each variant needs to have the discriminated property.
This is not ideal as it requires the type used in the union to be aware of the serialization details of a type that it might be completely unaware of. It also prevent using non model types as variant(like string, number, etc).
@discriminator("kind")
union Pet {
cat: Cat;
dog: Dog;
}
model Cat {
kind: "cat";
name: string;
meow: boolean;
}
model Dog {
kind: "dog";
name: string;
bark: boolean;
}
Serialized it would look like this
{
"kind": "cat",
"name": "Whiskers",
"meow": true
}
{
"kind": "dog",
"name": "Rex",
"bark": false
}
Proposal
The proposal is to make it so that discriminated named union are serialized as an envelopped by default where the discriminator is the name of the variant.
There is a few ways people might want the envelop to be serialized so we will add a few options to control this.
Would be to add a new decorator @discriminated which has the goal of replacing the @discriminator. We could also keep @discriminator name instead. But I think that might be easier to use the new one.
model DiscriminatedConfig {
envelope?: "object" | "tuple";
discriminator?: string;
envelopePropertyName?: string;
}
extern dec discriminated(target: Union, config: valueof DiscriminatedConfig);
Option 1: Default object envelope
@discriminated
union Pet {}
{"kind": "<variantName>", "value": <value>}
Option 1.1: Default object envelope with customized names
@discriminated(#{discriminator: "dataKind", envelopePropertyName: "data"})
union Pet {}
{"dataKind": "<variantName>", "data": <value>}
Option 2: Tuple envelope
Serialized as a tuple of the discriminator and the value
@discriminated(#{envelope: "tuple"})
union Pet {}
["<variantName>", <value>]
Option 3: No envelope
In this case the data is injected directly into the variant object, this should only be compatible with model variants(cannot inject it in a primitive).
@discriminated(#{envelope: false})
union Pet {}
{...<value>, "kind": "<variantName>"}
Examples
Option 1
@discriminated
union Pet {
cat: Cat;
dog: Dog;
}
model Cat {
name: string;
meow: boolean;
}
model Dog {
name: string;
bark: boolean;
}
Serialized as
{
"kind": "cat",
"value": {
"name": "Whiskers",
"meow": true
}
},
{
"kind": "dog",
"value": {
"name": "Rex",
"bark": false
}
}
Option 1.1
@discriminated(#{discriminator: "dataKind", envelopePropertyName: "data"})
union Pet {
cat: Cat;
dog: Dog;
}
model Cat {
name: string;
meow: boolean;
}
model Dog {
name: string;
bark: boolean;
}
Serialized as
{
"dataKind": "cat",
"data": {
"name": "Whiskers",
"meow": true
}
},
{
"dataKind": "dog",
"data": {
"name": "Rex",
"bark": false
}
}
Option 2
@discriminated(#{envelope: "tuple"})
union Pet {
cat: Cat;
dog: Dog;
}
model Cat {
name: string;
meow: boolean;
}
model Dog {
name: string;
bark: boolean;
}
Serialized as
["cat", {
"name": "Whiskers",
"meow": true
}],
["dog", {
"name": "Rex",
"bark": false
}]
Option 3
@discriminated(#{envelope: false})
union Pet {
cat: Cat;
dog: Dog;
}
model Cat {
name: string;
meow: boolean;
}
model Dog {
name: string;
bark: boolean;
}
Serialized as
{
"kind": "cat",
"name": "Whiskers",
"meow": true
}
{
"kind": "dog",
"name": "Rex",
"bark": false
}
Extensible unions
In the same way we define extensible union for primitive types by including a base variant, we can define a base variant for discriminated unions of complex types.
This can either be done by
- a reserved variant name like
default
- this might prevent using
default as a variant name which is not ideal
- only variant without a name is considered the base variant
- explicit
@defaultVariant decorator
Usage of discriminator in inheritance
Using inheritance to represent discriminated union is a commonly used but it inaccurately represent the semantics of a discriminated union in a goal of making it map better to language that do not have unions.
With the solution to extensible unions as above we can represent the same structure without inheritance in a much more explicit way as well.
@discriminated(#{envelope: false})
union Pet {
cat: Cat;
dog: Dog;
PetBase; // default variant
}
model Cat extends PetBase {
name: string;
meow: boolean;
}
model Dog extends PetBase {
name: string;
bark: boolean;
}
Discriminated named union Serialization
Issues #1283
Introduction
Currently when defining named union if wanting to serialize it to JSON with a discriminator each variant needs to have the discriminated property.
This is not ideal as it requires the type used in the union to be aware of the serialization details of a type that it might be completely unaware of. It also prevent using non model types as variant(like string, number, etc).
Serialized it would look like this
{ "kind": "cat", "name": "Whiskers", "meow": true } { "kind": "dog", "name": "Rex", "bark": false }Proposal
The proposal is to make it so that discriminated named union are serialized as an envelopped by default where the discriminator is the name of the variant.
There is a few ways people might want the envelop to be serialized so we will add a few options to control this.
Would be to add a new decorator
@discriminatedwhich has the goal of replacing the@discriminator. We could also keep@discriminatorname instead. But I think that might be easier to use the new one.Option 1: Default object envelope
{"kind": "<variantName>", "value": <value>}Option 1.1: Default object envelope with customized names
{"dataKind": "<variantName>", "data": <value>}Option 2: Tuple envelope
Serialized as a tuple of the discriminator and the value
Option 3: No envelope
In this case the data is injected directly into the variant object, this should only be compatible with model variants(cannot inject it in a primitive).
{...<value>, "kind": "<variantName>"}Examples
Option 1
Serialized as
{ "kind": "cat", "value": { "name": "Whiskers", "meow": true } }, { "kind": "dog", "value": { "name": "Rex", "bark": false } }Option 1.1
Serialized as
{ "dataKind": "cat", "data": { "name": "Whiskers", "meow": true } }, { "dataKind": "dog", "data": { "name": "Rex", "bark": false } }Option 2
Serialized as
Option 3
Serialized as
{ "kind": "cat", "name": "Whiskers", "meow": true } { "kind": "dog", "name": "Rex", "bark": false }Extensible unions
In the same way we define extensible union for primitive types by including a base variant, we can define a base variant for discriminated unions of complex types.
This can either be done by
defaultdefaultas a variant name which is not ideal@defaultVariantdecoratorUsage of discriminator in inheritance
Using inheritance to represent discriminated union is a commonly used but it inaccurately represent the semantics of a discriminated union in a goal of making it map better to language that do not have unions.
With the solution to extensible unions as above we can represent the same structure without inheritance in a much more explicit way as well.