Skip to content

Discuss implicit discriminator for named union #2738

@timotheeguerin

Description

@timotheeguerin

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;
} 

Metadata

Metadata

Labels

breaking-changeA change that might cause specs or code to breakcompiler:coreIssues for @typespec/compilerdesign:acceptedProposal for design has been discussed and accepted.triaged:core

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions