JSON and Serialization
Prerequisites: Records
.NET's built-in System.Text.Json handles F# records, lists, arrays, maps, and options out of the box:
open System.Text.Json
type Address = { City: string; Country: string }
type Customer = { Name: string; Age: int; Tags: string list; Address: Address option }
let ada = { Name = "Ada"; Age = 36; Tags = [ "vip"; "early" ]; Address = Some { City = "London"; Country = "UK" } }
let json = JsonSerializer.Serialize ada
printfn "%s" json
let back = JsonSerializer.Deserialize<Customer> json
printfn "%b" (back = ada)
let noAddress = JsonSerializer.Deserialize<Customer> """{"Name":"Bob","Age":41,"Tags":[],"Address":null}"""
printfn "%A" noAddress.Address
{"Name":"Ada","Age":36,"Tags":["vip","early"],"Address":{"City":"London","Country":"UK"}}
true
None
Records deserialize through their constructor, so they stay immutable.
Some x is written as plain x, and None as null.
Options: naming and indentation
open System.Text.Json
type Product = { ProductId: int; DisplayName: string; UnitPrice: decimal }
let options = JsonSerializerOptions(PropertyNamingPolicy = JsonNamingPolicy.CamelCase, WriteIndented = true)
let json = JsonSerializer.Serialize({ ProductId = 7; DisplayName = "Tea"; UnitPrice = 3.5m }, options)
printfn "%s" json
let parsed = JsonSerializer.Deserialize<Product>("""{"productId":8,"displayName":"Coffee","unitPrice":4.0}""", options)
printfn "%A" parsed
{
"productId": 7,
"displayName": "Tea",
"unitPrice": 3.5
}
{ ProductId = 8
DisplayName = "Coffee"
UnitPrice = 4.0M }
Reuse one JsonSerializerOptions instance — creating a new one per call
is slow. Paste JSON output into the JSON Formatter
or JSON Validator to inspect it.
Discriminated unions need a library
Plain System.Text.Json doesn't know how to represent unions:
open System.Text.Json
type Shape =
| Circle of radius: float
| Square of side: float
try
JsonSerializer.Serialize(Circle 1.5) |> printfn "%s"
with ex ->
printfn "%s" (ex.GetType().Name)
NotSupportedException
The FSharp.SystemTextJson package adds a converter for unions (and other F# types), with several encoding styles:
// illustrative: downloads FSharp.SystemTextJson on first run
#r "nuget: FSharp.SystemTextJson"
open System.Text.Json
open System.Text.Json.Serialization
type Shape =
| Circle of radius: float
| Square of side: float
let options = JsonFSharpOptions.Default().ToJsonSerializerOptions()
let json = JsonSerializer.Serialize([ Circle 1.5; Square 2.0 ], options)
printfn "%s" json
printfn "%A" (JsonSerializer.Deserialize<Shape list>(json, options))
[{"Case":"Circle","Fields":[1.5]},{"Case":"Square","Fields":[2]}]
[Circle 1.5; Square 2.0]
Other popular choices are Thoth.Json (explicit encoders and decoders, which also work in Fable) and Newtonsoft.Json with Fable.JsonConverter-style converters in older code.
Reading JSON without types
For quick exploration, JsonDocument gives read-only access to any
JSON:
open System.Text.Json
let doc = JsonDocument.Parse """{"user":{"name":"Ada","langs":["F#","C#"]}}"""
let user = doc.RootElement.GetProperty "user"
printfn "%s" (user.GetProperty("name").GetString())
for lang in user.GetProperty("langs").EnumerateArray() do
printfn "- %s" (lang.GetString())
Ada
- F#
- C#
- Validate JSON from outside your program (it's untrusted input): parse
into a DTO record, then convert to domain types with smart
constructors that return
Result.