United Kingdom: +44 (0)208 088 8978

JSON Serialisation in F#

The state of .NET's default JSON serialization library in relation to its ability to deal with F# types.

Implementing JSON serialisation and deserialisation for a project is a task that consistently makes its way into many developer's todo list. It mostly takes the form of parsing some data from a web API to an F# type or vice versa.

The default option for a .NET developer when you need to parse some JSON is to use the JsonSerializer object from the System.Text.Json library. The problem is that this library hasn’t supported F# types until recently, and now it only supports them in a limited way. In this blogpost, I will walk you through the state of JsonSerializer and talk about what else you can use when it falls short.

Script Files

Setup

For demonstration purposes, I’m going to run the following set of commands to set up an environment with the NuGet packages I need and load scripts to be consumed from my script file. Note that the version of the System.Text.Json package is important as they’ve only started supporting F# types after their latest major update.

mkdir json-serialization-in-fsharp
cd json-serialization-in-fsharp
paket init
paket add System.Text.Json --version 5.0.0-rc.1.20451.14
paket install
paket generate-load-scripts

Then, create a script file in the json-serialization-in-fsharp directory, open it and insert the following lines. This will load the dependencies I need to experiment with this library.

#load ".paket/load/netcoreapp3.1/main.group.fsx"

After this, I will be able to work with the JsonSerializer object.

Types

Let's assume that I have the following domain.

open System

type Human = { FullName : string ; Age : int }
type Robot = Guid * string
type Race = Human | Robot

Here, the type Human is a record, Robot a tuple, and Race a discriminated union. I will try to first serialise each type and then deserialise them so that we can see how well the System.Text.Json library does in handling these situations. First, let's create the values I will work with.

let human = { FullName = "Alican Demirtas" ; Age = 22 }
let robot = Guid.NewGuid(), "RZ-210"
let race = Human

Serialize

I will first serialize each type and bind them each to a keyword so that I can use the serialised JSON values later on in the Deserialize section.

open System.Text.Json

let humanJson = JsonSerializer.Serialize human
let robotJson = JsonSerializer.Serialize robot
let raceJson = JsonSerializer.Serialize race

If you execute this block of code in F# interactive, you will get the following result.

val humanJson : string = "{"FullName":"Alican Demirtas","Age":22}"
val robotJson : string = "{"Item1":"c075f0fc-189f-4518-9f73-cf71108a5c2d","Item2":"RZ-210"}"
val raceJson : string = "{"Tag":0,"IsHuman":true,"IsRobot":false}"

This means that serialization has worked and the JSON objects have been bound to the keywords we've defined (humanJson, etc.) as string values.

Deserialize

What about deserialisation? What happens when we try to parse the JSON values we've just created into F# types that we have in our system? Let's try.

JsonSerializer.Deserialize<Human> humanJson
JsonSerializer.Deserialize<Robot> robotJson
JsonSerializer.Deserialize<Race> raceJson

As soon as I run this block of code or any line thereof, I will get the following error.

System.NotSupportedException: Deserialization of reference types without parameterless constructor is not supported. Type 'FSI_0004+Human'
   at System.Text.Json.ThrowHelper.ThrowNotSupportedException_DeserializeCreateObjectDelegateIsNull(Type invalidType)
   at System.Text.Json.JsonSerializer.HandleStartObject(JsonSerializerOptions options, ReadStack& state)
   at System.Text.Json.JsonSerializer.ReadCore(JsonSerializerOptions options, Utf8JsonReader& reader, ReadStack& readStack)
   at System.Text.Json.JsonSerializer.ReadCore(Type returnType, JsonSerializerOptions options, Utf8JsonReader& reader)
   at System.Text.Json.JsonSerializer.Deserialize(String json, Type returnType, JsonSerializerOptions options)
   at System.Text.Json.JsonSerializer.Deserialize[TValue](String json, JsonSerializerOptions options)
   at <StartupCode$FSI_0014>.$FSI_0014.main@()
Stopped due to error

Which is a verbose way of saying "we don't support that". Now, we know that we can serialise F# types into JSON in F# script files, but cannot deserialise them. What if we tried the same in a console app?

Console Apps

In console apps, the JsonSerializer acts a bit differently. Let's say that I have the same domain, same types, same everything in an F# console app.

open System
open System.Text.Json

type Human = { FullName : string ; Age : int }
type Robot = Guid * string
type Race = Human | Robot

let human = { FullName = "Alican Demirtas" ; Age = 22 }
let robot = Guid.NewGuid(), "RZ-210"
let race = Human

let humanJson = JsonSerializer.Serialize human
let robotJson = JsonSerializer.Serialize robot
let raceJson = JsonSerializer.Serialize race

let humanDeserialized = JsonSerializer.Deserialize<Human> humanJson
let robotDeserialized = JsonSerializer.Deserialize<Robot> robotJson
let raceDeserialized = JsonSerializer.Deserialize<Race> raceJson

[<EntryPoint>]
let main argv =
    printfn "%A" humanJson
    printfn "%A" robotJson
    printfn "%A" raceJson

    printfn "%A" humanDeserialized
    printfn "%A" robotDeserialized
    printfn "%A" raceDeserialized
    0 // return an integer exit code

This app will fail to run, which shouldn't be so surprising since it has the deserialization code that failed in a script file, also. What's interesting is that if we go on and delete the following lines that has to do with deserializing discriminated union types, we will get a different result. Let's comment out the following lines.

let raceDeserialized = JsonSerializer.Deserialize<Race> raceJson
printfn "%A" raceDeserialized

If we run the app now, it'll run to completion with no errors and print out the values that we told it to. We'll get the value of humanJson, robotJson, raceJson, humanDeserialized and robotDeserialized printed in the console. So in this case, it's only deserializing discriminated unions that gives the JsonSerializer a headache.

Conclusion

The Serialize and Deserialize methods of the JsonSerializer object in System.Text.Json works with F# types in a limited way.

  • In both script files and F# projects, serialization works with no issues for records, tuples and discriminated unions.
  • In console apps, deserialisation does not work for discriminated union types.
  • In script files, deserialization does not work – period.

Needless to say that we might see some improvements on the JsonSerializer in .NET 5. You should also know that there are more libraries that you can use for serialising/deserialising JSON that you might want to look at. By far the most commonly used is Json.NET which has excellent F# support, including for Discriminated Unions. Thoth, FSharp.SystemTextJson and FSharp.Json are also worth checking out.

Thank you for reading and happy coding!