One alternative valid way is to use Jason and plain structs with @type t :: ... definitions plus Domo for validation.
Considering MapShaper module from this example app, the structs can be defined like:
defmodule Address do
@moduledoc false
use Domo
defstruct [city: "", state: ""]
@type t :: %__MODULE__{city: String.t(), state: String.t()}
end
defmodule User do
@moduledoc false
use Domo
today = NaiveDateTime.utc_now()
defstruct [id: 0, fname: "", lname: "", email: "", createdOn: today, updatedOn: today, addresses: [%Address{}]]
@type t :: %__MODULE__{
id: non_neg_integer(),
fname: String.t(),
lname: String.t(),
email: String.t(),
createdOn: NaiveDateTime.t(),
updatedOn: NaiveDateTime.t(),
addresses: [Address.t()]
}
defimpl MapShaper.Target do
def translate_source_map(_value, map) do
addresses = get_in(map, ["_embedded", "addresses"])
created_on = map |> get_in(["createdOn"]) |> parse_date()
updated_on = map |> get_in(["updatedOn"]) |> parse_date()
map
|> Map.put("addresses", addresses)
|> Map.put("createdOn", created_on)
|> Map.put("updatedOn", updated_on)
end
defp parse_date(str) do
str
|> NaiveDateTime.from_iso8601()
|> then(fn
{:ok, date_time} -> date_time
_ -> nil
end)
end
end
end
And parsing + validation that looks like:
with {:ok, binary} <- File.read("user.json"),
{:ok, map} <- Jason.decode(binary),
user = MapShaper.from_map(%User{}, map),
{:ok, user} <- User.ensure_type_ok(user) do
user
end
Returns the following nested struct for the valid input:
%User{
id: 283230,
fname: "John",
lname: "Smith",
email: "john.smith@example.com",
createdOn: ~N[2016-03-31 10:49:11.386],
updatedOn: ~N[2016-10-31 16:36:12.968],
addresses: [
%Address{city: "Minneapolis", state: "MN"},
%Address{city: "New Orleans", state: "LA"}
]
}


















