-
Notifications
You must be signed in to change notification settings - Fork 39
FSharpLu.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
.
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 ]"
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" |
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 }
]
}"
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
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)
}