TypeConverter working for Newtonsoft.Json, but not JsonConverter? (F#)

Viewed 457

Given these types and this unit test:

type DiscUnion =
    | D1
    | D2

type Foo =
    {
        Bar: int;
        Baz: Map<DiscUnion,int>;
    }

let ImportDiscUnionJson (json: string): Foo =
    Marshalling.Deserialize json

let DiscUnionExampleInJson =
    "{\"Bar\": 42, \"Baz\":" +
    "{\"D1\": 4242, " +
    "\"D2\": 424242 }}"

[<Test>]
let ``testing disc union deserialization``() =

    let deserializedDiscUnion =
        ImportDiscUnionJson
            DiscUnionExampleInJson

    Assert.That(deserializedDiscUnion, Is.Not.Null)

    Assert.That(deserializedDiscUnion.Bar,
        Is.EqualTo(42))

    Assert.That(deserializedDiscUnion.Baz.[DiscUnion.D1],
        Is.EqualTo(4242))

    Assert.That(deserializedDiscUnion.Baz.[DiscUnion.D2],
        Is.EqualTo(424242))

I was receiving this exception from Newtonsoft.Json (JSON.NET, I'm using version 9.0.1):

----> Newtonsoft.Json.JsonSerializationException : Could not convert string 'D1' to dictionary key type 'FSharpTests.Deserialization+DiscUnion'. Create a TypeConverter to convert from the string to the key type object. Path 'Value.Baz.D1', line 1, position 82. ----> Newtonsoft.Json.JsonSerializationException : Error converting value "D1" to type 'FSharpTests.Deserialization+DiscUnion'. Path 'Value.Baz.D1', line 1, position 82.

Then I added a TypeConverter to the type, this way:

let Construct<'T> (caseInfo: Microsoft.FSharp.Reflection.UnionCaseInfo) =
    Microsoft.FSharp.Reflection.FSharpValue.MakeUnion(caseInfo, [||]) :?> 'T
let GetUnionCaseInfoAndInstance<'T> (caseInfo: Microsoft.FSharp.Reflection.UnionCaseInfo) =
    (Construct<'T> caseInfo)
let GetAllElementsFromDiscriminatedUnion<'T>() =
    Microsoft.FSharp.Reflection.FSharpType.GetUnionCases(typeof<'T>)
    |> Seq.map GetUnionCaseInfoAndInstance<'T>

[<System.ComponentModel.TypeConverter(typeof<MyStringTypeConverter>)>]
type DiscUnion =
    | D1
    | D2
    override self.ToString() =
        sprintf "%A" self
    static member GetAll(): seq<DiscUnion> =
        GetAllElementsFromDiscriminatedUnion<DiscUnion>()

and private MyStringTypeConverter() =
    inherit System.ComponentModel.TypeConverter()
    override this.CanConvertFrom(context, sourceType) =
        sourceType = typedefof<string> || base.CanConvertFrom(context, sourceType)
    override this.ConvertFrom(context, culture, value) =
        match value with
        | :? string as stringValue ->
            Seq.find (fun discUnion -> discUnion.ToString() = stringValue) (DiscUnion.GetAll()) :> obj
        | _ -> base.ConvertFrom(context, culture, value)

And it works great, however, TypeConverter type doesn't exist in the PCL profile (I guess nothing from System.ComponentModel does), so I tried using JsonConverter instead:

let Construct<'T> (caseInfo: Microsoft.FSharp.Reflection.UnionCaseInfo) =
    Microsoft.FSharp.Reflection.FSharpValue.MakeUnion(caseInfo, [||]) :?> 'T
let GetUnionCaseInfoAndInstance<'T> (caseInfo: Microsoft.FSharp.Reflection.UnionCaseInfo) =
    (Construct<'T> caseInfo)
let GetAllElementsFromDiscriminatedUnion<'T>() =
    Microsoft.FSharp.Reflection.FSharpType.GetUnionCases(typeof<'T>)
    |> Seq.map GetUnionCaseInfoAndInstance<'T>

[<JsonConverter(typeof<MyStringTypeConverter>)>]
type DiscUnion =
    | D1
    | D2
    override self.ToString() =
        sprintf "%A" self
    static member GetAll(): seq<DiscUnion> =
        GetAllElementsFromDiscriminatedUnion<DiscUnion>()

and private MyStringTypeConverter() =
    inherit JsonConverter()

    override this.CanConvert(objectType): bool =
        objectType = typedefof<DiscUnion>

    override this.ReadJson(reader: JsonReader, objectType: Type, existingValue: Object, serializer: JsonSerializer) =
        if (reader.TokenType = JsonToken.Null) then
            null
        else
            let token =
                Newtonsoft.Json.Linq.JToken.Load(reader)
                      // not sure about the below way to convert to string, in stackoverflow it was a C# cast
                      .ToString()
            try
                DiscUnion.GetAll().First(fun discUnion -> discUnion.ToString() = token) :> Object
            with ex -> raise(new Exception(sprintf "DiscUnion case not found: %s" token, ex))

    override this.WriteJson(writer: JsonWriter, value: Object, serializer: JsonSerializer) =
        let discUnion = value :?> DiscUnion
        writer.WriteValue(discUnion.ToString())

But this doesn't work, I still get the previous exception:

----> Newtonsoft.Json.JsonSerializationException : Could not convert string 'D1' to dictionary key type 'FSharpTests.Deserialization+DiscUnion'. Create a TypeConverter to convert from the string to the key type object. Path 'Value.Baz.D1', line 1, position 82. ----> Newtonsoft.Json.JsonSerializationException : Error converting value "D1" to type 'FSharpTests.Deserialization+DiscUnion'. Path 'Value.Baz.D1', line 1, position 82.

How to solve this problem and still be PCL compatible?

1 Answers

I ended up using .NETStandard2.0 instead of PCL

Related