fsprojects / Fleece

Json mapper for F#
http://fsprojects.github.io/Fleece
Apache License 2.0
199 stars 31 forks source link

Compute Field in jsonObjCodec based on another field #127

Closed williamoanta closed 2 years ago

williamoanta commented 2 years ago

I have the following type with JsonObjCodec defined:

    type User = {
        Id: UserId
        DateOfBirth: DateTime
        Country: string
        State: string
        SexAtBirth: SexMeasure
        EthnicBackground: string
        Education: string
        Occupation: string
        Nationality: string
        SexualOrientation: string
        RelationshipStatus: string
        Children: int
        Phone: int
        Email: string
        RegistrationCompleted: bool
        Age: AgeMeasure
        Weight: BiometricMeasure
        Height: BiometricMeasure
        LastMod: UTCTick
        RoadmapStatus: RoadmapStatus
        LastProcessedGuidelineId: int
        QuestionsStore: QuestionStore list
        MedicalConditions: MedicalConditionStore list
        RejectedMedicalConditions: RejectedMedicalConditionStore list
        Scores: ScoreStore list
        Biometrics: BiometricStore list
    } with
      static member JsonObjCodec =
        (fun id db co st se eb educ occup nat sexo rels chi pho ema regc ag we he lm rs lid qs mc rmc sc bi ->
          {Id = id; DateOfBirth = db; Country = co; State = st; SexAtBirth = se; EthnicBackground = eb; 
           Education = educ; Occupation = occup; Nationality = nat; SexualOrientation = sexo; 
           RelationshipStatus = rels; Children = chi; Phone = pho; Email = ema
           RegistrationCompleted = regc; Age = getAge(db); Weight = we; Height = he;
           LastMod = lm; RoadmapStatus = rs; LastProcessedGuidelineId = lid; QuestionsStore = qs;
           MedicalConditions = mc; RejectedMedicalConditions = rmc; Scores = sc; Biometrics = bi })

        <!> jreq "Id" (Some << fun x -> x.Id)
        <*> jreq "DateOfBirth" (Some << fun x -> x.DateOfBirth)
        <*> jreq "Country" (Some << fun x -> x.Country)
        <*> jreq "State" (Some << fun x -> x.State)
        <*> jreq "SexAtBirth" (Some << fun x -> x.SexAtBirth)
        <*> jreq "EthnicBackground" (Some << fun x -> x.EthnicBackground)
        <*> jreq "Education" (Some << fun x -> x.Education)
        <*> jreq "Occupation" (Some << fun x -> x.Occupation)
        <*> jreq "Nationality" (Some << fun x -> x.Nationality)                                
        <*> jreq "SexualOrientation" (Some << fun x -> x.SexualOrientation)                                        
        <*> jreq "RelationshipStatus" (Some << fun x -> x.RelationshipStatus)                                                
        <*> jreq "Children" (Some << fun x -> x.Children)                                                        
        <*> jreq "Phone" (Some << fun x -> x.Phone)                                                                
        <*> jreq "Email" (Some << fun x -> x.Email)                                                                        
        <*> jreq "RegistrationCompleted" (Some << fun x -> x.RegistrationCompleted)                                                                         
        <*> jreq "Age" (Some << fun x -> getAge x.DateOfBirth)
        <*> jreq "Weight" (Some << fun x -> x.Weight)
        <*> jreq "Height" (Some << fun x -> x.Height)
        <*> jreq "LastMod" (Some << fun x -> x.LastMod)
        <*> jreq "RoadmapStatus" (Some << fun x -> x.RoadmapStatus)
        <*> jreq "LastProcessedGuidelineId" (Some << fun x -> x.LastProcessedGuidelineId)
        <*> jreq "QuestionsStore" (Some << fun x -> x.QuestionsStore)
        <*> jreq "MedicalConditions" (Some << fun x -> x.MedicalConditions)
        <*> jreq "RejectedMedicalConditions" (Some << fun x -> x.RejectedMedicalConditions)
        <*> jreq "Scores" (Some << fun x -> x.Scores)
        <*> jreq "Biometrics" (Some << fun x -> x.Biometrics)

I would like to compute "Age" based on "DateOfBirth":

<*> jreq "Age" (Some << fun x -> getAge x.DateOfBirth)

That means that the front-end should be able to send a JSON payload without "Age" in it, as age will always be computed on the backend based on "DateOfBirth". But this does not work, as when I send a JSON without "Age", I get the error that there is no match for "Age":

System.Exception: Property: 'Age' not found in object 'Microsoft.FSharp.Core.ExtraTopLevelOperators+DictImpl`3[Microsoft.FSharp.Core.CompilerServices....

System.Exception
Property: 'Age' not found in object 'Microsoft.FSharp.Core.ExtraTopLevelOperators+DictImpl`3[Microsoft.FSharp.Core.CompilerServices.RuntimeHelpers+StructBox`1[System.String],System.String,Newtonsoft.Json.Linq.JToken]'
   at Tests.Json.getType@2012-238.Invoke(String json)

Any ideas on how to achieve my goal? Thanks!

gusty commented 2 years ago

The computed field shouldn't have a dedicated jreq becuase by adding jreq "Age" you are effectively requiring that field in the json (therefore the combinator name) and it's my understanding that's not what you want.

Simply remove the whole line:

// <*> jreq "Age" (Some << fun x -> getAge x.DateOfBirth)

and remove the unused parameter ag, already hinted by the compiler.

Your code should look like this:

#r "nuget: Fleece.NewtonsoftJson"

open System
open Fleece.Newtonsoft
open Fleece.Newtonsoft.Operators

type UserId = int
type SexMeasure = bool
type AgeMeasure = int
type BiometricMeasure = float
type UTCTick = DateTime
type RoadmapStatus = string
type QuestionStore = string
type MedicalConditions = string
type ScoreStore = int
type MedicalConditionStore = int
type RejectedMedicalConditions = bool
type RejectedMedicalConditionStore = string
type Biometrics = string
type BiometricStore = string

let getAge (x: DateTime) = int ((DateTime.Now - x).TotalDays / 365.25)

type User = {
    Id: UserId
    DateOfBirth: DateTime
    Country: string
    State: string
    SexAtBirth: SexMeasure
    EthnicBackground: string
    Education: string
    Occupation: string
    Nationality: string
    SexualOrientation: string
    RelationshipStatus: string
    Children: int
    Phone: int
    Email: string
    RegistrationCompleted: bool
    Age: AgeMeasure
    Weight: BiometricMeasure
    Height: BiometricMeasure
    LastMod: UTCTick
    RoadmapStatus: RoadmapStatus
    LastProcessedGuidelineId: int
    QuestionsStore: QuestionStore list
    MedicalConditions: MedicalConditionStore list
    RejectedMedicalConditions: RejectedMedicalConditionStore list
    Scores: ScoreStore list
    Biometrics: BiometricStore list
} with
  static member JsonObjCodec =
    (fun id db co st se eb educ occup nat sexo rels chi pho ema regc we he lm rs lid qs mc rmc sc bi ->
      {Id = id; DateOfBirth = db; Country = co; State = st; SexAtBirth = se; EthnicBackground = eb; 
       Education = educ; Occupation = occup; Nationality = nat; SexualOrientation = sexo; 
       RelationshipStatus = rels; Children = chi; Phone = pho; Email = ema
       RegistrationCompleted = regc; Age = getAge(db); Weight = we; Height = he;
       LastMod = lm; RoadmapStatus = rs; LastProcessedGuidelineId = lid; QuestionsStore = qs;
       MedicalConditions = mc; RejectedMedicalConditions = rmc; Scores = sc; Biometrics = bi })

    <!> jreq "Id" (Some << fun x -> x.Id)
    <*> jreq "DateOfBirth" (Some << fun x -> x.DateOfBirth)
    <*> jreq "Country" (Some << fun x -> x.Country)
    <*> jreq "State" (Some << fun x -> x.State)
    <*> jreq "SexAtBirth" (Some << fun x -> x.SexAtBirth)
    <*> jreq "EthnicBackground" (Some << fun x -> x.EthnicBackground)
    <*> jreq "Education" (Some << fun x -> x.Education)
    <*> jreq "Occupation" (Some << fun x -> x.Occupation)
    <*> jreq "Nationality" (Some << fun x -> x.Nationality)                                
    <*> jreq "SexualOrientation" (Some << fun x -> x.SexualOrientation)                                        
    <*> jreq "RelationshipStatus" (Some << fun x -> x.RelationshipStatus)                                                
    <*> jreq "Children" (Some << fun x -> x.Children)                                                        
    <*> jreq "Phone" (Some << fun x -> x.Phone)                                                                
    <*> jreq "Email" (Some << fun x -> x.Email)                                                                        
    <*> jreq "RegistrationCompleted" (Some << fun x -> x.RegistrationCompleted)
    // <*> jreq "Age" (Some << fun x -> getAge x.DateOfBirth)
    <*> jreq "Weight" (Some << fun x -> x.Weight)
    <*> jreq "Height" (Some << fun x -> x.Height)
    <*> jreq "LastMod" (Some << fun x -> x.LastMod)
    <*> jreq "RoadmapStatus" (Some << fun x -> x.RoadmapStatus)
    <*> jreq "LastProcessedGuidelineId" (Some << fun x -> x.LastProcessedGuidelineId)
    <*> jreq "QuestionsStore" (Some << fun x -> x.QuestionsStore)
    <*> jreq "MedicalConditions" (Some << fun x -> x.MedicalConditions)
    <*> jreq "RejectedMedicalConditions" (Some << fun x -> x.RejectedMedicalConditions)
    <*> jreq "Scores" (Some << fun x -> x.Scores)
    <*> jreq "Biometrics" (Some << fun x -> x.Biometrics)

Please let me know if that solves your issue.

Note that I added some predefined values and #r directives to make your code compilable and I encourage to do so when writing repros to make them easy to reproduce for the person who's reading your code and that will surely increase chances to get help.

williamoanta commented 2 years ago

Thank you for your answer and advice.

This almost solves my problem, except that, although I am OK with not having the field when I do 'fromJSON' - because I am computing it from birth, I would still like to have the field present when I do 'toJSON', which does not happen with your proposed changes, as the field is not preset in the JSON representation when I do 'toJSON'.

Any ideas on how to narrow the solution to my case? Thank you!

p.s. To give more context, the reason I need this is because I would like the backend always to compute age based on birth - that is why I do not need it as part of the 'fromJSON'. But I would like to send the Age as part of the JSON sent by the backend to the frontend.

gusty commented 2 years ago

In that case take your original code and just change the Age fields's jreq to jopt.

gusty commented 2 years ago

@williamoanta does the above answer your question? If so could you please close the issue? Otherwise let me know. Thanks !

gusty commented 2 years ago

Assuming solved. Otherwise feel free to re-open.