Skip to content

FSharpLu.Json

William Blum edited this page Nov 5, 2019 · 5 revisions

FSharp.Json

Newtonsoft's Json.Net is the library of choice for Json serialization. Unfortunately the built-in converters generate rather verbose and ugly Json for certain F# data types like discriminated unions and option types. The module Microsoft.FSharpLu.Json provides more succinct serialization for those types.

To use our serializer open the module with

open Microsoft.FSharpLu.Json

To serialize an object using the default Json.Net format you can use Default.serialize.

Option type

The following examples shows how JSon.net serializes a simple value of type (int option) list:

Default.serialize [Some 5; None; Some 6]

val it : string = "
[
    {
        "Case": "Some",
        "Fields": [ 5 ]
    },
    null,
    {
        "Case": "Some",
        "Fields": [ 6 ]
    }
]"

Using the compact serializer provided by FSharpLu.Json the same objects gets serialized instead as a one-liner heterogeneous array:

Compact.serialize [Some 5; None; Some 6]

val it : string = "[ 5, null, 6 ]"

Discriminated unions

Now let's take a look at simple field-less discriminated unions. Take for instance the type type SimpleDu = Foo | Bar. The value Foo gets serialized by Json.Net as follows:

Value Default (Json.net) Compact
Foo { "Case": "Foo" } "Foo"

Discriminated unions with fields

Our serializer also supports generic discrimnated unions with fields for instance take the following binary Tree example:

type 'a Tree = Leaf of 'a | Node of 'a Tree * 'a Tree
let x = Node (Node((Leaf 1), (Leaf 4)), Leaf 6)

Default Json.net serialization:

Default.serialize x

val it : string = "
{
    "Case": "Node",
    "Fields": [
        {
            "Case": "Node",
            "Fields": [
                    {
                        "Case": "Leaf",
                        "Fields": [ 1 ]
                    },
                    {
                        "Case": "Leaf",
                        "Fields": [ 4 ]
                    }
                ]
        },
        {
            "Case": "Leaf",
            "Fields": [ 6 ]
        } ]
}"

where FSharpLu.Json produces the more succinct and easier to read:

Compact.serialize x

val it : string = "
{
    "Node": [
        {
            "Node": [
                { "Leaf": 1 },
                { "Leaf": 4 }
            ]
        },
        { "Leaf": 6 }
    ]
}"

Backward compatibility with Json.Net

FSharpLu.Json incldues a third serializer called BackwardCompatible. While it produces the same Json as the compact serializer it can deserialize Json in both compact format as well as the default format produced by the stock JSon.net serializer.

This is helpful when migrating projects from Json.Net to FSharpLu.Json as it lets you deserialize Json that was produced by earlier versions of your code.

Expressed more formally this serializer verifies the following properties:

  • BackwardCompatible.serialize = Compact.serialize
  • Default.serialize >> BackwardCompatible.deserialize = id
  • Compact.serialize >> BackwardCompatible.deserialize = id

Giraffe support

The following type can be used to set FSharpLu.Json as the default JSON serializer in Giraffe. See Giraffe doc for more details.

open Newtonsoft.Json
open System.Threading.Tasks

let Utf8EncodingWithoutBom = System.Text.UTF8Encoding(false)
let DefaultBufferSize = 1024

let Formatting = Microsoft.FSharpLu.Json.Compact.TupleAsArraySettings.formatting
let Settings = Microsoft.FSharpLu.Json.Compact.TupleAsArraySettings.settings
let serializer = JsonSerializer.Create Settings

/// A Giraffe serializer based on FSharpLu.Json
/// See https://github.com/giraffe-fsharp/Giraffe/blob/master/DOCUMENTATION.md#serialization
type FSharpLuJsonSerializer () =
    interface Giraffe.Serialization.Json.IJsonSerializer with
        member __.SerializeToString (o:'T) =
            JsonConvert.SerializeObject(o, Formatting, Settings)

        member __.SerializeToBytes<'T> (o: 'T) : byte array =
            JsonConvert.SerializeObject(o, Formatting, Settings)
            |> System.Text.Encoding.UTF8.GetBytes

        member __.SerializeToStreamAsync<'T> (o: 'T) (stream:System.IO.Stream) : Task =
            use sw = new System.IO.StreamWriter(stream, Utf8EncodingWithoutBom, DefaultBufferSize, true)
            use jw = new JsonTextWriter(sw, Formatting = Formatting)
            serializer.Serialize(jw, o)
            Task.CompletedTask

        member __.Deserialize<'T> (json:string) :'T =
            JsonConvert.DeserializeObject<'T>(json, Settings)

        member __.Deserialize<'T> (bytes:byte[]) :'T =
            let json = System.Text.Encoding.UTF8.GetString bytes
            JsonConvert.DeserializeObject<'T>(json, Settings)

        member __.DeserializeAsync (stream: System.IO.Stream) : Task<'T> =
            use streamReader = new System.IO.StreamReader(stream)
            use jsonTextReader = new JsonTextReader(streamReader)
            serializer.Deserialize<'T>(jsonTextReader)
            |> Task.FromResult

There is a known issue in Giraffe on .Net Core 3.0 https://github.com/giraffe-fsharp/Giraffe/issues/351 where serialization fails with Synchronous operations are disallowed. Call ReadAsync or set AllowSynchronousIO to true instead.. The issue has now been addressed in Giraffe 4.0 (https://github.com/giraffe-fsharp/Giraffe/pull/354/files). See also related https://github.com/JamesNK/Newtonsoft.Json/issues/1193. @artemkv has kindly provided the corresponding workaround for FSharpLu.Json:

open Newtonsoft.Json
open System.Threading.Tasks
open System.IO
open FSharp.Control.Tasks.V2.ContextInsensitive

let Utf8EncodingWithoutBom = System.Text.UTF8Encoding(false)
let DefaultBufferSize = 1024

let Formatting = Microsoft.FSharpLu.Json.Compact.TupleAsArraySettings.formatting
let Settings = Microsoft.FSharpLu.Json.Compact.TupleAsArraySettings.settings
let serializer = JsonSerializer.Create Settings

/// A Giraffe serializer based on FSharpLu.Json
/// See https://github.com/giraffe-fsharp/Giraffe/blob/master/DOCUMENTATION.md#serialization
type FSharpLuJsonSerializer() =
    interface Giraffe.Serialization.Json.IJsonSerializer with
        member __.SerializeToString(o: 'T) = JsonConvert.SerializeObject(o, Formatting, Settings)

        member __.SerializeToBytes<'T>(o: 'T): byte array =
            JsonConvert.SerializeObject(o, Formatting, Settings)
            |> System.Text.Encoding.UTF8.GetBytes

        member __.SerializeToStreamAsync (x : 'T) (stream : Stream) = 
            task {
                use memoryStream = new MemoryStream()
                use streamWriter = new StreamWriter(memoryStream, Utf8EncodingWithoutBom)
                use jsonTextWriter = new JsonTextWriter(streamWriter)
                serializer.Serialize(jsonTextWriter, x)
                jsonTextWriter.Flush()
                memoryStream.Seek(0L, SeekOrigin.Begin) |> ignore
                do! memoryStream.CopyToAsync(stream)
            } :> Task

        member __.Deserialize<'T>(json: string): 'T =
            JsonConvert.DeserializeObject<'T>(json, Settings)

        member __.Deserialize<'T>(bytes: byte []): 'T =
            let json = System.Text.Encoding.UTF8.GetString bytes
            JsonConvert.DeserializeObject<'T>(json, Settings)

        member __.DeserializeAsync(stream: System.IO.Stream): Task<'T> =
            task {
                use memoryStream = new MemoryStream()
                do! stream.CopyToAsync(memoryStream)
                memoryStream.Seek(0L, SeekOrigin.Begin) |> ignore
                use streamReader = new StreamReader(memoryStream)
                use jsonTextReader = new JsonTextReader(streamReader)
                return serializer.Deserialize<'T>(jsonTextReader)
            }
Clone this wiki locally