Hi all,
I have some JSON from a 3rd party API that can take a few different forms. For each response, there are
- Shared fields which are present in every response (
id,timestamp, etc) - There is a field
type(present in every response) whose value is another JSON object withidandnamekeys - There is an additional key whose name is is the name of the
typeobject. Sometypes don’t actually have any additional metadata (and thus have no extra key in the response JSON).
For example:
{
"id": 1,
"timestamp": "etc",
"type": {"id": 1, "name": "chair"},
"chair": {
// (Chair metadata goes here)
}
}
Or...
{
"id": 2,
"timestamp": "etc",
"type": {"id": 4, "name": "bed"},
"bed": {
// (Bed metadata goes here)
}
}
Or...
{
"id": 2,
"timestamp": "etc",
"type": {"id": 4, "name": "stool"},
// (no metadata)
}
Where the metadata for each type has a different shape.
This is complicated by the fact that
I could naively decode this into an Elm record with lots of optional fields:
type alias NaiveRecord =
{ id : Int
, timestamp : Timestamp
, recordType : NaiveRecordType
, chair : Maybe ChairMetadata
, bed : Maybe ChairMetadata
, table : Maybe TableMetadata
-- and so on...
}
type alias NaiveRecordType =
{ id : Int,
, name : String
}
However, this seems bad for a couple of reasons:
- It makes (theoretically) impossible states possible. The relevant metadata field for each record’s type should be
Just aif and only if therecordTypeis the corresponding one. - Any production code using this type will result in unnecessarily getting any metadata in/out of
Maybes when we know that the value is present (or rather, if the value isn’t present, something has gone very wrong and we would like to handle that case differently!)
So, I would rather use a type like:
type Record =
{ id : Int
, timestamp : Timestamp
, recordType : RecordType
}
type RecordType
= Chair ChairMetadata
| Bed BedMetadata
| Table TableMetadata
| Stool -- Has no additional metadata
| Bookcase -- Has no additional metadata
-- And so on
However, I’m not sure quite how to decode this from the JSON, as it requires checking the type's name field, and then (possibly) nesting the top level metadata field within the RecordType.
My current best guess is to use the “naive” type as an intermediate type. For example:
decodeNaiveRecord : Decoder NaiveRecord
-- i.e. relatively cookiecutter decoder with Decode.mapN or similar
fromNaiveRecord : NaiveRecord -> Decoder Record
-- i.e. succeed if "type" matches the metadata field, fail otherwise
decodeRecord : Decoder Record
decodeRecord =
Decode.andThen fromNaiveRecord decodeNaiveRecord
I have 2 main questions that I would like help with:
- Is my “target” type (
Record) a good design for this problem or is there a better solution/pattern I should be aware of - If it is an acceptable design, then what is the best approach to decode the JSON into it? Is there any clear and elegant way to do so without going via an intermediate data type?
Thanks!