-
Notifications
You must be signed in to change notification settings - Fork 1
3. Custom codec rules
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
}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.AddRuleExample 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.AddRuleIf 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.