Dec 15, 2015 Using Chiron to serialize types that you can't control

Chiron: JSON + Ducks + Monads

Dec 13, 2015

There are a multitude of ways to handle JSON data on the .NET framework. You can pull in Json.NET, use the JsonSerializer from the now proprietary ServiceStack.Text, or use the JsonDataContractSerializer provided by the Base Class Libraries. Developers in F# have access to the strongly-typed erasing type provider through FSharp.Data's JsonProvider . In terms of simplicity, though, Chiron delivers a fully-functional JSON serializer in a compact, single-file implementation.

At the core of Chiron is a simple discriminated union:

1: 2: 3: 4: 5: 6: 7: type Json = | Null of unit | Bool of bool | String of string | Number of decimal | Array of Json list | Object of Map < string , Json >

Serialization of a Json instance to a JSON string is handled by the format and formatWith functions:

1: 2: 3: 4: 5: 6: 7: 8: 9: let formatExample = Object <| Map . ofList [ "name" , String "Marcus Griep" "isAdmin" , Bool true "numbers" , Array [ Number 1m ; Number 2m ; String "Fizz" ] ] let formatCompact = Json . format formatExample let formatPretty = Json . formatWith JsonFormattingOptions . Pretty formatExample

By default, Chrion formats JSON in a compact form:

"{"isAdmin":true,"name":"Marcus Griep","numbers":[1,2,"Fizz"]}"

By specifying custom formatting options, you can get a more readable print out.

"{ "isAdmin": true, "name": "Marcus Griep", "numbers": [ 1, 2, "Fizz" ] }"

Text is turned into Json with the parse and tryParse functions. Chiron implements a custom FParsec parser to convert strings into corresponding Json instances. For example:

1: 2: 3: 4: 5: 6: Json . parse """ { "foo": [ { "bar": 1 }, { "bar": 2 }, { "bar": "fizz" } ], "test": { "one":3.5, "two":null, "three":false } } """

Parses into the following Json :

Object (map [("foo", Array [Object (map [("bar", Number 1M)]); Object (map [("bar", Number 2M)]); Object (map [("bar", String "fizz")])]); ("test", Object (map [("one", Number 3.5M); ("three", Bool false); ("two", Null null)]))])

There are several reasons that parsing a JSON structure might fail. Using the tryParse function will return a Choice2Of2 in the event parsing fails.

1: 2: Json . tryParse """{ "foo": [ { "bar": 1 }, { "bar": 2 } { "bar": "fizz" } ] }"""

This results in an error message clearly indicating where the parsing error occurred.

Choice2Of2 "Error in Ln: 1 Col: 39 { "foo": [ { "bar": 1 }, { "bar": 2 } { "bar": "fizz" } ] } ^ Expecting: ',' or ']' "

Converting data from Json to string and back again is all well and good, but every JSON library needs to provide a means to convert JSON strings into Plain Old [insert language] Objects. Most .NET converters rely on reflection to inspect data objects and perform conversion by convention. Chiron doesn't rely on convention or decorate members with attributes. Instead, any type that has the static methods FromJson and ToJson can be serialized or deserialized. Chiron's serialize and deserialize functions use statically-resolved type parameters, similar to duck-typing, to hook in to the appropriate methods at compile time.

1: 2: 3: type User = { Name : string IsAdmin : bool }

For an explanation that uses fewer custom operators and may be easier to follow, check out this article on the json{} computation expression.

As an example, let's create a data type for a user:

Chiron uses a monadic type, Json<'a> , to build up the serialized Json type:

1: 2: 3: static member ToJson ( x : User ) = Json . write "name" x . Name *> Json . write "isAdmin" x . IsAdmin

The ToJson function consciously separates the name of the field in code from its representation in a JSON object. This allows them to vary independently. This way we can later change how we refer to the field in code, without accidentally breaking our JSON contract. ToJson takes two parameters, a User and a Json . That second parameter is hidden in the Json<'a> return type. Json<'a> is a state monad which we use to build up a Json instance in one direction, and extract values out of a Json instance in the other. Json<'a> is represented by the following signature.

1: type Json < ' a > = Json -> JsonResult < ' a > * Json

The *> operator that we used in ToJson discards the JsonResult<'a> (which is only used when writing), but continues to build upon the Json object from the previous operation. By chaining these operations together, we build up the members of a Json.Object .

Deserialization is done using FromJson :

1: 2: 3: 4: 5: 6: static member FromJson (_ : User ) = fun n a -> { Name = n IsAdmin = a } <!> Json . read "name" <*> Json . read "isAdmin"

The FromJson function reads its value out of the implicit Json instance provided by Json<'a> . The dummy User parameter is used by the F# compiler to resolve the statically-resolved type parameter on the Json.deserialize function. The FromJson function makes use of lift and apply functions from our Json<'a> monad, which are identified by the custom operators <!> and <*> , respectively.

With these two functions defined, we can serialize an instance of our custom type:

1: 2: 3: { Name = "Marcus Griep" ; IsAdmin = true } |> Json . serialize |> Json . formatWith JsonFormattingOptions . Pretty

"{ "isAdmin": true, "name": "Marcus Griep" }"

And deserialize it:

1: 2: 3: 4: let deserializedUser : User = """{"name":"Marcus Griep","isAdmin":true}""" |> Json . parse |> Json . deserialize

{Name = "Marcus Griep"; IsAdmin = true;}

Chiron provides built-in serializers for common primitives, such as int , string , DateTimeOffset , as well as arrays, lists, sets, and simple tuples.

This should give you a start toward serializing and deserializing your own custom types, but what about types that you don't control? We'll take a look at how to provide custom serializers for those types, in my next post.

This post is a part of the F# Advent Calendar in English. Many thanks to Sergey Tihon for promoting this event. For more posts on F# and functional programming throughout December, check out the list of posts on his site.

union case Json.Null: unit -> Json

type unit = Unit



Full name: Microsoft.FSharp.Core.unit

union case Json.Bool: bool -> Json

type bool = System.Boolean



Full name: Microsoft.FSharp.Core.bool

Multiple items

union case Json.String: string -> Json



--------------------

module String



from Microsoft.FSharp.Core

Multiple items

val string : value:'T -> string



Full name: Microsoft.FSharp.Core.Operators.string



--------------------

type string = System.String



Full name: Microsoft.FSharp.Core.string

union case Json.Number: decimal -> Json

Multiple items

val decimal : value:'T -> decimal (requires member op_Explicit)



Full name: Microsoft.FSharp.Core.Operators.decimal



--------------------

type decimal = System.Decimal



Full name: Microsoft.FSharp.Core.decimal



--------------------

type decimal<'Measure> = decimal



Full name: Microsoft.FSharp.Core.decimal<_>

Multiple items

union case Json.Array: Json list -> Json



--------------------

module Array



from Microsoft.FSharp.Collections

type Json =

| Null of unit

| Bool of bool

| String of string

| Number of decimal

| Array of Json list

| Object of Map<string,Json>



Full name: 12-13-chiron-json-ducks-monads_.Json

type 'T list = List<'T>



Full name: Microsoft.FSharp.Collections.list<_>

union case Json.Object: Map<string,Json> -> Json

Multiple items

module Map



from Microsoft.FSharp.Collections



--------------------

type Map<'Key,'Value (requires comparison)> =

interface IEnumerable

interface IComparable

interface IEnumerable<KeyValuePair<'Key,'Value>>

interface ICollection<KeyValuePair<'Key,'Value>>

interface IDictionary<'Key,'Value>

new : elements:seq<'Key * 'Value> -> Map<'Key,'Value>

member Add : key:'Key * value:'Value -> Map<'Key,'Value>

member ContainsKey : key:'Key -> bool

override Equals : obj -> bool

member Remove : key:'Key -> Map<'Key,'Value>

...



Full name: Microsoft.FSharp.Collections.Map<_,_>



--------------------

new : elements:seq<'Key * 'Value> -> Map<'Key,'Value>

module Chiron

module Operators



from Chiron

val marcusJson : Json



Full name: 12-13-chiron-json-ducks-monads_.marcusJson

val ofList : elements:('Key * 'T) list -> Map<'Key,'T> (requires comparison)



Full name: Microsoft.FSharp.Collections.Map.ofList

Multiple items

module Json



from Chiron.Mapping



--------------------

module Json



from Chiron.Formatting



--------------------

module Json



from Chiron.Parsing



--------------------

module Json



from Chiron.Optics



--------------------

module Json



from Chiron.Functional



--------------------

type Json =

| Array of Json list

| Bool of bool

| Null of unit

| Number of decimal

| Object of Map<string,Json>

| String of string

static member Array_ : Prism<Json,Json list>

static member private Array__ : (Json -> Json list option) * (Json list -> Json)

static member Bool_ : Prism<Json,bool>

static member private Bool__ : (Json -> bool option) * (bool -> Json)

static member Null_ : Prism<Json,unit>

static member private Null__ : (Json -> unit option) * (unit -> Json)

static member Number_ : Prism<Json,decimal>

static member private Number__ : (Json -> decimal option) * (decimal -> Json)

static member Object_ : Prism<Json,Map<string,Json>>

static member private Object__ : (Json -> Map<string,Json> option) * (Map<string,Json> -> Json)

static member String_ : Prism<Json,string>

static member private String__ : (Json -> string option) * (string -> Json)



Full name: Chiron.Json



--------------------

type Json<'a> = Json -> JsonResult<'a> * Json



Full name: 12-13-chiron-json-ducks-monads_.Json<_>

type JsonResult<'a> =

| Value of 'a

| Error of string



Full name: Chiron.Functional.JsonResult<_>

val formatExample : Json



Full name: 12-13-chiron-json-ducks-monads_.formatExample

val formatCompact : string



Full name: 12-13-chiron-json-ducks-monads_.formatCompact

val format : json:Json -> string



Full name: Chiron.Formatting.Json.format

val formatPretty : string



Full name: 12-13-chiron-json-ducks-monads_.formatPretty

val formatWith : options:JsonFormattingOptions -> json:Json -> string



Full name: Chiron.Formatting.Json.formatWith

type JsonFormattingOptions =

{Spacing: StringBuilder -> StringBuilder;

NewLine: int -> StringBuilder -> StringBuilder;}

static member Compact : JsonFormattingOptions

static member Pretty : JsonFormattingOptions

static member SingleLine : JsonFormattingOptions



Full name: Chiron.Formatting.JsonFormattingOptions

property JsonFormattingOptions.Pretty: JsonFormattingOptions

val parse : (string -> Json)



Full name: Chiron.Parsing.Json.parse

val tryParse : (string -> Choice<Json,string>)



Full name: Chiron.Parsing.Json.tryParse

type User =

{Name: string;

IsAdmin: bool;}

static member FromJson : User -> Json<User>

static member ToJson : x:User -> Json<unit>



Full name: 12-13-chiron-json-ducks-monads_.User

User.Name: string

User.IsAdmin: bool

static member User.ToJson : x:User -> Json<unit>



Full name: 12-13-chiron-json-ducks-monads_.User.ToJson

val x : User

val write : key:string -> value:'a -> Json<unit> (requires member ToJson)



Full name: Chiron.Mapping.Json.write

static member User.FromJson : User -> Json<User>



Full name: 12-13-chiron-json-ducks-monads_.User.FromJson

val n : string

val a : bool

val read : key:string -> Json<'a> (requires member FromJson)



Full name: Chiron.Mapping.Json.read

val serialize : a:'a -> Json (requires member ToJson)



Full name: Chiron.Mapping.Json.serialize

val deserializedUser : User



Full name: 12-13-chiron-json-ducks-monads_.deserializedUser

val deserialize : json:Json -> 'a (requires member FromJson)



Full name: Chiron.Mapping.Json.deserialize