Ozzie Neher

Developing for the Web

Ecto build_assoc inserting empty values

December 29th, 2017

In my application I have two schema's: User and Board. A user has_many boards and a board belongs_to a user. I went through the biggest headache trying to figure out why the params I'm passing from my form to build_assoc kept being inserted as NULL.

TL;DR: It turns out that Ecto.build_assoc/3 expects the fields you pass it—the last argument—to be a map keyed by :atoms not "strings".

Take a look at the example below:

def create(conn, %{"board" => board_params}) do
  user = App.Auth.current_user

  board = user
    |> Ecto.build_assoc(:boards, board_params)
    |> Board.changeset()
    |> Repo.insert()

  case board do
    {:ok} ->
      conn
      |> redirect(to: board_path(conn, :index))
    {:error, changeset} ->
      conn
      |> render("new.html", changeset: changeset)
  end
end

A pretty standard create action in Phoenix. We assign the forms parameters to board_params, which looks like this:

%{ "name" => "Example" }

And then we create a board from the current logged in user using Ecto.build_assoc/3. The only problem is that the result of that build was an empty Board changeset.

iex(10)> Ecto.build_assoc(user, :boards, %{"name" => "test"})
%App.Accounts.Board{__meta__: #Ecto.Schema.Metadata<:built, "boards">,
 id: nil, inserted_at: nil, name: nil, updated_at: nil,
 user: #Ecto.Association.NotLoaded<association :user is not loaded>, user_id: 5}

I needed to turn board_params from a map keyed by strings to a map keyed by atoms.

%{ "name" => "Example" }
# becomes
%{ name: "Example" }

To do this I've created a helper function in my Util module, map_keys_to_atom:

defmodule App.Util do
  def map_keys_to_atom(%{} = map) do
    Map.new(map, fn {k, v} ->
      case is_bitstring(k) do
        true ->
          {String.to_atom(k), v}
        false ->
          {k, v}
      end
    end)
  end
end

So the final create function looks like this:

def create(conn, %{"board" => board_params}) do
  user = App.Auth.current_user

  board_params = App.Util.map_keys_to_atom(board_params)

  board = user
    |> Ecto.build_assoc(:boards, board_params)
    |> Board.changeset()
    |> Repo.insert()

  case board do
    {:ok} ->
      conn
      |> redirect(to: board_path(conn, :index))
    {:error, changeset} ->
      conn
      |> render("new.html", changeset: changeset)
  end
end