Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save pepeiborra/7f4b5414ae8fb85bc436f9a945214d6c to your computer and use it in GitHub Desktop.
Save pepeiborra/7f4b5414ae8fb85bc436f9a945214d6c to your computer and use it in GitHub Desktop.
This JSON.Net converter handles F# discriminated unions with slightly more "idiomatic" JSON than what is generated by the current version of JSON .NET. Tuple-style properties are used rather than array notation.
namespace DU.Json
open Microsoft.FSharp.Reflection
open Newtonsoft.Json
open Newtonsoft.Json.Serialization
open System
module Exts =
type TokenValue =
{ typ : JsonToken
value : obj option }
override this.ToString() = sprintf "%A" this
type JsonReader with
member r.Peek() =
{ typ = r.TokenType; value = match r.Value with | null -> None | v -> Some v }
member r.Next() = if r.Read() then Some(r.Peek()) else None
open Exts
/// Simple maybe monad
[<RequireQualifiedAccess>]
type Maybe<'a> =
{ run : unit -> option<'a> }
module Maybe =
let Nothing = {Maybe.run = fun () -> None}
let Just x = {Maybe.run = fun () -> Some x}
type MB() =
member this.Delay (x : unit -> Maybe<'a>) : Maybe<'a> = { Maybe.run = fun () -> x().run()}
member this.Return x = Just x
member this.Return(x:option<'a>) : Maybe<'a> = {Maybe.run = fun () -> x}
member this.Bind(x:option<'a>, k : 'a -> Maybe<'b>) =
{ Maybe.run = fun _ -> match x with | Some y -> k(y).run() | _ -> None }
member this.Bind(x:Maybe<'a>, k : 'a -> Maybe<'b>) =
{ Maybe.run = fun _ -> match x.run() with | Some y -> k(y).run() | _ -> None }
let maybe = MB()
let guard x = if x then maybe.Return(()) else Nothing
let run (it:Maybe<_>) = it.run()
let sequence (xx:Maybe<'a>[]) : Maybe<'a[]> =
{ Maybe.run = fun () ->
let result = Array.create xx.Length Unchecked.defaultof<'a>
let mutable i = 0
while(i > -1 && i < result.Length ) do
match xx.[i].run() with
| Some x ->
result.[i] <- x
i <- i+1
| None ->
i <- -1
if i >= 0 then Some result else None
}
open Maybe
[<AbstractClass>]
type ReflectionTypeInvoker<'b>() =
abstract f<'a> : unit -> 'b
member this.Invoke(typ) =
let invoker = this.GetType()
let m = invoker.GetMethod("f")
m.MakeGenericMethod([|typ|]).Invoke(this,[||]) :?> 'b
type DiscriminatedUnionConverter() =
inherit JsonConverter()
[<Literal>]
let discriminator = "__Case"
let primitives = Set [ JsonToken.Boolean; JsonToken.Date; JsonToken.Float; JsonToken.Integer; JsonToken.Null; JsonToken.String ]
let writeValue (value:obj) (serializer:JsonSerializer, writer : JsonWriter) =
if value.GetType().IsPrimitive then writer.WriteValue value
else serializer.Serialize(writer, value)
let writeProperties (fields : obj array) (serializer:JsonSerializer, writer : JsonWriter) =
fields |> Array.iteri (fun index value ->
writer.WritePropertyName(sprintf "Item%d" index)
(serializer, writer) |> writeValue value)
let writeDiscriminator (name : string) (writer : JsonWriter) =
writer.WritePropertyName discriminator
writer.WriteValue name
let readDiscriminator (reader : JsonReader) : Maybe<string> =
maybe {
let! propName = reader.Next()
do! Maybe.guard (propName.typ = JsonToken.PropertyName && propName.value = Some(box discriminator))
let! case = reader.Next()
do! Maybe.guard(case.typ = JsonToken.String)
let! v = case.value
return (v :?> string)
}
override __.WriteJson(writer, value, serializer) =
let unionType = value.GetType()
let case, fields = FSharpValue.GetUnionFields(value, unionType)
writer.WriteStartObject()
writer |> writeDiscriminator case.Name
(serializer, writer) |> writeProperties fields
writer.WriteEndObject()
override __.ReadJson(reader, destinationType, _, serializer) =
maybe{
let unionType = destinationType
let unionCases = FSharpType.GetUnionCases(unionType)
let firstToken = reader.Peek()
do! Maybe.guard(firstToken.typ = JsonToken.StartObject)
let! caseName = readDiscriminator reader
let! case = unionCases |> Seq.tryFind(fun x -> x.Name = caseName)
let fields = case.GetFields()
let! values =
[| for i in 0.. fields.Length - 1 ->
maybe{
let! propName = reader.Next()
do! Maybe.guard(propName.typ = JsonToken.PropertyName && (propName.value.Value :?> string) = "Item" + string i)
let! _ = reader.Next()
return { new ReflectionTypeInvoker<obj>() with
member this.f<'a>() =
box(serializer.Deserialize<'a>(reader))
}.Invoke(fields.[i].PropertyType)
}
|]
|> Maybe.sequence
let! lastToken = reader.Next()
do! Maybe.guard (lastToken.typ = JsonToken.EndObject)
return FSharpValue.MakeUnion(case, values)
}
|> Maybe.run
|> Option.toObj
override __.CanConvert(objectType) = FSharpType.IsUnion objectType
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment