Skip to content
Dave Curylo edited this page Sep 28, 2018 · 3 revisions

Chiron 6.x

As of 6.3.0, Chiron is compatible with .NET Standard 2.0 and can be used safely on .NET Core 2.0 or .NET Framework 4.5.2 or later application runtimes.

Installation

Within a .NET SDK application, Chiron can be added using the dotnet CLI.

dotnet add package Chiron

Writing and reading JSON

In Chiron, the serialization and formatting are two separate steps, albeit they can be easily composed. Serialization converts a custom type to a Json type that Chiron knows how to format as a string of JSON.

A common flow for serialization is to create a type and pipe it to the serialize and format functions:

customRecord |> Json.serialize |> Json.format

Reading JSON requires parsing followed by deserialization, and often a type annotation is helpful here:

// a string named json deserialized into a type
let customRecord = json |> Json.parse |> Json.deserialize : MyCustomType

Types to and from JSON

Chiron uses Statically Resolved Type Parameters (SRTP) to perform serialization and ensure that a type explictly defines how it may be serialized. Rather than a relection-based approach, this allows the type definition itself to change without directly affecting the JSON. As a result, a serialization function can be attached to an F# type such as a discriminated union, which may serialize differently per union case.

The json Builder

The json computation expression allows you to write logic to perform serialiation and deserialization between types and Chiron's Chiron.Functional.Json type.

Record

Record serialization is the most straight forward. Take a simple Rectangle record:

type Rectangle = {
    Length : int
    Width : int
}

Chiron is able to serialize strings, numbers, booleans, options, and lists out of the box, but more complex types like records cannot fulfill the SRTP requirements, so attempts to serialize with Chiron will not compile. To satisfy the Json.serialize function, the ToJson static member must be added to the record type:

type Rectangle with
    static member ToJson (r:Rectangle) =
        json {
            do! Json.write "length" r.Length
            do! Json.write "width" r.Width
        }

To deserialize, a FromJson static member is required:

    static member FromJson (_:Rectangle) =
        json {
            let! length = Json.read "length"
            let! width = Json.read "wdith"
            return {
                Length = length
                Width = width
            }
        }

Discriminated Union

The ToJson and FromJson members can be added to a discriminated union in order to produce or consume JSON that may have different structures. For example, a Shape might serialize to one of these two JSON structures:

A rectangle:

{ "length":15, "width":23 }

A circle:

{ "radius":42 }

The F# definition for this could be a discriminated union:

type Shape =
    | Rectangle of Length:int * Width:int
    | Circle of Radius:int

Depending on the union case, different fields could be written:

type Shape with
    static member ToJson (s:Shape) =
        json {
            match s with
            | Circle (radius) ->
                do! Json.write "radius" radius
            | Rectangle (length, width) ->
                do! Json.write "length" length
                do! Json.write "width" width
        }

Deserialization is a bit more complex, as the different cases would need to be detected based on the presence of JSON elements:

type Shape with
    static member FromJson (_:Shape) =
        json {
            let! maybeRadius = Json.tryRead "radius"
            return!
                match maybeRadius with
                | Some radius -> json { return Circle (radius) }
                | None ->
                    json {
                        let! maybeLength = Json.tryRead "length"
                        let! maybeWidth = Json.tryRead "width"
                        match maybeLength, maybeWidth with
                        | Some length, Some width -> return Rectangle (Length=length, Width=width)
                        | _ -> return! Json.error "Shape requires either 'radius' or 'length' and 'width'"
                    }
        }

Notice the Json.error function can be used when the JSON doesn't have the required elements for either case. An alternative could be to provide a member in the JSON that can be used to differentiate the type:

{ "shape":"rectangle", "length":15, "width":23 }
{ "shape":"circle", "radius":42 }

The JSON serialization members can be implmented as follows:

static member ToJson (s:Shape) =
    json {
        match s with
        | Circle (radius) ->
            do! Json.write "shape" "circle"
            do! Json.write "radius" radius
        | Rectangle (length, width) ->
            do! Json.write "shape" "rectangle"
            do! Json.write "length" length
            do! Json.write "width" width
    }

static member FromJson (_:Shape) =
    json {
        let! shape = Json.read "shape"
        return!
            match shape with
            | "circle" ->
                json { 
                    let! radius = Json.read "radius"
                    return Circle (radius)
                }
            | "rectangle" ->
                json {
                    let! length = Json.read "length"
                    let! width = Json.read "width"
                    return Rectangle (Length=length, Width=width)
                }
            | _ -> Json.error "Shape requires either 'radius' or 'length' and 'width'"
    }

Primitives

Primitives are a bit tricky, but given the ease at which new types are often created in F# applications, it is often helpful to be able to serialize these types as if they were a primitive. For example, one may define a MAC address type as a single case DU:

type MacAddress = MacAddress of uint8 * uint8 * uint8 * uint8 * uint8 * uint8

One might prefer to represent this type in JSON as the typical string format, like 30-56-0D-8D-B0-38.

type MacAddress with
    static member ToJson (MacAddress(b1, b2, b3, b4, b5, b6)) =
        sprintf "%02X-%02X-%02X-%02X-%02X-%02X" b1 b2 b3 b4 b5 b6
        |> Json.Optic.set Json.String_

Now any F# type that contains a MacAddress can be serialized.

type NetworkCard = {
    Name : string
    Mac : MacAddress
} with
    static member ToJson (n:NetworkCard) =
        json {
            do! Json.write "name" n.Name
            do! Json.write "mac" n.Mac
        }

And now this NIC:

let nic = {
    Name = "eth0"
    Mac = MacAddress (12uy, 34uy, 56uy, 78uy, 10uy, 23uy)
}

nic |> Json.serialize |> Json.format |> printfn "%s"

serializes to JSON:

{
    "mac":"0C-22-38-4E-0A-17",
    "name":"eth0"
}