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 withid
andname
keys - There is an additional key whose name is is the name of the
type
object. Sometype
s 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 a
if and only if therecordType
is the corresponding one. - Any production code using this type will result in unnecessarily getting any metadata in/out of
Maybe
s 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!