Decoding JSON into Elixir Structs

Last reviewed on January 22, 2019

For the past 10 years, my programming experience has mostly focussed on JavaScript and PHP. I've learned a few functional programming concepts through JavaScript, such as function purity, immutability, and side effects, but I've never worked with a true functional language. To learn more about functional programming, I've decided to learn Elixir and write about what I learn to help me learn.


I've been going through the Pragmatic Studio Elixir course, and this post is based on one of the exercises. I highly recommend this course!

Let's say we have the following JSON string:

json = ~s([
  {
    "id": 1,
    "name": "Teddy",
    "type": "Brown",
    "hibernating": true
  },
  {
    "id": 2,
    "name": "Smokey",
    "type": "Black"
  },
  {
    "id": 3,
    "name": "Paddington",
    "type": "Brown"
  }
])

Note, ~s is a sigil and it allows us to easily create a string of JSON without having to escape all of the quotes.

We can parse this JSON into a list of maps using the Poison library:

bears = Poison.decode!(json)
# [
#   %{"hibernating" => true, "id" => 1, "name" => "Teddy", "type" => "Brown"},
#   %{"id" => 2, "name" => "Smokey", "type" => "Black"},
#   %{"id" => 3, "name" => "Paddington", "type" => "Brown"}
# ]

What if we wanted this to be a list of Bear structs instead?

The Bear struct used in the course is similar to the following:

defmodule Bear do
  defstruct id: nil, name: "", type: "", hibernating: false
end

To convert each map to a Bear struct, we can map over the list:

Enum.map bears, fn(bear) ->
  %Bear{
    id: bear["id"],
    name: bear["name"],
    type: bear["type"],
    hibernating: (if bear["hibernating"], do: true, else: false)
  }
end
# [
#   %Bear{hibernating: true, id: 1, name: "Teddy", type: "Brown"},
#   %Bear{hibernating: false, id: 2, name: "Smokey", type: "Black"},
#   %Bear{hibernating: false, id: 3, name: "Paddington", type: "Brown"}
# ]

This works, but setting each key-value pair is pretty tedious. After some googling, I found the Kernel.struct/2 function, which allows us to create a struct from a map.

So I tried this:

Enum.map bears, fn(bear) ->
  struct(Bear, bear)
end
# [
#   %Bear{hibernating: false, id: nil, name: "", type: ""},
#   %Bear{hibernating: false, id: nil, name: "", type: ""},
#   %Bear{hibernating: false, id: nil, name: "", type: ""}
# ]

Hmmm, not what I was expecting. All of the values are set to the struct's default values.

After reading about Kernel.struct/2 a bit more, it turns out that string keys are ignored. The keys of the map must be atoms. To convert the keys from strings to atoms, we can use Map.new/2. This function allows us to create a new map from an existing map.

Enum.map bears, fn(bear) ->
  bear = Map.new bear, fn({key, value}) ->
    {String.to_atom(key), value}
  end

  struct(Bear, bear)
end
# [
#   %Bear{hibernating: true, id: 1, name: "Teddy", type: "Brown"},
#   %Bear{hibernating: false, id: 2, name: "Smokey", type: "Black"},
#   %Bear{hibernating: false, id: 3, name: "Paddington", type: "Brown"}
# ]

Great, it works!

Now it turns out, there is an even easier way to do this using the Poison library.

Poison.decode!(json, as: [%Bear{}])

Unfortunately, I couldn't find the documentation for this in the Poison.decode!/2 v3.1.0 docs, but thankfully I learned about it in the Pragmatic Studio Elixir course!