Another more robust option is to use the structure of YAML or equivalent JSON that looks like the elixir definition above and utilize Nestru library (bias warning - I’m the author) to turn the map into the nested structs like the following:
Install dependencies
Mix.install(
[
{:nestru, "~> 0.3.2"},
{:yaml_elixir, "~> 2.9"}
],
consolidate_protocols: false
)
Define the payload
All nodes with childs has the same named child_list
for simplicity.
payload =
"""
Parent:
field: Value
another_field: "Another value"
child_list:
- Child:
child_field: Child 1 data
child_list:
- TypeOne:
field: Value
- TypeTwo:
another_field: Some other value
- Child:
child_field: Child 2 data
"""
|> YamlElixir.read_from_string!()
%{
"Parent" => %{
"another_field" => "Another value",
"child_list" => [
%{
"Child" => %{
"child_field" => "Child 1 data",
"child_list" => [
%{"TypeOne" => %{"field" => "Value"}},
%{"TypeTwo" => %{"another_field" => "Some other value"}}
]
}
},
%{"Child" => %{"child_field" => "Child 2 data"}}
],
"field" => "Value"
}
}
Define nodes
defmodule Parent do
defstruct [:field, :another_field, :child_list]
end
defmodule Child do
defstruct [:child_field, :child_list]
end
defmodule TypeOne do
defstruct [:field]
end
defmodule TypeTwo do
defstruct [:another_field]
end
Implement Nestru.Decoder for nodes
StructDetector.split_module_fields/1
returns the list of struct atom and fields from the given maps list (or a single map). Nestru
will call it with an appropriate map to build the child list of nested structs according to the hint.
defmodule StructDetector do
def split_module_fields(map) do
list = List.wrap(map)
try do
{:ok,
Enum.flat_map(list, fn a_map ->
Enum.map(a_map, fn {module_string, fields} ->
{Module.safe_concat([module_string]), fields}
end)
end)}
rescue
ArgumentError -> {:error, :nonexisting_struct}
end
end
end
require Protocol
defimpl Nestru.Decoder, for: [Parent, Child, TypeOne, TypeTwo] do
def from_map_hint(_struct, _context, map) do
if Map.has_key?(map, "child_list") do
with {:ok, module_fields_list} <-
StructDetector.split_module_fields(Map.fetch!(map, "child_list")) do
{modules_list, fields_list} = Enum.unzip(module_fields_list)
{:ok,
%{
child_list: fn _value -> Nestru.decode_from_list_of_maps(fields_list, modules_list) end
}}
end
else
# Empty hint, decode all keys as-is.
{:ok, %{}}
end
end
end
Decode
{:ok, [{root_module, fields}]} = StructDetector.split_module_fields(payload)
Nestru.decode_from_map!(fields, root_module)
%Parent{
field: "Value",
another_field: "Another value",
child_list: [
%Child{
child_field: "Child 1 data",
child_list: [%TypeOne{field: "Value"}, %TypeTwo{another_field: "Some other value"}]
},
%Child{child_field: "Child 2 data", child_list: nil}
]
}
payload
can start from any node that Nestru.Decoder
protocol is implemented for and be of any level of nestedness.
And you can easily support new nodes by including their names in the list in defimpl
.