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!