Decode JSON Nested Objects

I’ll preface that I am very new to Elixir. I am trying to decode a paginated JSON response from my API but I am a bit lost on exactly how to implement the decoding.

This is what the top-level JSON response looks like:

_embedded:
      collection_currently:
         0: {...}
         1: {...}
         2: {...}
         ...
_links:
     first: {...}
     self: {...}
     ...
page:
     size: 4
     totalElemnts: 28
     totalPages: 7
     number: 7

I’ve successfully decoded a single collection_currently object using the code from the Poison README. My question now is how exactly do I go about decoding this whole response? I’ve looked at this question which is close to what I’m trying to do, but as I said I’m new to Elixir and don’t fully understand the answer.

Any help would be appreciated! Thanks.

Your example looks like yaml, have you tried a yaml parser?

1 Like

Ahh I just copied the output in my broswer. I promise it’s JSON though; I’m querying an API endpoint I created hahaha.

Here’s the actual JSON response:

{
  "_embedded": {
    "collection_currently": [
      {
        "id": "5f4ff12189a6a3a49d61d7f2",
        "time": 1599074590,
        "summary": "Clear",
        "nearestStormDistance": 80,
        "nearestStormBearing": 182,
        "precipIntensity": 0,
        "precipProbability": 0,
        "temperature": 85.6,
        "apparentTemperature": 85.6,
        "dewPoint": 37.1,
        "humidity": 0.18,
        "pressure": 1016.2,
        "windSpeed": 5.25,
        "windGust": 6.48,
        "windBearing": 3,
        "cloudCover": 0.01,
        "uvIndex": 8,
        "visibility": 10,
        "ozone": 277.8,
        "formattedTime": "2020-09-02 12:23:10 (PDT)",
        "_links": {
          "self": {
            "href": "http://192.168.0.98:8080/rest-service-0.0.1-SNAPSHOT/api/currently_collection/5f4ff12189a6a3a49d61d7f2"
          },
          "currently_collection": {
            "href": "http://192.168.0.98:8080/rest-service-0.0.1-SNAPSHOT/api/currently_collection"
          }
        }
      },
      {
        "id": "5f469c3011c084b8589041ff",
        "time": 1598463022,
        "summary": "Clear",
        "nearestStormDistance": 52,
        "nearestStormBearing": 331,
        "precipIntensity": 0,
        "precipProbability": 0,
        "temperature": 67.35,
        "apparentTemperature": 67.35,
        "dewPoint": 42.23,
        "humidity": 0.4,
        "pressure": 1013.3,
        "windSpeed": 3.7,
        "windGust": 5.07,
        "windBearing": 39,
        "cloudCover": 0,
        "uvIndex": 5,
        "visibility": 10,
        "ozone": 297.6,
        "formattedTime": "2020-08-26 10:30:22 (PDT)",
        "_links": {
          "self": [
            {
              "href": "http://192.168.0.98:8080/rest-service-0.0.1-SNAPSHOT/api/currently_collection/5f469c3011c084b8589041ff"
            },
            {
              "href": "http://192.168.0.98:8080/rest-service-0.0.1-SNAPSHOT/api/currently_collection/latest"
            }
          ],
          "currently_collection": {
            "href": "http://192.168.0.98:8080/rest-service-0.0.1-SNAPSHOT/api/currently_collection"
          }
        }
      }
    ]
  },
  "_links": {
    "first": {
      "href": "http://192.168.0.98:8080/rest-service-0.0.1-SNAPSHOT/api/currently_collection?page=0&size=4"
    },
    "self": {
      "href": "http://192.168.0.98:8080/rest-service-0.0.1-SNAPSHOT/api/currently_collection"
    },
    "next": {
      "href": "http://192.168.0.98:8080/rest-service-0.0.1-SNAPSHOT/api/currently_collection?page=1&size=4"
    },
    "last": {
      "href": "http://192.168.0.98:8080/rest-service-0.0.1-SNAPSHOT/api/currently_collection?page=6&size=4"
    }
  },
  "page": {
    "size": 4,
    "totalElements": 28,
    "totalPages": 7,
    "number": 0
  }
}

You can use the :as option to Poison.decode. That would be something along the lines of:

Poison.decode(
     json_string,
     as: %{
        "_embeded" => %{
            "collection_currently" => [%CollectionCurrently{}]
        }
     }
)

(Assuming that you have defined an appropriate CollectionCurrently struct which derives Poison.Encoder.)

The idea here is that the :as option expresses the structure of the data you want to decode. If in addition to the collection_currently list you want to decode the other parts (page and _links) in addition, you can include them in the :as as well.

Ahh ok yeah I was using as: but I just wasn’t sure what the whole syntax would look like.

This is what I have now:

Poison.decode(body, as: %{
          "_embedded" => %{
            "currently_collection" => [%CurrentlyReprModule.CurrentlyBlock{}]
            },
          "_links" => %CurrentlyReprModule.Links{},
          "page" => [%CurrentlyReprModule.Page{}]
        })

But then I get this error:

expected a map, got: {"number", 0}

This must be coming from the "page" part of the JSON response but I don’t know why it’s happening. Decoding this should be straightforward. Here is the Page struct:

defmodule Page do
    @derive [Poison.Encoder]
    defstruct [:size, :totalPages, :totalElements, :number]
end

Thanks!

The square bracket syntax in the :as option indicates a list (which makes sense, because that’s what square brackets are in Elixir.) So it makes sense to have them around %CurrentlyReprModule.CurrentlyBlock{}, because the "currently_collection" key is indeed supposed to have a list of those objects. But I’m pretty sure you don’t want that around %CurrentlyReprModule.Page{}, because there is only supposed to be a single "page" object (not a list of them.)

Ok yeah that’s what I assumed. However, when I remove the brackets I get a Poison.EncoderError:

unable to encode value: {"page", %ElixirServer.CurrentlyReprModule.Page{number: 0, size: 4, totalElements: 30, totalPages: 8}}

Any idea why this is happening? The Page struct follows the same lines as my CurrentlyBlock struct (which works).

I’ve tried a very simplified version of it, which works:

Poison.decode!(json_string, as: %{ "page" => %Page{} })

(With the Page module defined as you defined it above, and with json_string being the json string of your data.)

This should work for you too. I suggest you proceed from there, and see at what point that EncoderError creeps in. (For instance, quite possibly you have a typo somewhere.)

You can use :jsn.as_map/1 (jsn | Hex)

~s(
      {
        "fields": {
          "summary": "Bug!",
          "issuetype": {
            "id": "10005"
          },
          "project": {
            "key": "BUG"
          },
          "description": {
            "type": "doc",
            "version": 1,
            "content": [
              {
                "type": "paragraph",
                "content": [
                  {
                    "text": "Description - Bug Found",
                    "type": "text"
                  }
                ]
              }
            ]
          }
        }
      }
    )
    |> Jason.decode! 
    |> :jsn.as_map
  end
%{
  "fields" => %{
    "description" => %{
      "content" => [
        %{
          "content" => [
            %{"text" => "Description - Bug Found", "type" => "text"}
          ],
          "type" => "paragraph"
        }
      ],
      "type" => "doc",
      "version" => 1
    },
    "issuetype" => %{"id" => "10005"},
    "project" => %{"key" => "BUG"},
    "summary" => "Bug!"
  }
}