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:
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:
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:
typeUser=
{ Name:stringIsAdmin: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:
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:
typeJson<'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.
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:
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