I’m working with an API which supports returning only selected fields. I want to wrap resources with struct (not just plain map), but then nil
can mean two things: unfetched, or nil is given.
(If I use map to hold the data, I may use Map.has_key?
to determine it’s unfetched or nil is given, but I cannot use pattern matching for two cases)
So… I’m thinking of introducing a magic value, :null
, in my library to indicate it is “real” null value. If the value is nil
then it’s just not fetched. Here is how it would work.
defmodule Blog.Post do
defstruct [:id, :name, :body, :extra]
end
defmodule Blog do
def get_post(1) do
body_str = ~s({"id": 1, "name": "name-foo", "body": null})
Poison.decode!(body_str)
|> to_post
end
defp to_post(map), do: to_struct(map, %Blog.Post{})
defp to_struct(map, struct) do
Enum.reduce Map.to_list(struct), struct, fn {k, _}, acc ->
case Map.fetch(map, Atom.to_string(k)) do
{:ok, v} -> %{acc | k => nullify(v)}
:error -> acc
end
end
end
def to_json(post = %Blog.Post{}) do
post
|> Map.from_struct
|> Enum.filter(fn {_, v} -> v != nil end)
|> Enum.map(fn {k, v} -> {k, denullify(v)} end)
|> Enum.into(%{})
|> Poison.encode!
end
defp nullify(nil), do: :null
defp nullify(val), do: val
defp denullify(:null), do: nil
defp denullify(val), do: val
end
post = Blog.get_post(1)
IO.inspect(post)
# => %Blog.Post{body: :null, extra: nil, id: 1, name: "name-foo"}
:null = post.body
nil = post.extra
IO.puts Blog.to_json(post)
# => {"name":"name-foo","id":1,"body":null}
IO.puts Poison.encode!(post)
# => {"name":"name-foo","id":1,"extra":null,"body":"null"}
Now I can get the correct JSON output for update API - it includes JSON null
for actual null value (:null
) but excludes key/value when value is unfetched or undetermined (nil
).
Is there any better way to handle this problem?