user1607808
user1607808

Reputation: 127

Deserializing F# Map by Json.Net

I have one simply question: Is it possible to parse F# Map type from ? Because when I try it (With F# Map<string, string>), it is easy to serialize and it looks how it have to, but when I try to deserialize it is throwing an exception.

Newtonsoft.Json.JsonSerializationException: Unable to find a default constructor to use for type Microsoft.FSharp.Collections.FSharpMap`2[System.Int32,System.String]. Path '1', line 2, position 7.
  at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateNewDictionary (Newtonsoft.Json.JsonReader reader, Newtonsoft.Json.Serialization.JsonDictionaryContract contract, System.Boolean& createdFromNonDefaultConstructor) [0x00000] in <filename unknown>:0 
  at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateObject (Newtonsoft.Json.JsonReader reader, System.Type objectType, Newtonsoft.Json.Serialization.JsonContract contract, Newtonsoft.Json.Serialization.JsonProperty member, Newtonsoft.Json.Serialization.JsonContainerContract containerContract, Newtonsoft.Json.Serialization.JsonProperty containerMember, System.Object existingValue) [0x00000] in <filename unknown>:0 
  at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateValueInternal (Newtonsoft.Json.JsonReader reader, System.Type objectType, Newtonsoft.Json.Serialization.JsonContract contract, Newtonsoft.Json.Serialization.JsonProperty member, Newtonsoft.Json.Serialization.JsonContainerContract containerContract, Newtonsoft.Json.Serialization.JsonProperty containerMember, System.Object existingValue) [0x00000] in <filename unknown>:0 
  at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.Deserialize (Newtonsoft.Json.JsonReader reader, System.Type objectType, Boolean checkAdditionalContent) [0x00000] in <filename unknown>:0

And it is deserializing from classic:

Map.ofList [ ("1", "one"); ("2", "two"); ("3", "three") ]

The resulting JSON looks like C# dictionary

{
  "1": "one",
  "2": "two",
  "3": "three"
}

It is serializing without settings (Only indentation). So is it possible to serialize this, or is there some working workaround?

Thanks for answer

Upvotes: 5

Views: 2792

Answers (4)

Paul Westcott
Paul Westcott

Reputation: 911

This functionality became part of JSON.Net in version 6.0.3. (April 30th, 2014)

But, if you are stuck for some reason using an earlier version then a simplified (and more efficient as less reflection) version of Dax Fohl's version could be:

type mapConvert<'f,'t when 'f : comparison>() =
    static member readJson (reader:JsonReader, serializer:JsonSerializer) =
        serializer.Deserialize<Dictionary<'f, 't>> (reader)
        |> Seq.map (fun kv -> kv.Key, kv.Value)
        |> Map.ofSeq

let mapConverter = {
  new JsonConverter() with
    override __.CanConvert (t:Type) =
      t.IsGenericType && t.GetGenericTypeDefinition() = typedefof<Map<_, _>>

    override __.WriteJson (writer, value, serializer) =
      serializer.Serialize(writer, value)

    override __.ReadJson (reader, t, _, serializer) =
      let converter = 
        typedefof<mapConvert<_,_>>.MakeGenericType (t.GetGenericArguments())

      let readJson =
        converter.GetMethod("readJson")

      readJson.Invoke(null, [| reader; serializer |])
}

Upvotes: 2

Dax Fohl
Dax Fohl

Reputation: 10781

You can make your own converter to do this. It's a lot of reflection and constructing appropriate generic types, but it can be done.

You first deserialize to a Dictionary<Key, Val>, then create and fill a List<Tuple<Key, Val>> manually via reflection (because the Map constructor requires Tuples, not KeyValuePairs), then finally pass that into the Map constructor.

Not sure if there's an easier way, but this is what I came up with:

open System
open System.Collections
open System.Collections.Generic
open Newtonsoft.Json

let mapConverter = {
  new JsonConverter() with

    override x.CanConvert(t:Type) =
      t.IsGenericType && t.GetGenericTypeDefinition() = typedefof<Map<_, _>>

    override x.WriteJson(writer, value, serializer) =
      serializer.Serialize(writer, value)

    override x.ReadJson(reader, t, _, serializer) =
      let genArgs = t.GetGenericArguments()
      let generify (t:Type) = t.MakeGenericType genArgs
      let tupleType = generify typedefof<Tuple<_, _>>
      let listType = typedefof<List<_>>.MakeGenericType tupleType
      let create (t:Type) types = (t.GetConstructor types).Invoke
      let list = create listType [||] [||] :?> IList
      let kvpType = generify typedefof<KeyValuePair<_, _>>
      for kvp in serializer.Deserialize(reader, generify typedefof<Dictionary<_, _>>) :?> IEnumerable do
        let get name = (kvpType.GetProperty name).GetValue(kvp, null)
        list.Add (create tupleType genArgs [|get "Key"; get "Value"|]) |> ignore        
      create (generify typedefof<Map<_, _>>) [|listType|] [|list|]
}

Once you have your converter, then you just pass it into the DeserializeObject method and JsonConvert will use it wherever appropriate.

let str = JsonConvert.SerializeObject (Map<_, _> [333, 1234])
JsonConvert.DeserializeObject<Map<int, int>>(str, mapConverter)

The nice thing about doing it this way is that if you've got a big/deep record where your Map is just a single field, then it'll work with that too--you don't have to go changing your record structure to use Dictionaries just to support serialization.

Upvotes: 3

Eriawan Kusumawardhono
Eriawan Kusumawardhono

Reputation: 4906

You can't serialize F#'s Map directly, since it has no default contructor (constructor with no parameter) at all.

This is the original documentation of F# map: (from http://msdn.microsoft.com/en-us/library/ee353686%28v=vs.110%29.aspx)

[<Sealed>]
type Map<[<EqualityConditionalOnAttribute>] 'Key,[<ComparisonConditionalOnAttribute>] [<EqualityConditionalOnAttribute>] 'Value (requires comparison)> =
    class
interface IEnumerable
interface IComparable
interface IEnumerable
interface ICollection
interface IDictionary
new Map : seq<'Key * 'Value> -> Map< 'Key, 'Value>
member this.Add : 'Key * 'Value -> Map<'Key, 'Value>
member this.ContainsKey : 'Key -> bool
member this.Remove : 'Key -> Map<'Key, 'Value>
member this.TryFind : 'Key -> 'Value option
member this.Count :  int
member this.IsEmpty :  bool
member this.Item ('Key) : 'Value
end

As you see above, Map doesn't have default constructor but the serializer need a class with default constructor.

The best way to serialize a map is mapping the map to be regular .NET dictionary, but then the new dictionary doesn't have all of the advantages of F#'s Map, especially the immutability of F#'s Map.

Upvotes: 0

Simon Dickson
Simon Dickson

Reputation: 711

The problem is that json.net can't construct a Map<int,string>. However if you deserialize to a regular .net Dictionary<int,string> it will work, as the json is the same.

Upvotes: 1

Related Questions