Skip to content

3. Custom codec rules

percyqaz edited this page Jul 6, 2021 · 1 revision

For types that are not supported (or to override types that are), you will want to give your JsonEncoder additional rules for generating codecs.

Here is what a codec looks like

type JsonCodec<'T> = 
    {
        Encode: 'T -> JSON
        Decode: 'T -> JSON -> 'T //may throw MapFailure exception

        Default: unit -> 'T
    }

The most important thing to note here is that Decode must take an existing instance of 'T that is is "decoding onto".

What it does with this instance doesn't matter (it can ignore it, create a new instance entirely, or it can modify it)

What does matter is that the incoming instance is a well-formed, F#-safe instance of 'T. The Default generator for 'T does just that (we can't just use Unchecked.defaultOf as it causes non-nullable types to be null)

Here is the string codec (the version disallowing null) from the code as an example:

let stringNoNull =
    {
        Encode = fun (s: string) ->
            match s with
            | null -> nullArg "s"
            | _ -> JSON.String s
        Decode = fun _ json ->
            match json with
            | JSON.String s -> s
            | _ -> Error.expectedStr json
        Default = fun () -> String.Empty
    }

Create your own

Mapping.Rules contain some helper functions for creating a CustomCodecRule

Example for creating an override for unit option from scratch:

open Percyqaz.Json

let json = new JsonEncoder()

fun (cache, settings, rules) ->
    {
        Encode = function Some () -> JSON.Bool true | None -> JSON.Bool false
        Decode = fun _ json ->
            match json with
            | JSON.Bool b -> if b then Some () else None
            | _ -> Mapping.Error.expectedBool json
        Default = fun () -> None
    }
|> Mapping.Rules.typeRule<unit option>
|> json.AddRule

Example for creating an override for unit option that reuses the behaviour from the bool codec:

open Percyqaz.Json
open Percyqaz.Json.Mapping

let json = new JsonEncoder()

fun (cache, settings, rules) ->
   getCodec<bool>(cache, settings, rules)
   |> Codec.map
       (function Some () -> true | None -> false)
       (function true -> Some () | false -> None)
|> Rules.typeRule<unit option>
|> json.AddRule

If you have direct access to the type definition, you can also add a custom codec as a static method. In this case there is no need to add a rule to your JsonEncoder as there is an existing rule by default to detect this and use it for the codec.

type POCO<'T when 'T : equality>(defaultValue: 'T, value: 'T) =
    member this.Value = value
    member this.DefaultValue = defaultValue
    override this.Equals(other) =
        match other with
        | :? POCO<'T> as other -> other.Value = this.Value && other.DefaultValue = this.DefaultValue
        | _ -> false
    override this.GetHashCode() = -1
    static member JsonCodec(cache, settings, rules): Json.Mapping.JsonCodec<POCO<'T>> =
        let tP = Mapping.getCodec<'T>(cache, settings, rules)
        {
            Encode = fun (o: POCO<'T>) -> tP.Encode o.Value
            Decode = fun (instance: POCO<'T>) json -> POCO(instance.DefaultValue, tP.Decode instance.Value json)
            Default = fun _ -> let d = tP.Default() in POCO(d, d)
        }

Note that in this example, only the Value member is decoded from the JSON data, while DefaultValue is preserved from the existing instance.

If this decoder was used in a Record with default instances of POCO<'T> as members, the default values provided there would carry through to the decoded record.

Clone this wiki locally