Proper JsonObjCodec with Fleece for Complex, Nested Discriminated Unions

Viewed 79

I have the following DU which is composed of other DUs or/and Records.

type BiometricRules =
        | Age of Comparator * AgeMeasure
        | Glycemia of Comparator * BiometricMeasure
        | Biometric of BiometricType * Comparator * BiometricMeasure
        | Sex of SexMeasure
        | MedicalCondition of MedicalCondition
        | Score of ScoreType * Comparator * ScoreMeasure

While trying to deserialize and serialize with Fleece, I have written the following JsonObjCodec.

with
        static member JsonObjCodec =
            Age <!> jreq "Age" (function Age (comp, ageMeasure) -> Some (comp |> string, ageMeasure |> string) | _ -> None)
            <|> (Glycemia <!> jreq "Glycemia" (function Glycemia (comp, bioMeasure) -> Some (comp |> string, bioMeasure) | _ -> None))
            <|> (Biometric <!> jreq "BiometricRule" (function Biometric (bt, comp, bm) -> Some (bt |> string, comp |> string, bm) | _ -> None))
            <|> (Sex <!> jreq "Sex" (function Sex s -> Some (s |> string) | _ -> None))
            <|> (BiometricRules.MedicalCondition <!> jreq "MedicalCondition" (function BiometricRules.MedicalCondition x -> Some (x) | _ -> None))
            <|> (Score <!> jreq "Score" (function Score (st, comp, scoreMeasure) -> Some (st |> string, comp |> string, scoreMeasure) | _ -> None))

For unknown reason it does not compile with error No overloads match for method 'Map'. All the nested DUs or Records have either a JsonObjCodec or static FromString and ToString methods defined.

Any solution with respect to how I could solve this via Fleece would be appreciated. The library is already deeply used in the project, so changing it would involve too much refactoring.

Below I copy pasted the definition of the other DU and Records, as reference:


    type Comparator =
        | GreaterThan
        | LowerThan
        | LowerThanOrEqual
        | GreaterThanOrEqual
        | EqualTo
    with
        override this.ToString() =
            match this with
            | GreaterThan -> ">"
            | LowerThan -> "<"
            | LowerThanOrEqual -> "<="
            | GreaterThanOrEqual -> ">="
            | EqualTo -> "="   
        static member FromString s =
            match s with
            | ">" -> GreaterThan
            | "<" -> LowerThan
            | ">=" -> GreaterThanOrEqual
            | "<=" -> LowerThanOrEqual
            | "=" -> EqualTo
            | _ -> failwith "Not a valid comparator."


    type AgeMeasure =
        | Years of decimal
        | Months of decimal
        | Weeks of decimal
    with
        override this.ToString() =
            match this with
            | Years y -> string y + " years"
            | Months m -> string m + " months"
            | Weeks w -> string w + " weeks"
        static member FromString (s: string) =
            match s with
            | _ when s.EndsWith("years") -> Years (Decimal.Parse(s.Replace("years", "")))
            | _ when s.EndsWith("months") -> Months (Decimal.Parse(s.Replace("months", "")))
            | _ when s.EndsWith("weeks") -> Weeks (Decimal.Parse(s.Replace("weeks", "")))


    type BiometricMeasure = {
        Value: decimal
        UoM: string option
    } with
        static member JsonObjCodec =
            fun va uom -> {
                Value = va
                UoM = if uom = "NA" then None else Some uom
            }
            <!> jreq "Value" (Some << fun bm -> bm.Value)
            <*> jreq "UoM" (Some << fun bm -> if bm.UoM |> Option.isNone then "NA" else bm.UoM |> Option.get)


    type BiometricType =
        | SBP
        | DBP
        | Glycemia
        | Specified of string
    with
        override this.ToString() =
            match this with
            | SBP -> "SBP"
            | DBP -> "DBP"
            | Glycemia -> "Glycemia"
            | Specified s -> s
        static member FromString s =
            match s with
            | "SBP" -> SBP
            | "DBP" -> DBP
            | "Glycemia" -> Glycemia
            | _ -> Specified s  


    type SexMeasure =
        | Female
        | Male
        | Other of string
    with
        override this.ToString() =
            match this with
            | Female -> "Female"
            | Male -> "Male"
            | Other s -> s
        static member FromString (s: string) =
            match s.ToLower() with
            | "Female" -> Female
            | "Male" -> Male
            | other -> Other other   


    type MedicalCondition =
        | ICD of ICD
        | Other of string
    with
        static member JsonObjCodec =
            ICD <!> jreq "MedicalCondition" (function ICD v -> Some v | _ -> None)            
            <|> (Other <!> jreq "MedicalCondition" (function Other v -> Some v | _ -> None))


    type ScoreType =
        | BMI
        | Other of string 
    with
        override this.ToString() =
            match this with
            | BMI -> "BMI"
            | Other s -> s
        static member FromString s =
            match s with
            | "BMI" -> BMI
            | _ -> Other s  


    type ScoreMeasure = decimal

Libraries Used:

    <PackageReference Update="FSharp.Core" Version="4.7" />
    <PackageReference Include="FSharpPlus" Version="1.1.1" />
    <PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
    <PackageReference Include="Fleece.NewtonsoftJson" Version="0.8.0" />
    <PackageReference Include="FSharp.Data" Version="3.3.3" />
1 Answers

The Problem

Fleece provides Json codecs, not string codecs, so defining ToString and FromString is not the way to go, unless you need them for other stuff.

The solution

Define ToJson and OfJson for your internal DUs. Then remove all the |> string fragments in JsonObjCodec body.

Here's a quick and dirty example (I advise error handling to be improved) for Comparator :

    static member ToJson x = JString (string x)
    static member OfJson x =
        match x with
        | JString x -> Ok (Comparator.FromString x)
        | _ -> Error (Uncategorized "JString expected")

Alternative solution

Leave all your internal DUs like this, but add the missing "parse" section in your JsonObjCodec:

    ...
    with
    static member JsonObjCodec =
        (fun (a, b) -> Age (Comparator.FromString a, AgeMeasure.FromString b)) <!> jreq "Age" (function Age (comp, ageMeasure) -> Some (comp |> string, ageMeasure |> string) | _ -> None)
    <|> ...

this becomes a bit verbose but will do the job.

Tips

  • Instead of using the <|> operator to add codecs, you colud use the jchoice combinator, it will read better.

  • If you really need your String / FromString methods I would suggest renaming FromString to Parse or renaming it to TryParse and returning an option type. This way you can take advantage of FSharpPlus tryParse function.

  • Also, if you are using the string/parse pattern everywhere it might worth to create a codec combinator that works from transforming to/from strings. This is not an easy task but it might worth the mental effort.

  • For debugging stuff like this purpose, try not to open the namespace FSharpPlus as it contains generic definitions of operators like <|>, <!> and <*>, this way you'll get better compile error messages.

Related