Reputation: 912
Probably it is a beginner's question. I have a JSON data format that holds polymorphic records and I need to parse it. These are vertices or edges of a graph
{
"records": [{
"id": 0,
"object": {
"id": "vertex1"
}
}, {
"id": 1,
"object": {
"id": "vertex2"
}
}, {
"id": 2,
"object": {
"from": "vertex1",
"to": "vertex2"
}
}]
}
As you can see they all have id
, but vertices and edges have different record structures.
I tried to find something on parsing such structures, but the only thing I found was Handling records with shared substructure in Elm, but I cannot translate the answer to Elm 0.17 (a simple renaming of data
to type
did not help)
In general there are 2 challenges:
This is how far I got:
type alias RecordBase =
{ id : Int
}
type Records = List (Record RecordBase)
type Record o =
VertexRecord o
| EdgeRecord o
type alias VertexRecord o =
{ o | object : {
id : Int
}
}
type alias EdgeRecord o =
{ o | object : {
from : Int
, to : Int
}
}
but the compiler complains with
Naming multiple top-level values
VertexRecord
makes things ambiguous.
Apparently union
already defined the VertexRecord
and EdgeRecord
types.
I really don't know how to proceed from here. All suggestions are most welcome.
Upvotes: 3
Views: 394
Reputation: 36375
Since you have the label id
in multiple places and of multiple types, I think it makes things a little cleaner to have type aliases and field names that indicate each id's purpose.
Edit 2016-12-15: Updated to elm-0.18
type alias RecordID = Int
type alias VertexID = String
type alias VertexContents =
{ vertexID : VertexID }
type alias EdgeContents =
{ from : VertexID
, to : VertexID
}
Your Record
type doesn't actually need to include the field name of object
anywhere. You can simply use a union type. Here is an example. You could shape this a few different ways, the important part to understand is fitting both types of data in as a single Record type.
type Record
= Vertex RecordID VertexContents
| Edge RecordID EdgeContents
You could define a function that returns the recordID
given either a vertex or edge like so:
getRecordID : Record -> RecordID
getRecordID r =
case r of
Vertex recordID _ -> recordID
Edge recordID _ -> recordID
Now, onto decoding. Using Json.Decode.andThen
, you can decode the common record ID field, then pass the JSON off to another decoder to get the rest of the contents:
recordDecoder : Json.Decoder Record
recordDecoder =
Json.field "id" Json.int
|> Json.andThen \recordID ->
Json.oneOf [ vertexDecoder recordID, edgeDecoder recordID ]
vertexDecoder : RecordID -> Json.Decoder Record
vertexDecoder recordID =
Json.object2 Vertex
(Json.succeed recordID)
(Json.object1 VertexContents (Json.at ["object", "id"] Json.string))
edgeDecoder : RecordID -> Json.Decoder Record
edgeDecoder recordID =
Json.object2 Edge
(Json.succeed recordID)
(Json.object2 EdgeContents
(Json.at ["object", "from"] Json.string)
(Json.at ["object", "to"] Json.string))
recordListDecoder : Json.Decoder (List Record)
recordListDecoder =
Json.field "records" Json.list recordDecoder
Putting it all together, you can decode your example like this:
import Html exposing (text)
import Json.Decode as Json
main =
text <| toString <| Json.decodeString recordListDecoder testData
testData =
"""
{
"records": [{
"id": 0,
"object": {
"id": "vertex1"
}
}, {
"id": 1,
"object": {
"id": "vertex2"
}
}, {
"id": 2,
"object": {
"from": "vertex1",
"to": "vertex2"
}
}]
}
"""
Upvotes: 2