Handling unfetched and null value

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?

Sounds like the normal tagged-tuple result would be better?

{:ok, theValue}
{:ok, nil} # Successful, but no return value
:error # Oh no!
{:error, reason} # Oh no with a reason!
1 Like

Hm… I see the pros of this approach - no magic value, standard tagged tuple. However then building new struct would be too verbose :frowning:

post = %Blog.Post{name: {:ok, "foo"}}

Or define custom initializer.

post = %Blog.new_post(name: "foo")

If you aren’t planning the “add” or “erase” any values in the struct you could just track the originally set keys in a meta list.

defmodule Blog.Post do
  defstruct [:id , :name, :body, :extra, meta: []]

  def post_keys do
    %Blog.Post{}
    |> Map.keys()
    |> Enum.reject(fn(k) -> Enum.member?([:meta, :__struct__], k) end)
    |> Enum.map(fn(k) -> {k, Atom.to_string(k)} end)
  end
end

defmodule Blog do
  @post_keys (Blog.Post.post_keys()) # keys to fetch as [{atom(),String.t()}]

  def to_post(map),
    do: Enum.reduce @post_keys, %Blog.Post{}, &(fetch_post_value &2, map, &1)

  defp fetch_post_value(post, map, {k, name}) do
    case Map.fetch(map, name) do
      {:ok, v} ->
        %{post | k => v, :meta => [k | post.meta]} # also track key in meta list
      :error ->
        post
    end
  end

  def to_map(post),
    do: Map.take post, post.meta

end

map =  %{"id" => 1, "name" => "name-foo", "body" => nil }
IO.inspect map
post = Blog.to_post(map)
IO.inspect post
IO.inspect (Blog.to_map post)
$ elixir meta.exs
%{"body" => nil, "id" => 1, "name" => "name-foo"}
%Blog.Post{body: nil, extra: nil, id: 1, meta: [:name, :id, :body],
 name: "name-foo"}
%{body: nil, id: 1, name: "name-foo"}
$

Perfectly reasonable.

Or make a helper function somewhere:

def ok(v), do: {:ok, v}

ok("foo") # {:ok, "foo"}

Saves a couple keystrokes, but eh. ^.^

1 Like