JSON Converter for F# Map Type
Thursday, March 21, 2013 at 11:14AM Newtonsoft's Json.NET library provides an extension mechanism to add support for the serialization of additional types not natively supported by the library. This is handy for the F# developer wanting to work with JSON. Here I add support for F#'s Map type by defining a MapTypeConverter. This code will be made available, along with other converters, in the FSharp.Enterprise library on github in Json.fs.
type MapTypeConverter() =
inherit JsonConverter()
let doRead (reader:JsonReader) =
reader.Read() |> ignore
let readKeyValuePair (serializer:JsonSerializer) (argTypes:Type []) (reader:JsonReader) =
doRead reader // "key"
doRead reader // value
let key = serializer.Deserialize(reader, argTypes.[0])
doRead reader // "value"
doRead reader // value
let value = serializer.Deserialize(reader, argTypes.[1])
doRead reader // }
FSharpValue.MakeTuple([|key;value|], FSharpType.MakeTupleType(argTypes))
let readArray elementReaderF (reader:JsonReader) =
doRead reader // [
if reader.TokenType = JsonToken.StartArray then
[|
while reader.TokenType <> JsonToken.EndArray do
doRead reader // {
if reader.TokenType = JsonToken.StartObject then
let element = elementReaderF reader
yield element
|]
else
Array.empty
let writeKeyValuePair (serializer:JsonSerializer) (writer:JsonWriter) kv =
let kvType = kv.GetType()
let k = kvType.GetProperty("Key").GetValue(kv, null)
let v = kvType.GetProperty("Value").GetValue(kv, null)
writer.WriteStartObject()
writer.WritePropertyName("key")
serializer.Serialize(writer,k)
writer.WritePropertyName("value")
serializer.Serialize(writer,v)
writer.WriteEndObject()
let writeArray elementWriterF (writer:JsonWriter) (kvs:System.Collections.IEnumerable) =
writer.WriteStartArray()
for kv in kvs do
elementWriterF writer kv
writer.WriteEndArray()
override x.CanConvert(typ:Type) =
typ.IsGenericType && typ.GetGenericTypeDefinition() = typedefof<Map<_,_>>
override x.WriteJson(writer:JsonWriter, value:obj, serializer:JsonSerializer) =
if value <> null then
let valueType = value.GetType()
if valueType.IsGenericType then
let baseType = valueType.GetGenericTypeDefinition()
if baseType = typedefof<Map<_,_>> then
let kvs = value :?> System.Collections.IEnumerable
writer.WriteStartObject()
writer.WritePropertyName("pairs")
writeArray (writeKeyValuePair serializer) writer kvs
writer.WriteEndObject()
override x.ReadJson(reader:JsonReader, objectType:Type, existingValue:obj, serializer:JsonSerializer) =
let argTypes = objectType.GetGenericArguments()
let tupleType = FSharpType.MakeTupleType(argTypes)
let constructedIEnumerableType = typedefof<IEnumerable<_>>.GetGenericTypeDefinition().MakeGenericType(tupleType)
if reader.TokenType <> JsonToken.Null then
doRead reader // "pairs"
let kvs = readArray (readKeyValuePair serializer argTypes) reader
doRead reader // }
let kvs' = System.Array.CreateInstance(tupleType, kvs.Length)
System.Array.Copy(kvs, kvs', kvs.Length)
let methodInfo = objectType.GetMethod("Create", BindingFlags.Static ||| BindingFlags.NonPublic, null, [|constructedIEnumerableType|], null)
methodInfo.Invoke(null, [|kvs'|])
else
box Map.empty
The MapTypeConverter can be added to the list of converters used by the Newtonsoft JSON library:
let settings =
let jss = new JsonSerializerSettings()
jss.Converters.Add(new MapTypeConverter())
...
jss
Add a couple of helper functions:
let ofObject payload =
JsonConvert.SerializeObject(payload,Formatting.None, settings)
let toObject<'a> js =
JsonConvert.DeserializeObject<'a>(js, settings)
Then F# Maps can be converted to and from JSON like so:
let m = List.zip ["a";"b";"c"] [1..3] |> Map.ofList
let j = Json.ofObject m
let o =Json.toObject<Map<string,int>> j
m = o
Resulting in the following:
val m : Map<string,int> = map [("a", 1); ("b", 2); ("c", 3)]
val j : string =
"{"pairs":[{"key":"a","value":1},{"key":"b","value":2},{"key":"+[16 chars]
val o : Map<string,int> = map [("a", 1); ("b", 2); ("c", 3)]
val it : bool = true
