wfgilman
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?
Marked As Solved
wfgilman
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!
Also Liked
jwarlander
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 
bbense
customers -> %Customer requires 3 things.
-
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) -
Convert all the string keys to atoms.
step1 |> Enum.map( fn {k, v} -> {String.to_existing_atom(k), v} end ) |> Enum.into(%{}) -
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.







