# Fleece Json Library [Fleece](https://github.com/fsprojects/Fleece) is a json mapper for F#. The library uses either `System.Json`, `System.Text.Json`, `FSharp.Data`, or `Json.NET` as the underlining deserializer. There is a separate nuget package for each. You should prefer [Fleece.SystemTextJson](https://www.nuget.org/packages/Fleece.SystemTextJson/) (`#r "nuget:Fleece.SystemTextJson"`). - Types are automatically serializable if they have two static members (`ToJson` and `OfJson`), or, in case of objects, a `get_Codec ()`) - To do the parsing you should use `ofJsonText : string -> Result<'a, DecodeError>` and `toJsonText`. Note: for some of these examples you may need to reference [[FSharpPlus]] (`#r "nuget: FSharpPlus, 1.2.*"`), and open `Fleece.SystemTextJson` and `Fleece.SystemTextJson.Operators`. ## Understanding Codecs A `Codec<S1, S2, t1, t2` is a combination of two functions, a `Decoder<S1, t1>` and an `Encoder<S2, t2>`. The decoder defines a way of converting `S1` to `t1` and the encoder converts `t2` to `S2`. Think of `S1` and `S2` as your raw or soft types (hence the 's'), such as `JsonValue` and of `t1` and `t2` as hard types. A simpler `Codec<S,t>` also exists (with two generic parameters) and this is the same as `Codec<S,S,t,t>`. The exact definitions of the encoder and decoders are: ```fsharp type Decoder<S1, t1> = ReaderT<S1, ParseResult<t1>> type Encoder<S2, t2> = t2 -> Const<S2, unit> ``` Notice that the encoder is nothing other than a read-only lens (see [[FSharpPlus Const|Const]]). Given two functions `decode : S1 -> ParseResult<t1>` and `encode : t2 -> S2` you can construct a codec by using the `<->` operator like: `decode <-> encode`. A `Codec<S1, S2, t1, t2` is a: - [[FSharpPlus Functor]] that acts on `t1` - [[FSharpPlus Applicative Functor]] that acts on `t1` ## Examples ### Codec Definition With `get_Codec ()` ```fsharp open Fleece type Person = { name : string * string age : int option children: Person list } with static member get_Codec () = fun f l a c -> { name = (f, l); age = a; children = c } <!> jreq "firstName" (Some << fun x -> fst x.name) <*> jreq "lastName" (Some << fun x -> snd x.name) <*> jopt "age" (fun x -> x.age) // Optional fields can use 'jopt' <*> jreq "children" (fun x -> Some x.children) |> ofObjCodec ``` ### Conversion Methods (`ToJson` / `OfJson`) Note that these HAVE TO be defined as static methods, that is to say `static member OfJson = function` (without explicitly defining the argument) WILL NOT work. ```fsharp type Person = { Name: string Age: int Children: Person list } static member ToJson (x: Person) = jobj [ "name" .= x.Name "age" .= x.Age "children" .= x.Children ] static member OfJson json = match json with | JObject o -> let name = o .@ "name" let age = o .@ "age" let children = o .@ "children" match name, age, children with | Decode.Success name, Decode.Success age, Decode.Success children -> Decode.Success { Person.Name = name Age = age Children = children } | x -> Error <| Uncategorized (sprintf "Error parsing person: %A" x) | x -> Decode.Fail.objExpected x ``` `OfJson` can also be written monadically: ```fsharp static member OfJson json = match json with | JObject o -> Person.Create <!> (o .@ "name") <*> (o .@ "age") <*> (o .@ "children") | x -> Decode.Fail.objExpected x ``` Note that you probably need to open `FSharpPlus` here, together with `Fleece.Operators`. For optional parameters you can use `.@?`. ```fsharp static member OfJson json = match json with | JObject o -> monad { let! name = o .@ "name" let! age = o .@ "age" let! children = o .@ "children" return { Person.Name = name Age = age Children = children } } | x -> Decode.Fail.objExpected x ``` ### Custom Codecs ```fsharp let colorDecoder = function | JString "red" -> Decode.Success Red | JString "blue" -> Decode.Success Blue | JString "white" -> Decode.Success White | JString x as v -> Decode.Fail.invalidValue v ("Wrong color: " + x) | x -> Decode.Fail.strExpected x let colorEncoder = function | Red -> JString "red" | Blue -> JString "blue" | White -> JString "white" let colorCodec = colorDecoder <-> colorEncoder ``` ### API Parsing Note this example is valid for Fleece v0.9 and below. ```fsharp let localDateDecoder = function | JString dateRaw -> let result = LocalDatePattern.Iso.Parse dateRaw if result.Success then Ok result.Value else Decode.Fail.parseError result.Exception dateRaw | x -> Decode.Fail.strExpected x type Holiday = { Date : LocalDate } static member OfJson json = match json with | JObject o -> fun d -> { Date = d } <!> (jgetWith localDateDecoder o "date") | x -> Decode.Fail.objExpected x type Holidays = { Holidays : Holiday list } static member OfJson json = match json with | JObject o -> fun hs -> { Holidays = hs } <!> (o .@ "feiertage") | x -> Decode.Fail.objExpected x ``` Or with codecs ```fsharp let localDateCodec = let ofJson = function | JString dateRaw -> let result = LocalDatePattern.Iso.Parse dateRaw if result.Success then Ok result.Value else Decode.Fail.parseError result.Exception dateRaw | x -> Decode.Fail.strExpected x let toJson x = LocalDatePattern.Iso.Format x |> JString ofJson, toJson type Holiday = { Date : LocalDate } static member JsonObjCodec = fun d -> { Date = d } |> withFields |> jfieldWith localDateCodec "date" (fun x -> x.Date) type Holidays = { Holidays : Holiday list } static member JsonObjCodec = fun hs -> { Holidays = hs } |> withFields |> jfield "feiertage" (fun x -> x.Holidays) ``` ### Applying Two Codec on Same Object Source ```fsharp type Item = { ItemInfo : ItemInfo Fields : Field list Files : File list } with static member get_Codec () = let itemInfo : Codec<PropertyList<'a>,PropertyList<'a>,ItemInfo,Item> = let c = ItemInfo.get_ObjCodec () Codec.decode c <-> (fun { ItemInfo = x } -> Codec.encode c x) fun fields files info -> { ItemInfo = info Fields = (fields |> Option.defaultValue []) Files = (files |> Option.defaultValue []) } <!> jopt "fields" (fun { Fields = fs } -> Some fs) <*> jopt "files" (fun { Files = fs } -> Some fs) <*> itemInfo |> ofObjCodec ``` ### Parsing Optional NodaTime Instants in Fleece 0.10+ Both of these examples rely on a codec to parse `DateTimeOffset`. Note that the default `DateTimeOffset` codec uses exact parsing: ```fsharp static member dateTimeOffsetD x = match x with | JString null -> Decode.Fail.nullString | JString s -> match DateTimeOffset.TryParseExact (s, [| "yyyy-MM-ddTHH:mm:ss.fffK"; "yyyy-MM-ddTHH:mm:ssK" |], null, DateTimeStyles.RoundtripKind) with | true, t -> Ok t | _ -> Decode.Fail.invalidValue (Encoding x) "" | a -> Decode.Fail.strExpected (Encoding a) ``` Solution is to use a more tolerant `Instant` codec like so: ```fsharp let instantCodec = let decode (x : Encoding) = match x with | JString null -> Decode.Fail.nullString | JString s -> match DateTimeOffset.TryParse(s) with | true, t -> Ok (Instant.FromDateTimeOffset(t)) | _ -> Decode.Fail.invalidValue x "" | a -> Decode.Fail.strExpected x let encode (x : Instant) = x.ToDateTimeOffset() |> toJson decode <-> encode ``` ```fsharp type VaultInfo = { Id : VaultId Title : VaultTitle Version : VaultVersion CreatedAt : CreatedAt UpdatedAt : UpdatedAt ItemCount : ItemCount } with static member get_Codec () = let instantCodec = let decode = ofJson >> Result.map Instant.FromDateTimeOffset let encode (x : Instant) = x.ToDateTimeOffset() |> toJson decode <-> encode let vaultId = let c = VaultId.get_ObjCodec () Codec.decode c <-> (fun { Id = x } -> Codec.encode c x) fun i t v c u ic -> { Id = i Title = (VaultTitle t) Version = VaultVersion (v |> Option.defaultValue 0) CreatedAt = CreatedAt (c |> Option.defaultValue Instant.MinValue) UpdatedAt = UpdatedAt (u |> Option.defaultValue Instant.MinValue) ItemCount = ItemCount (ic |> Option.defaultValue 0) } <!> vaultId <*> jreq "name" (fun { Title = (VaultTitle t) } -> Some t) <*> jopt "contentVersion" (fun { Version = VaultVersion v } -> Some v) <*> joptWith (Codecs.option instantCodec) "createdAt" (fun { CreatedAt = CreatedAt c } -> Some c) <*> joptWith (Codecs.option instantCodec) "updatedAt" (fun { UpdatedAt = UpdatedAt u } -> Some u) <*> jopt "items" (fun { ItemCount = ItemCount ic } -> Some ic) |> ofObjCodec ``` Simpler example: ```fsharp type Snapshot = { Id : string Host : string Paths : string list Time : Instant } with static member get_Codec () = let instantCodec = let decode = ofJson >> Result.map Instant.FromDateTimeOffset let encode (x : Instant) = x.ToDateTimeOffset() |> toJson decode <-> encode fun i h p t -> { Id = i Host = h Paths = p Time = t } <!> jreq "short_id" (fun { Snapshot.Id = i } -> Some i) <*> jreq "hostname" (fun { Snapshot.Host = h } -> Some h) <*> jreq "paths" (fun { Snapshot.Paths = p } -> Some p) <*> jreqWith instantCodec "time" (fun { Snapshot.Time = t } -> Some t) |> ofObjCodec ``` ### Serializing With Indentation ```fsharp JsonSerializer.Serialize( toJsonValue defaultSettings, JsonSerializerOptions(WriteIndented = true)) ``` ## Differences Between 0.10 and 0.9 - Instead of `static member get_Codec ()`, 0.9 used `static member JsonObjCodec` - The definition of a codec has changed (type parameters), so defining a codec via custom encoder and decoder functions used to be done like so ```fsharp let localDateCodec = let toJson x = LocalDatePattern.Iso.Format x |> JString let ofJson = function | JString dateRaw -> let result = LocalDatePattern.Iso.Parse dateRaw if result.Success then Ok result.Value else Decode.Fail.parseError result.Exception dateRaw | x -> Decode.Fail.strExpected x ofJson, toJson ```