Grim's Scythe
Marcus Griep

Writings on software engineering.

Recent posts


Synchronous messaging and lightweight threading


Powerful JSON processing with computation expressions


Using Chiron to serialize types that you can't control


Getting started with Chiron


Getting started — all over again

Chiron: JSON + Ducks + Monads

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.

As an example, let's create a data type for a user:
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.

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