Pattern matching encoded JSON

I have a nested JSON HTTP response I want to map to my structs. All responses are formatted: {"_embedded": {"entity": [entity-specific json]}}. I want to pattern match on the entity to map the entity data to the correct struct.

Example decoded HTTP response:

%{"_embedded" => 
  %{"customers" => 
    %{"id" => 1, "name" => "Joe Shmo", "addresses" => [
        %{"city" => "San Francisco", "state" => "CA"},
        %{"city" => "Lake Tahoe", "state" => "NV"}
      ]
     }
   }
}

My structs:

%Customer{:id, :name, :addresses}
%Address{:city, :state}

Right now I’m doing something like this to pattern match on the decoded response:

def map_response(%{"_embedded" => %{"customers" => customers}}) do
  customers |> Poison.encode! |> Poison.decode!(as: %Customer{addresses: [%Address{}]})
end

This works great, but I don’t like that I’m having to re-encode the HTTP response into order to utilize the decode!/2 function. I don’t know how to pattern match on the first part of the HTTP response without first decoding the response.

Is there a way around this?

you could use
https://github.com/appcues/exconstructor

If the situation really IS anywhere near that simple, I wouldn’t reach for a dependency…

defmodule MyApp do
  defmodule Customer do
    defstruct [id: nil, name: nil, addresses: []]
  end

  defmodule Address do
    defstruct [city: nil, state: nil]
  end

  def map_response(%{"_embedded" => %{"customers" => customers}}) do
    customers |> Enum.map(&make_customer/1)
  end
  def map_response(%{"_embedded" => %{"suppliers" => _suppliers}}), do: {:error, :not_implemented}
  # ..and so on, of course

  def make_customer(%{"id" => id, "name" => name, "addresses" => addresses}) do
    %Customer{id: id, name: name, addresses: Enum.map(addresses, &make_address/1)}
  end

  def make_address(%{"city" => city, "state" => state}) do
    %Address{city: city, state: state}
  end
end

response = 
%{"_embedded" => 
  %{"customers" => [
    %{"id" => 1, "name" => "Joe Shmo", "addresses" => [
        %{"city" => "San Francisco", "state" => "CA"},
        %{"city" => "Lake Tahoe", "state" => "NV"}
      ]
     }
    ]
   }
}

IO.inspect MyApp.map_response(response)
# [%MyApp.Customer{addresses: [%MyApp.Address{city: "San Francisco", state: "CA"},
#    %MyApp.Address{city: "Lake Tahoe", state: "NV"}], id: 1, name: "Joe Shmo"}]

However, if you need something generic for quite a few more use cases, and see it becoming hard to maintain, then yeah… do check out that library :slight_smile:

1 Like

customers -> %Customer requires 3 things.

  1. Remove any keys that are not in the Customer struct.

    good_keys =  Map.keys(Map.from_struct(Customer)) |> Enum.map(&Atom.to_string/1)
    {step1, _bad_keys } = customers |> Map.get("customers") |> Map.split(good_keys)
    
  2. Convert all the string keys to atoms.

     step1 |> Enum.map( fn {k, v} -> {String.to_existing_atom(k), v} end ) |> Enum.into(%{})
    
  3. Add a :__struct__ key to the Map with Customer as the Module.

     new_customer = Map.put(step1, :__struct__, Customer)
    

Then you need to pull out the address list and apply the same kind of transformation for each
map in the list.

1 Like

…so, as long as the situation is reasonably simple, the cost of doing all that dynamic work is somewhat steep, at least if you have to process a lot of requests :wink:

Yeah, your solution is much better for the actual stated problem. I have a tendency to overgeneralize my solutions.

I know where you’re coming from, I fight that urge every time I sit down to write some code :sunglasses:

Thanks for the suggestions. This is just some sample data - the real data has too many fields to pattern match all the variables and set them in the struct.

I’ve taken a look at ExConstructor before, perhaps it’s time for another look.

I haven’t looked at Poison.decode/2 source code for this. But if the decoding process is not part of mapping the data to a specified struct, then abstracting that mapping function into a standalone one would be awesome.

It doesn’t look like exconstructor can handle nested maps or lists :confused:

I figured it out using Poison.

You can put complex maps into structs by using the Poison.Decode.decode/2 function. You can create the map from an encoded JSON by using Poison.Parser.parse/1. Poison.decode/2 combines these steps.

If you’re making HTTP calls using HTTPoison, you can put in this override function which will parse the response body into a map:

  def process_response_body(body) do
    Poison.Parser.parse!(body)
  end

Then pattern match on the response body and map to a struct as follows:

def map_response_body(%{"_embedded" => %{"customers" => customers}}) do
  Poison.Decode.decode(customers, as: %Customer{addresses: [%Address{}]})
end

No re-encoding and decoding needed!

6 Likes