Lukasz Guminski
Lukasz Guminski

Reputation: 912

Parsing JSON polymorphic records with Elm

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:

  1. defining a polymorphic record
  2. decode JSON dynamically into a vertex or an edge

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

Answers (1)

Chad Gilbert
Chad Gilbert

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

Related Questions