Serialization of F# UnionType

Union Type serialization is not a built-in option in Newtonsoft.Json. In this article we will take a look how to work around that.

December 20, 2018 - 5 minute read -
F# Patterns

Hi,

last time I decided to rewrite one of the services from C# to F#. One of the problems was to keep the integrity in terms of contract generated via this project. Previously the contract contains an enum field which I decided to represent as a union type in F#. By default Newtonsoft.Json which we are using to serialize data, serialize differently enum from C# and union type from F#. So going to the details of a problem. The contract looked like this:

public enum SomeEnum
{
    Value1,
    Value2
}

public class Contract
{
    public string Name { get; set; }
    public SomeEnum Type { get; set; }
}

The serialized contract, which were consumed by a front-end app looked like this:

{
    Type: value1,
    Name: name,
}

Rewriting an object to F# wasn’t a hard task. As I said before I decided that enum would be represented as a union type and a class as a Type. So the code in F# looked like this:

type SomeEnum = Value1 | Value2

type Contract =
    {
        Name: string
        Type: SomeEnum
    }

The problem appears during the serialization of this type and passing it to a front-end. Because the serialized object looked like this:

{
    Type: { Case: value1 }
    Name: name
}

So as we could see this json looks a little bit different than the previous one. Because of that I decided to write my own converter for union types. The first thing was a serialization. What I want to achieve in the serialized object is information only about the value of union type as a string. But this value has to start with a lower case. Keeping in mind what I plan about that here is a piece of code to do that:

type EnumAsStringConverter () =
    inherit JsonConverter()

    override this.CanConvert objectType = FSharpType.IsUnion(objectType)
    override this.WriteJson (writer: JsonWriter, value: obj, serializer: JsonSerializer): unit =
        if value = null then writer.WriteValue value
        else
            let str = value.ToString()
            let firstLetter = Char.ToLowerInvariant(str.[0])
            let theRest = str.Substring(1)
            writer.WriteValue(sprintf "%c%s" firstLetter theRest)

This code has a task to get the actual union value and then invoke a ‘toString’ on it. But keeping in mind that it has to start with a lower case. So I run my tests which checks a compatibility with the old json and everything seems to work fine. But here appeared a problem. What with deserialization to a union type? Right now there is no possibility to deserialize string to union. To do that I override a read method which looks right now like this:

...
override this.CanRead = true

override this.ReadJson (reader: JsonReader, objectType: Type, existingValue: obj, serializer: JsonSerializer) : obj =
    if reader.TokenType <> JsonToken.String then
        (sprintf "Cannot deserialize %A type to Union" reader.TokenType) |> ArgumentException |> raise
    else
        if FSharpType.IsUnion (objectType, BindingFlags.Public) then
            let matchedCase = FSharpType.GetUnionCases(objectType, BindingFlags.Public) |> Array.tryFind (fun x -> x.Name.ToLower() = (reader.Value :?> string).ToLower())
            match matchedCase with
            | Some s -> FSharpValue.MakeUnion(s, [||])
            | None -> (sprintf "Cannot find maching case %A in union case: %A" reader.Value objectType) |> ArgumentException |> raise
        else (sprintf "objectType is not an union, instead it is a: %A" objectType) |> ArgumentException |> raise

To create an ACL at first I checked if the value which I want to deserialize is a string and if the type to which I want to deserialize is a union type. Then I looked in all of the fields of a union and check if it has a value like this ignoring the case. If I found one I create a union with the following value. I run tests one more time and everything is green.

To sum up moving parts of C# code to F# with keeping the backward compatibility in a contract could be problematic. And sometimes we have to do some extra work to achieve this. But still, I think that because of easy writing programs in F# in comparison to C# sometimes it is worth to take a while to look and do this.

Thanks for reading!