Fleece


Sometimes, the JSON required by a given situation will contain fields that do not need to be present in the F# data model. For example, the JSON-RPC 2.0 specification requires every request/response object to carry the field jsonrpc with value "2.0". In a codebase that only uses JSON-RPC 2.0, why capture this field on a record?

When writing ToJson and OfJson methods for this data, handling the required field is fairly natural:

type Request =
    { Method: string
      MethodParams: Map<string, string>}
    static member ToJson (r: Request) =
        jobj [
            "method"  .= r.Method
            "params"  .= r.MethodParams
            "jsonrpc" .= "2.0"
        ]
    static member OfJson json =
        match json with
        | JObject o ->
            let method = o .@ "method"
            let methodParams = o .@ "params"
            // We require the "jsonrpc" field to be present
            let jsonrpc = o .@ "jsonrpc"
            match method, methodParams, jsonrpc with
            | Decode.Success m, Decode.Success p, Decode.Success "2.0" -> // We enforce the value of the field
                // ...but do not use it in the final object
                Decode.Success {
                    Method = m
                    MethodParams = p
                }
            | x -> Error <| Uncategorized (sprintf "Error parsing person: %A" x)
        | x -> Decode.Fail.objExpected x

This can also be modeled with Codecs:

type Response =
    { Result: string option
      Error: string option }
      static member JsonObjCodec =
          fun r e _ -> { Result = r; Error = e }
          |> withFields
          |> jfieldOpt "result"  (fun r -> r.Result)
          |> jfieldOpt "error"   (fun r -> r.Error)
          |> jfield    "jsonrpc" (fun _ -> "2.0")

There are three parts to this. First, the constructor is given an unused third parameter, which will receive the field required on the JSON object. Second, the "jsonrpc" field is required using jfield; its getter always returns "2.0" Finally: the fields must be in the correct order -- that is, the field specs must follow the order of the arguments in the constructor.

namespace Fleece
module SystemJson from Fleece
module Operators from Fleece.SystemJson
Request.Method: string
Multiple items
val string : value:'T -> string
<summary>Converts the argument to a string using <c>ToString</c>.</summary>
<remarks>For standard integer and floating point values the and any type that implements <c>IFormattable</c><c>ToString</c> conversion uses <c>CultureInfo.InvariantCulture</c>. </remarks>
<param name="value">The input value.</param>
<returns>The converted string.</returns>


--------------------
type string = System.String
<summary>An abbreviation for the CLI type <see cref="T:System.String" />.</summary>
<category>Basic Types</category>
Request.MethodParams: Map<string,string>
Multiple items
module Map from Microsoft.FSharp.Collections
<summary>Contains operations for working with values of type <see cref="T:Microsoft.FSharp.Collections.Map`2" />.</summary>

--------------------
type Map<'Key,'Value (requires comparison)> = interface IReadOnlyDictionary<'Key,'Value> interface IReadOnlyCollection<KeyValuePair<'Key,'Value>> 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 Change : key:'Key * f:('Value option -> 'Value option) -> Map<'Key,'Value> ...
<summary>Immutable maps based on binary trees, where keys are ordered by F# generic comparison. By default comparison is the F# structural comparison function or uses implementations of the IComparable interface on key values.</summary>
<remarks>See the <see cref="T:Microsoft.FSharp.Collections.MapModule" /> module for further operations on maps. All members of this class are thread-safe and may be used concurrently from multiple threads.</remarks>


--------------------
new : elements:seq<'Key * 'Value> -> Map<'Key,'Value>
type ToJson = inherit Default1 static member Invoke : x:'t -> JsonValue (requires member ToJson) static member ToJson : x:bool * ToJson -> JsonValue + 44 overloads
val r : Request
type Request = { Method: string MethodParams: Map<string,string> } static member OfJson : json:JsonValue -> Result<Request,DecodeError> static member ToJson : r:Request -> JsonValue
val jobj : x:seq<string * System.Json.JsonValue> -> System.Json.JsonValue
<summary> Creates a new Json object for serialization </summary>
type OfJson = inherit Default1 static member Invoke : x:JsonValue -> 't ParseResult (requires member OfJson) static member OfJson : decimal * OfJson -> (JsonValue -> Result<decimal,DecodeError>) + 47 overloads
val json : System.Json.JsonValue
Multiple items
val JObject : x:System.Collections.Generic.IReadOnlyDictionary<string,System.Json.JsonValue> -> System.Json.JsonValue

--------------------
active recognizer JObject: System.Json.JsonValue -> Choice<System.Collections.Generic.IReadOnlyList<System.Json.JsonValue>,System.Collections.Generic.IReadOnlyDictionary<string,System.Json.JsonValue>,System.Json.JsonPrimitive,bool,string,unit>
val o : System.Collections.Generic.IReadOnlyDictionary<string,System.Json.JsonValue>
val method : Result<string,DecodeError>
val methodParams : Result<Map<string,string>,DecodeError>
val jsonrpc : Result<string,DecodeError>
module Decode from Fleece.SystemJson
Multiple items
val Success : x:'a -> Result<'a,'b>

--------------------
active recognizer Success: Result<'a,'b> -> Choice<'a,'b>
val m : string
val p : Map<string,string>
val x : Result<string,DecodeError> * Result<Map<string,string>,DecodeError> * Result<string,DecodeError>
union case Result.Error: ErrorValue: 'TError -> Result<'T,'TError>
<summary> Represents an Error or a Failure. The code failed with a value of 'TError representing what went wrong. </summary>
union case DecodeError.Uncategorized: string -> DecodeError
val sprintf : format:Printf.StringFormat<'T> -> 'T
<summary>Print to a string using the given format.</summary>
<param name="format">The formatter.</param>
<returns>The formatted result.</returns>
val x : System.Json.JsonValue
module Fail from Fleece.SystemJson.Decode
val objExpected : v:System.Json.JsonValue -> Result<'t,DecodeError>
Multiple items
Response.Result: string option

--------------------
module Result from Microsoft.FSharp.Core
<summary>Contains operations for working with values of type <see cref="T:Microsoft.FSharp.Core.Result`2" />.</summary>
<category>Choices and Results</category>


--------------------
[<Struct>] type Result<'T,'TError> = | Ok of ResultValue: 'T | Error of ErrorValue: 'TError
<summary>Helper type for error handling without exceptions.</summary>
<category>Choices and Results</category>
type 'T option = Option<'T>
<summary>The type of optional values. When used from other CLI languages the empty option is the <c>null</c> value. </summary>
<remarks>Use the constructors <c>Some</c> and <c>None</c> to create values of this type. Use the values in the <c>Option</c> module to manipulate values of this type, or pattern match against the values directly. 'None' values will appear as the value <c>null</c> to other CLI languages. Instance methods on this type will appear as static methods to other CLI languages due to the use of <c>null</c> as a value representation.</remarks>
<category index="3">Options</category>
Response.Error: string option
val r : string option
val e : string option
Multiple items
module Result from Microsoft.FSharp.Core
<summary>Contains operations for working with values of type <see cref="T:Microsoft.FSharp.Core.Result`2" />.</summary>
<category>Choices and Results</category>


--------------------
[<Struct>] type Result<'T,'TError> = | Ok of ResultValue: 'T | Error of ErrorValue: 'TError
<summary>Helper type for error handling without exceptions.</summary>
<category>Choices and Results</category>
val withFields : f:'a -> ('b -> Result<'a,'c>) * ('d -> System.Collections.Generic.IReadOnlyDictionary<'e,'f>) (requires equality)
<summary>Initialize the field mappings.</summary>
<param name="f">An object constructor as a curried function.</param>
<returns>The resulting object codec.</returns>
val jfieldOpt : fieldName:string -> getter:('T -> 'Value option) -> Decoder<System.Collections.Generic.IReadOnlyDictionary<string,System.Json.JsonValue>,('Value option -> 'Rest)> * Encoder<System.Collections.Generic.IReadOnlyDictionary<string,System.Json.JsonValue>,'T> -> (System.Collections.Generic.IReadOnlyDictionary<string,System.Json.JsonValue> -> Result<'Rest,DecodeError>) * ('T -> System.Collections.Generic.IReadOnlyDictionary<string,System.Json.JsonValue>) (requires member ToJson and member OfJson)
<summary>Appends an optional field mapping to the codec.</summary>
<param name="fieldName">A string that will be used as key to the field.</param>
<param name="getter">The field getter function.</param>
<param name="rest">The other mappings.</param>
<returns>The resulting object codec.</returns>
val r : Response
Response.Result: string option
val jfield : fieldName:string -> getter:('T -> 'Value) -> Decoder<System.Collections.Generic.IReadOnlyDictionary<string,System.Json.JsonValue>,('Value -> 'Rest)> * Encoder<System.Collections.Generic.IReadOnlyDictionary<string,System.Json.JsonValue>,'T> -> (System.Collections.Generic.IReadOnlyDictionary<string,System.Json.JsonValue> -> Result<'Rest,DecodeError>) * ('T -> System.Collections.Generic.IReadOnlyDictionary<string,System.Json.JsonValue>) (requires member OfJson and member ToJson)
<summary>Appends a field mapping to the codec.</summary>
<param name="fieldName">A string that will be used as key to the field.</param>
<param name="getter">The field getter function.</param>
<param name="rest">The other mappings.</param>
<returns>The resulting object codec.</returns>