ExConstructor and Poison

Getting Them to Play Nicely

I recently encountered a problem trying to decode a JSON payload on the fly with Poison and converting that payload to structs via ExConstructor.


Poison is a terrific library and it offers a decode/2 method that takes your payload as the first argument and a keyword list of options as it’s second argument. Among those options you can pass in is as: which instructs Poison how you want to decode the payload.

For instance, if you had some struct that looked like this:

defmodule MyApp.MyStruct do
defstruct :some_key
end

and you invoked decode/2 like this:

Poison.decode(%{"some_key" => "some value"}, as: %MyApp.MyStruct{})

it would return:

%MyApp.MyStruct{some_key: "some value"}


My specific problem seemed to arise from a payload with nested keys I wanted to turn into structs, and those nested keys where in camelCase in the JSON payload. Let me show what I was trying to do by example.

In this example, I want to turn the JSON payload into a %Post{} struct that has keys meta and comments which hold values that are also structs (or structs inside a list).

Post struct

defmodule MyApp.Post do
defstruct [author_id: nil, meta: nil, comments: []]
end

Meta struct

defmodule MyApp.Meta do
defstruct [:total_comments]
use ExConstructor
end

Comment struct

defmodule MyApp.Comment do
defstruct author_id: nil, publish_date: nil, text: ""
use ExConstructor
end

JSON Payload

The payload coming in used camelCase keys and looked something like this:

{
"authorId": "5555",
"meta": {"totalComments": 22},
"comments": [
{
"authorId": "3333",
"publishDate": "01/10/2016",
"text": "This is a comment"
}]
}

Attempt #1

ExConstructor is normally excellent at converting camelCased strings into snake_cased atoms, so you’ll notice above that I dropped the use ExConstructor macro into several of the structs I wanted to use. Then, based upon some of the examples I read through in the Poison docs, I setup my attempt to decode the payload like so:

def deserialize(payload) do
Poison.decode(payload, as: [%Post{meta: %Meta{}, comments: [%Comment{}]})
end

This left me with:

%Post{
author_id: "5555",
meta: %Meta{total_comments: nil},
comments: [
%Comment{
text: "This is a comment",
author_id: nil,
publish_date: nil
}
]
}

Not exactly what I wanted. The weird thing was that the %Post{} struct’s author_id key was correctly populated, but the %Comment{} struct’s author_id key didn’t receive the value it should have.

Into the Source Code: The ExConstructor API

ExConstructor has a method called populate_struct/3 . It’s function signature is as follows:

@spec populate_struct(struct, map_or_kwlist, %Options{}) :: struct

You can see that you specify the struct you want to create and the map_or_kwlist param contains the values that you want to populate the struct with.

Implementing the Decoder Protocol

Poison allows developers to implement Decoders for JSON payloads in order to deserialize them. This is done by specifying you want to implement a protocol via defimpl. The Decoder protocol looks for you to define your own decode/2 method. The value you return from the decode/2 method will be the deserialized payload.

Attempt #2: Combining Decoder and populate_struct

import ExConstructor
defmodule MyApp.Post do
defstruct [author_id: nil, meta: nil, comments: []]
end
defimpl Poison.Decoder, for: MyApp.Post do
def decode(payload, _options) do
%{payload | meta: populate_struct(%MyApp.Meta{}, payload.meta), comments: decode_comments(payload)}
end
  defp decode_comment(%MyApp.Post{comments: comments}) do
Enum.map(comments, &(populate_struct(%MyApp.Comment{}, &1))
end
end

This resulted in:

%Post{
author_id: "5555",
meta: %Meta{total_comments: 22},
comments: [
%Comment{
text: "This is a comment",
author_id: "3333",
publish_date: "01/10/2018"
}
]
}

Exactly what I wanted.

One clap, two clap, three clap, forty?

By clapping more or less, you can signal to us which stories really stand out.