In my last post, I gave an overview of Chiron, described how to parse JSON, and demonstrated how to
write ToJson and FromJson static methods to support serialization of custom types. At the end of the article, I
left a question hanging: What if you don't control the data type that you want to serialize? What if you can't add the
static ToJson/FromJson methods required by Chiron? That's where serialization extensions come in.
As an example, let's consider the NodaTime library. NodaTime provides a preferable set of types for interacting
with date/time values when compared to the BCL, and I regularly reference
NodaTime anywhere that I need to work with or manipulate time. While it is possible to convert an Instant to a
DateTimeOffset and then serialize that value, it would be much nicer to be able to serialize an Instant directly.
We will use the representation defined in ISO-8601 for serializing Instant values to JSON strings. We could
choose any other form, like WCF's \/Date(1234567890)\/, but representing a date/time in any format other than the
ISO standard generally leads to confusion.
Using NodaTime's facilities for formatting and parsing date/time strings we can define a serialization
extension for an Instant:
GeneralPattern provides serialization of Instants in the ISO-8601 format. If you prefer a compatible
representation with sub-second resolution, you can use the ExtendedIsoPattern instead.
The instantToJson function has the type signature Instant -> Json. This is different from the monadic Json<'a>
signature that was used in the serializers in the previous post. Instead of calling Json.serialize, instantToJson
can be used as a drop-in replacement.
Next we define the deserialization function. In doing so, I will also define an active pattern to encapsulate
NodaTime's pattern parsing logic. This will help to keep the intent of the deserialization function clear.
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13:
let (|ParsedInstant|InvalidInstant|) str=letparseResult=InstantPattern.GeneralPattern.ParsestrifparseResult.SuccessthenParsedInstantparseResult.ValueelseInvalidInstantletinstantFromJson=function
| String (ParsedInstanti) ->Valuei
| json->Json.formatWithJsonFormattingOptions.SingleLinejson|>sprintf"Expected a string containing a valid ISO-8601 date/time: %s"|>Error
instantFromJson has the type signature Json -> JsonResult<Instant>. Together with the signature of instantToJson,
these functions provide the complimentary facets of the Json<'a> state monad. JsonResult<'a> holds the working
value while Json stores the state of the JSON data that we are serializing or deserializing.
Now we can try to convert instantJson back to an Instant to validate that our serialization can round-trip.
We can also check that an invalid value produces a relevant error message during deserialization:
1: 2: 3: 4:
letinstantError=""" "Tomorrow at 13:30" """|>Json.parse|>instantFromJson
Error
"Expected a string containing a valid ISO-8601 date/time: "Tomorrow at 13:30""
This looks good so far. Of course, it is rare to want to serialize something like an Instant all on its own. More
commonly, the data is incorporated into a larger JSON object or array. In this case, there are a few additional
read and write functions that provide points to inject custom (de)serialization functions: readWith and writeWith.
To demonstrate their use, we will consider a trivial type containing an Instant and add FromJson and ToJson
static methods.
Note how instantToJson and instantFromJson are injected as the first argument to Json.writeWith and
Json.readWith, respectively. Now we can demonstrate round-trip serialization:
We now have serializers that can round-trip an external type either alone or as part of a type we control. What if the
external type is contained in yet another external type? To demonstrate this case, we will consider serializing an
Instant list:
Before we even get to running this code, the compiler has already started disagreeing with us:
1:
Error: No overloads match for method 'ToJson'. …
Where did we go wrong? The issue is that an a' list is serializable through Chiron's default functions if and only
if 'a contains the necessary ToJson/FromJson functions. Instant doesn't have the needed hooks needed for
Chiron's default serialization functions. Since we wrote our own serialization for Instant, we need need to write a
function to serialize our list too. Instead of defining a specialized Instant list serializer, we can instead write a
generic 'a list serializer and include a parameter so that we can plug in an arbitrary serializer.
The deserializer is a little more complicated because we can't just map it over the list.
We need a function that maps Json -> JsonResult<Instant list>. Chiron already has a function that fits this need:
fromJsonFold, which it uses to support default serialization of arrays and lists.
fromJsonFold iterates over a Json list wrapped by a Json.Array and produces a JsonResult over the list. This
function is marked internal, though, so we don't have direct access to it. Instead, we can extract the function's
logic and refactor it to fit our needs. Replacing fromJson with a new deserialize parameter gives us a generic
function for applying a custom deserializer over a Json list.
fromJsonFoldWith is likely to be added in to Chiron in a future version, but for now, our custom serialization
functions suffice as demonstrated by another round-trip:
Value [1970-01-01T00:00:00Z; 2016-04-13T14:30:46Z]
Thus far, I've only been creating custom serializers for records and tuples, a.k.a. product types,
and none of these serializers would deal well if they were given an object with missing data or null values:
In order to handle missing data or null values and other discriminated unions, a.k.a. sum types,
we will need to learn about a few more tricks that Chiron has up its sleeve. In my next post, I will focus
on the Chiron features that allow you to provide defaults for missing values and serialize the disjoint cases of
a discriminated union.
This post is a continuation of my post for 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.
module Chiron
module Operators
from Chiron
namespace NodaTime
namespace NodaTime.Text
val instantToJson : i:Instant -> Json
Full name: 12-15-chiron-taming-types-in-the-wild_.instantToJson
val i : Instant
Multiple items union case Json.String: string -> Json
-------------------- module String
from Microsoft.FSharp.Core
type InstantPattern = member Format : value:Instant -> string member Parse : text:string -> ParseResult<Instant> member PatternText : string member WithCulture : cultureInfo:CultureInfo -> InstantPattern member WithMinMaxLabels : minLabel:string * maxLabel:string -> InstantPattern static member Create : patternText:string * cultureInfo:CultureInfo -> InstantPattern static member CreateNumericPattern : cultureInfo:CultureInfo * includeThousandsSeparators:bool -> InstantPattern static member CreateWithCurrentCulture : patternText:string -> InstantPattern static member CreateWithInvariantCulture : patternText:string -> InstantPattern static member ExtendedIsoPattern : InstantPattern ...
val sprintf : format:Printf.StringFormat<'T> -> 'T
Full name: Microsoft.FSharp.Core.ExtraTopLevelOperators.sprintf
union case JsonResult.Error: string -> JsonResult<'a>
val instantRoundTrip : JsonResult<Instant>
Full name: 12-15-chiron-taming-types-in-the-wild_.instantRoundTrip
val parse : (string -> Json)
Full name: Chiron.Parsing.Json.parse
val instantError : JsonResult<Instant>
Full name: 12-15-chiron-taming-types-in-the-wild_.instantError
type MyType = {Time: Instant;} static member FromJson : MyType -> Json<MyType> static member ToJson : x:MyType -> Json<unit>
Full name: 12-15-chiron-taming-types-in-the-wild_.MyType
MyType.Time: Instant
Multiple items type Instant = struct new : ticks:int64 -> Instant member CompareTo : other:Instant -> int member Equals : obj:obj -> bool + 1 overload member GetHashCode : unit -> int member InUtc : unit -> ZonedDateTime member InZone : zone:DateTimeZone -> ZonedDateTime + 1 overload member Minus : other:Instant -> Duration + 1 overload member Plus : duration:Duration -> Instant member PlusTicks : ticksToAdd:int64 -> Instant member Ticks : int64 ... end
Full name: NodaTime.Instant
-------------------- Instant() Instant(ticks: int64) : unit
static member MyType.ToJson : x:MyType -> Json<unit>
Full name: 12-15-chiron-taming-types-in-the-wild_.MyType.ToJson
static member MyType.FromJson : MyType -> Json<MyType>
Full name: 12-15-chiron-taming-types-in-the-wild_.MyType.FromJson
val t : Instant
val readWith : fromJson:(Json -> JsonResult<'a>) -> key:string -> Json<'a>
Full name: Chiron.Mapping.Json.readWith
val myTypeJson : string
Full name: 12-15-chiron-taming-types-in-the-wild_.myTypeJson
val serialize : a:'a -> Json (requires member ToJson)
Full name: Chiron.Mapping.Json.serialize
val myTypeRoundTrip : MyType
Full name: 12-15-chiron-taming-types-in-the-wild_.myTypeRoundTrip
val deserialize : json:Json -> 'a (requires member FromJson)
Full name: Chiron.Mapping.Json.deserialize
val listOfInstantJson : obj
Full name: chirontamingtypesinthewild.listOfInstantJson
val listToJsonWith : serialize:('a -> Json) -> lst:'a list -> Json
Full name: 12-15-chiron-taming-types-in-the-wild_.listToJsonWith
val serialize : ('a -> Json)
val lst : 'a list
Multiple items union case Json.Array: Json list -> Json
-------------------- module Array
from Microsoft.FSharp.Collections
Multiple items module List
from Microsoft.FSharp.Collections
-------------------- type List<'T> = | ( [] ) | ( :: ) of Head: 'T * Tail: 'T list interface IEnumerable interface IEnumerable<'T> member GetSlice : startIndex:int option * endIndex:int option -> 'T list member Head : 'T member IsEmpty : bool member Item : index:int -> 'T with get member Length : int member Tail : 'T list static member Cons : head:'T * tail:'T list -> 'T list static member Empty : 'T list
Full name: Microsoft.FSharp.Collections.List<_>
val map : mapping:('T -> 'U) -> list:'T list -> 'U list
Full name: Microsoft.FSharp.Collections.List.map
val fromJsonFoldWith : deserialize:('a -> JsonResult<'b>) -> fold:('b -> 'c -> 'c) -> zero:'c -> xs:'a list -> JsonResult<'c>
Full name: 12-15-chiron-taming-types-in-the-wild_.fromJsonFoldWith
val deserialize : ('a -> JsonResult<'b>)
val fold : ('b -> 'c -> 'c)
val zero : 'c
val xs : 'a list
val fold : folder:('State -> 'T -> 'State) -> state:'State -> list:'T list -> 'State
Full name: 12-15-chiron-taming-types-in-the-wild_.listFromJsonWith
val deserialize : (Json -> JsonResult<'a>)
val l : Json list
val listOfInstantJson : string
Full name: 12-15-chiron-taming-types-in-the-wild_.listOfInstantJson
val listOfInstantRoundTrip : JsonResult<Instant list>
Full name: 12-15-chiron-taming-types-in-the-wild_.listOfInstantRoundTrip
val result : Choice<MyType,string>
Full name: 12-15-chiron-taming-types-in-the-wild_.result
Multiple items type Choice<'T1,'T2> = | Choice1Of2 of 'T1 | Choice2Of2 of 'T2
Full name: Microsoft.FSharp.Core.Choice<_,_>
-------------------- type Choice<'T1,'T2,'T3> = | Choice1Of3 of 'T1 | Choice2Of3 of 'T2 | Choice3Of3 of 'T3
Full name: Microsoft.FSharp.Core.Choice<_,_,_>
-------------------- type Choice<'T1,'T2,'T3,'T4> = | Choice1Of4 of 'T1 | Choice2Of4 of 'T2 | Choice3Of4 of 'T3 | Choice4Of4 of 'T4
Full name: Microsoft.FSharp.Core.Choice<_,_,_,_>
-------------------- type Choice<'T1,'T2,'T3,'T4,'T5> = | Choice1Of5 of 'T1 | Choice2Of5 of 'T2 | Choice3Of5 of 'T3 | Choice4Of5 of 'T4 | Choice5Of5 of 'T5
Full name: Microsoft.FSharp.Core.Choice<_,_,_,_,_>
-------------------- type Choice<'T1,'T2,'T3,'T4,'T5,'T6> = | Choice1Of6 of 'T1 | Choice2Of6 of 'T2 | Choice3Of6 of 'T3 | Choice4Of6 of 'T4 | Choice5Of6 of 'T5 | Choice6Of6 of 'T6
Full name: Microsoft.FSharp.Core.Choice<_,_,_,_,_,_>
-------------------- type Choice<'T1,'T2,'T3,'T4,'T5,'T6,'T7> = | Choice1Of7 of 'T1 | Choice2Of7 of 'T2 | Choice3Of7 of 'T3 | Choice4Of7 of 'T4 | Choice5Of7 of 'T5 | Choice6Of7 of 'T6 | Choice7Of7 of 'T7
Full name: Microsoft.FSharp.Core.Choice<_,_,_,_,_,_,_>
val tryDeserialize : json:Json -> Choice<'a,string> (requires member FromJson)