How to encode/decode nested entities?

Hello folks. I will tell here a fake SSCCE to talk about the right approach for a very common use case that I am stuck.
Let’s say I have an API with 3 database entities: User, Job, Location.
You want to be able to create and edit an user, that have only the information of the id of job, you can chose the job in a select field.
Let’s create some code:

module Data.Location exposing (Location, decoder)

type alias Location =
    { id : Int 
    , name : String
    , ...
    }

decoder : JD.Decoder Location
decoder = 
    JD.map2 Location
        (JD.field "id" JD.int)
        (JD.field "name" JD.string)
    
------------------------------------    
module Data.Job exposing (Job, decoder)

type alias Job = 
    { id : Int 
    , name : String 
    , location : Location
    , ...
    }

decoder : JD.Decoder Job 
decoder = 
    JD.map3 Job 
        (JD.field "id" JD.int)
        (JD.field "name" JD.string)
        (JD.field "location" Data.Location.decoder) -- Problem [2] and [3]

------------------------------------
module Data.User exposing (User, decoder, encoder)

type alias User =
    { id : Int
    , job : Job
    , ...
    }

decoder : JD.Decoder User
decoder =
    JD.map2 User 
        (JD.field "id"  JD.int)
        (JD.field "job" Data.Job.decoder)

encoder : User -> JE.Value
encoder user =
    JE.object
        [ ( "id", JE.int user.id ) -- Problem [1]
        , ( "job", JE.int user.job.id)
        ]

So, this approach have some problems:

[1] This encoder will work for both creating and editing. But when creating it will send id as 0. Works but not great.

[2] Probably in this case Job will receive the json { id, name, id_location }, so Data.Job.decoder can’t reuse the Data.Location.decoder. Is there a way to defining only one decoder for the entity that cover both cases? When pointing to entity or when pointing to the id of entity?

[3] When creating/editing a job, we probably will edit only the ID of the new location. We could chose in a select field. But it would be strange to edit the entity’s id. In this case it would be better if the User entity had id_job, but then I would need to duplicate the Job entity and it’s not ideal.

So I am trying to think in the best nested encode/decode approach that is consistent and is not required to create 2 entities and 2 decoders and 2 encoders for the same thing.

What do you think about it?

Spontaneously I think you are not actually talking about the same thing as these are two different things (job/location ID and job/location itself) so having two encoders and decoders for them would be a right way to go.

However, Elm encoders and decoders are extremely powerful and you can do whatever you want with them, so it should not be a problem to at least give you one decoder for both job/location and their id.

Before I come with a proposal could you please clarify how’d you imagine using incomplete records (i.e. Job or Location record when there is only its ID)? Would you populate rest of the fields with some default values? Or did I miss it from your rather big explanation?

Thanks for your reply.

I was trying to avoid a pattern where I would have to duplicate entities 2 times, one with the related entities, and other with the same exactly fields but having the id’s of the entities instead. This short example with 2 fields seems to be not a problem. But when I have 3+ entity fields and 20+ normal fields in the same record, in lot of records, that is a big problem.

I am considering too having some Maybe fields in the end of the record with a __ preffix like __job so the decoder will try to decode an entity if it exists, otherwise it will be Nothing. I am not sure if it would be ok.

Yes, I think I would need an init function for every record, unless it is an optional field.

I see. I would definitely go with custom types if I were you. Namely:

type EntityOrId a = Entity a | Id Int
type alias Job = { id : Int, name : String }
type alias JobOrId = EntityOrId Job

Then you can write something similar in decoder:

type alias User = {id : Int, jobOrId: JobOrId}
decoder : JD.Decoder User
decoder =
JD.map2 User
(JD.field “id” JD.int)
(JD.oneOf [(JD.map Entity <| JD.field “job” Data.Job.decoder), (JD.map Id <| JD.field “id_job” JD.int)])

Alternatively you can use an init function you mentioned (let’s assume it is called Job.fromId):

type alias User = {id : Int, job: Job}
decoder : JD.Decoder User
decoder =
JD.map2 User
(JD.field “id” JD.int)
(JD.oneOf [(JD.field “job” Data.Job.decoder), (JD.map Job.fromId <| JD.field “id_job” JD.int)])

The latter way is acceptable if your job instance with default values still makes sense in your app. Otherwise going with first option is preferrable, as for me.

This topic was automatically closed 10 days after the last reply. New replies are no longer allowed.