Skip to content

Unions

The variantCodec computation expression builds a codec for a discriminated union.

Each Codec.case names a tag, gives the case constructor, and the codec for the case fields. The return maps a value back to the case that was declared for it.

open Thoth.Json.Core
open Thoth.Json.JavaScript

type Shape =
    | Square of width: int
    | Rectangle of width: int * height: int
    | Circle of radius: int

module Shape =

    let codec: Codec<Shape> =
        variantCodec {
            let! square = Codec.case "square" Square Codec.int

            and! rectangle =
                Codec.case
                    "rectangle"
                    Rectangle
                    (Codec.tuple2 Codec.int Codec.int)

            and! circle = Codec.case "circle" Circle Codec.int

            return
                function
                | Square width -> square width
                | Rectangle(width, height) -> rectangle (width, height)
                | Circle radius -> circle radius
        }

let json = Rectangle(7, 2) |> Encode.codec Shape.codec |> Encode.toString 0

printfn "%s" json

Decode.fromString Shape.codec json |> Docs.print

A case with several fields takes a Codec.tuple2 to Codec.tuple8, in the order the fields are declared.

The representations

One property per case

variantCodec writes an object with a single property, named after the tag.

{ "square": 4 }

{ "rectangle": [ 7, 2 ] }

Decoding fails unless the object carries exactly one recognised tag.

This is sometimes called "External tag" encoding.

A tag and a value property

variantCodecWithTag writes the tag and the value under property names you choose.

{ "type": "square", "value": 4 }

{ "type": "rectangle", "value": [ 7, 2 ] }
open Thoth.Json.Core
open Thoth.Json.JavaScript

type Shape =
    | Square of width: int
    | Circle of radius: int

module Shape =

    let codec: Codec<Shape> =
        variantCodecWithTag "type" "value" {
            let! square = Codec.case "square" Square Codec.int
            and! circle = Codec.case "circle" Circle Codec.int

            return
                function
                | Square width -> square width
                | Circle radius -> circle radius
        }

let json = Circle 3 |> Encode.codec Shape.codec |> Encode.toString 0

printfn "%s" json

Decode.fromString Shape.codec json |> Docs.print

This is sometimes called "Adjacent tag" encoding.

A tuple of tag and data

variantCodecTuple writes a two element array where the first element is the union case tag and the second is the case data.

[ "square", 4 ]

[ "rectangle", [ 7, 2 ] ]

This is sometimes called "Internal tag" encoding. It is what earlier versions of Thoth wrote.

Intentionally omitted

Two shapes are left out.

The first puts the case fields beside the tag in one object:

{ "type": "rectangle", "width": 7, "height": 2 }

A field of the case can then collide with the tag.

The second carries no tag at all:

4

[ 7, 2 ]

Nothing says which case the value belongs to, so two cases of the same shape cannot be told apart.

Cases without fields

Give the case Codec.unit, and a constructor ignoring its argument. The value is written as null.

open Thoth.Json.Core
open Thoth.Json.JavaScript

type Color =
    | Red
    | Rgb of int * int * int

module Color =

    let codec: Codec<Color> =
        variantCodec {
            let! red = Codec.case "red" (fun () -> Red) Codec.unit

            and! rgb =
                Codec.case
                    "rgb"
                    Rgb
                    (Codec.tuple3 Codec.int Codec.int Codec.int)

            return
                function
                | Red -> red ()
                | Rgb(r, g, b) -> rgb (r, g, b)
        }

Red |> Encode.codec Color.codec |> Encode.toString 0 |> printfn "%s"

Rgb(255, 0, 0) |> Encode.codec Color.codec |> Encode.toString 0 |> printfn "%s"

Decode.fromString Color.codec """{ "red": null }""" |> Docs.print

Enums

Codec.Enum.int and its siblings represent an enum as the number it compiles to. There is one per underlying type: byte, sbyte, int16, uint16, int and uint32.

open Thoth.Json.Core
open Thoth.Json.JavaScript

type Rating =
    | One = 1
    | Two = 2
    | Three = 3

let codec: Codec<Rating> = Codec.Enum.int

Encode.codec codec Rating.Two |> Encode.toString 0 |> printfn "%s"

Decode.fromString codec "2" |> Docs.print
Edit this page