Another failure at deserializing data with discriminated unions, in F#

我怕爱的太早我们不能终老 提交于 2020-06-29 03:47:31

问题


Following a question where the answer provided a working solution to serialize / deserialize discriminated unions (IgnoreMissingMember setting doesn't seem to work with FSharpLu.Json deserializer)

I have now a practical case where this fails (although it works in simpler cases).

here is the test code:

open System.Collections.Generic
open Microsoft.FSharpLu.Json
open Newtonsoft.Json
open Newtonsoft.Json.Serialization

// set up the serialization / deserialization based on answer from:
// https://stackoverflow.com/questions/62364229/ignoremissingmember-setting-doesnt-seem-to-work-with-fsharplu-json-deserializer/62364913#62364913

let settings =
    JsonSerializerSettings(
        NullValueHandling = NullValueHandling.Ignore,
        Converters = [| CompactUnionJsonConverter(true, true) |]
    )

let serialize object =
    JsonConvert.SerializeObject(object, settings)

let deserialize<'a> object =
    JsonConvert.DeserializeObject<'a>(object, settings)


// define the type used
type BookSide =
    | Bid
    | Ask

type BookEntry =
    {
        S : float
        P : float
    }

type BookSideData =
    Dictionary<int, BookEntry>

type BookData =
    {
        Data: Dictionary<BookSide, BookSideData>
    }

    static member empty =
        {
            Data = Dictionary<BookSide, BookSideData>(dict [ (BookSide.Bid, BookSideData()); (BookSide.Ask, BookSideData()) ])
        }

// make some sample data
let bookEntry = { S=3.; P=5. }
let bookData = BookData.empty
bookData.Data.[BookSide.Bid].Add(1, bookEntry)

// serialize. This part works
let s = serialize bookData

// deserialize. This part fails
deserialize<BookData> s

the serialized test data will look like this:

{"Data":{"Bid":{"1":{"S":3.0,"P":5.0}},"Ask":{}}}

but deserializing will crash like this:

Could not convert string 'Bid' to dictionary key type 'FSI_0023+BookSide'. Create a TypeConverter to convert from the string to the key type object.

although the serialization / deserialization of the DU through FSharpLu which has a DU converter.

The reason I am trying to find some automated solution, vs writing a custom TypeConverter (besides the fact I've never done it) is that I have a lot of types I do not control to go through.

Here is a fiddle: https://dotnetfiddle.net/Sx0k4x


回答1:


Your basic problem is that you are using BookSide as a dictionary key -- but this is an f# union which makes it a complex key -- one not immediately convertible to and from a string. Unfortunately Json.NET does not support complex dictionary keys out of the box as is stated in its Serialization Guide:

When serializing a dictionary, the keys of the dictionary are converted to strings and used as the JSON object property names. The string written for a key can be customized by either overriding ToString() for the key type or by implementing a TypeConverter. A TypeConverter will also support converting a custom string back again when deserializing a dictionary.

There are two basic approaches to handling this issue:

  1. Implement a TypeConverter as is shown in, e.g., Not ableTo Serialize Dictionary with Complex key using Json.net.

  2. Serialize the dictionary as an array of key/value pair objects e.g. as is shown in Serialize dictionary as array (of key value pairs).

Since your data model includes dictionaries with a variety of keys (DU, strings and ints) the second solution would appear to be the only possibility. The following DictionaryConverter should have the necessary logic:

let inline isNull (x:^T when ^T : not struct) = obj.ReferenceEquals (x, null)

type Type with
    member t.BaseTypesAndSelf() =
        t |> Seq.unfold (fun state -> if isNull state then None else Some(state, state.BaseType))
    member t.DictionaryKeyValueTypes() = 
        t.BaseTypesAndSelf()
            |> Seq.filter (fun i -> i.IsGenericType && i.GetGenericTypeDefinition() = typedefof<Dictionary<_,_>>)
            |> Seq.map (fun i -> i.GetGenericArguments())

type JsonReader with
    member r.ReadAndAssert() = 
        if not (r.Read()) then raise (JsonReaderException("Unexpected end of JSON stream."))
        r
    member r.MoveToContentAndAssert() =
        if r.TokenType = JsonToken.None then r.ReadAndAssert() |> ignore
        while r.TokenType = JsonToken.Comment do r.ReadAndAssert() |> ignore
        r

type internal DictionaryReadOnlySurrogate<'TKey, 'TValue>(i : IDictionary<'TKey, 'TValue>) =
    interface IReadOnlyDictionary<'TKey, 'TValue> with
        member this.ContainsKey(key) = i.ContainsKey(key)
        member this.TryGetValue(key, value) = i.TryGetValue(key, &value)
        member this.Item with get(index) = i.[index]
        member this.Keys = i.Keys :> IEnumerable<'TKey>
        member this.Values = i.Values :> IEnumerable<'TValue>
        member this.Count = i.Count
        member this.GetEnumerator() = i.GetEnumerator()
        member this.GetEnumerator() = i.GetEnumerator() :> IEnumerator        

type DictionaryConverter () =
    // ReadJson adapted from this answer https://stackoverflow.com/a/28633769/3744182
    // To https://stackoverflow.com/questions/28451990/newtonsoft-json-deserialize-dictionary-as-key-value-list-from-datacontractjsonse
    // By https://stackoverflow.com/users/3744182/dbc
    inherit JsonConverter()

    override this.CanConvert(t) = (t.DictionaryKeyValueTypes().Count() = 1) // If ever implemented for IReadOnlyDictionary<'TKey, 'TValue> then reject DictionaryReadOnlySurrogate<'TKey, 'TValue>

    member private this.ReadJsonGeneric<'TKey, 'TValue> (reader : JsonReader, t : Type, existingValue : obj, serializer : JsonSerializer) : obj =
        let contract = serializer.ContractResolver.ResolveContract(t)
        let dict = if (existingValue :? IDictionary<'TKey, 'TValue>) then existingValue :?> IDictionary<'TKey, 'TValue> else contract.DefaultCreator.Invoke() :?> IDictionary<'TKey, 'TValue>
        match reader.MoveToContentAndAssert().TokenType with 
        | JsonToken.StartArray -> 
            let l = serializer.Deserialize<List<KeyValuePair<'TKey, 'TValue>>>(reader)
            for p in l do dict.Add(p) 
            dict :> obj
        | JsonToken.StartObject ->
            serializer.Populate(reader, dict)
            dict :> obj
        | JsonToken.Null -> null // Or throw an exception if you prefer
        | _ -> raise (JsonSerializationException(String.Format("Unexpected token {0}", reader.TokenType)))

    override this.ReadJson(reader, t, existingValue, serializer) = 
        let keyValueTypes = t.DictionaryKeyValueTypes().Single(); // Throws an exception if not exactly one.
        let m = typeof<DictionaryConverter>.GetMethod("ReadJsonGeneric", BindingFlags.NonPublic ||| BindingFlags.Instance ||| BindingFlags.Public);
        m.MakeGenericMethod(keyValueTypes).Invoke(this, [| reader; t; existingValue; serializer |])

    member private this.WriteJsonGeneric<'TKey, 'TValue> (writer : JsonWriter, value : obj, serializer : JsonSerializer) =
        let dict = value :?> IDictionary<'TKey, 'TValue>
        let keyContract = serializer.ContractResolver.ResolveContract(typeof<'Key>)
        // Wrap the value in an enumerator or read-only surrogate to prevent infinite recursion.
        match keyContract with
        | :? JsonPrimitiveContract -> serializer.Serialize(writer, new DictionaryReadOnlySurrogate<'TKey, 'TValue>(dict)) 
        | _ -> serializer.Serialize(writer, seq { yield! dict }) 
        ()

    override this.WriteJson(writer, value, serializer) = 
        let keyValueTypes = value.GetType().DictionaryKeyValueTypes().Single(); // Throws an exception if not exactly one.
        let m = typeof<DictionaryConverter>.GetMethod("WriteJsonGeneric", BindingFlags.NonPublic ||| BindingFlags.Instance ||| BindingFlags.Public);
        m.MakeGenericMethod(keyValueTypes).Invoke(this, [| writer; value; serializer |])
        ()

Which you would add to settings as follows:

let settings =
    JsonSerializerSettings(
        NullValueHandling = NullValueHandling.Ignore,
        Converters = [| CompactUnionJsonConverter(true, true); DictionaryConverter() |]
    )

And generates the following JSON for your bookData:

{
  "Data": [
    {
      "Key": "Bid",
      "Value": [
        {
          "Key": 1,
          "Value": {
            "S": 3.0,
            "P": 5.0
          }
        }
      ]
    },
    {
      "Key": "Ask",
      "Value": []
    }
  ]
}

Notes:

  • The converter works for all Dictionary<TKey, TValue> types (and subtypes).

  • The converter detects whether the dictionary keys will be serialized using a primitive contract, and if so, serializes the dictionary compactly as a JSON object. If not the dictionary is serialized as an array. You can observe this in the JSON shown above: the Dictionary<BookSide, BookSideData> dictionary is serialized as a JSON array, and the Dictionary<int, BookEntry> dictionary is serialized as a JSON object.

    During deserialization the converter detects whether the incoming JSON value is an array or object, and adapts as required.

  • The converter is only implemented for the mutable .Net Dictionary<TKey, TValue> type. The logic would require some slight modification to deserialize the immutable Map<'Key,'Value> type.

Demo fiddle here.



来源:https://stackoverflow.com/questions/62377246/another-failure-at-deserializing-data-with-discriminated-unions-in-f

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!