-
Notifications
You must be signed in to change notification settings - Fork 41
Chiron 6
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.
Within a .NET SDK application, Chiron can be added using the dotnet
CLI.
dotnet add package Chiron
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
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
computation expression allows you to write logic to perform serialiation and deserialization between types and Chiron's Chiron.Functional.Json
type.
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
}
}
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 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"
}