"user_id can't be blank" error when creating a record with "user" association

Hello all,

I’m new to Elixir and Phoenix and I’m creating a simple API for a fake project management app (Tradtrack), just for practice.

The app has two schemas: user and project. One user has many projects, so the params to create a new project must include user_id. However, even if I pass a valid user_id when creating a project, I get this error:

errors: [
    user_id: {"can't be blank", [validation: :required]}
],

Here are the user and project schemas and migrations.

  • Project migration:
defmodule Tradtrack.Repo.Migrations.CreateProjectsTable do
  use Ecto.Migration

  def change do
    create table :projects do
      add :code, :string
      add :client, :string
      add :word_count, :integer
      add :rate, :decimal
      add :total, :decimal
      add :date_received, :date
      add :delivery_date, :utc_datetime
      add :delivery_status, :boolean, default: false
      add :payment_status, :boolean, default: false
      add :notes, :string, default: ""
      add :user_id, references(:users, type: :binary_id, on_delete: :delete_all)

      timestamps()
    end

    create constraint(:projects, :word_count_must_be_greater_than_zero, check: "word_count > 0")
    create constraint(:projects, :rate_must_be_greater_than_zero, check: "rate > 0")
  end
end
  • Project schema:
defmodule Tradtrack.Project do
  use Ecto.Schema
  import Ecto.Changeset
  alias Ecto.Changeset
  alias Tradtrack.User

  @primary_key {:id, :binary_id, autogenerate: true}
  @foreign_key_type :binary_id

  @allowed [
    :code,
    :client,
    :word_count,
    :rate,
    :date_received,
    :delivery_date,
    :delivery_status,
    :payment_status,
    :notes
  ]

  @required_fields [:code, :client, :word_count, :rate, :date_received, :delivery_date, :user_id]

  schema "projects" do
    field :code, :string
    field :client, :string
    field :word_count, :integer
    field :rate, :decimal
    field :total, :decimal
    field :date_received, :date
    field :delivery_date, :utc_datetime
    field :delivery_status, :boolean, default: false
    field :payment_status, :boolean, default: false
    field :notes, :string, default: ""
    belongs_to :user, User, foreign_key: :user_id
    timestamps()
  end

  def changeset(project \\ %__MODULE__{}, params) do
    project
    |> cast(params, @allowed)
    |> validate_required(@required_fields)
    |> check_constraint(:word_count, name: :word_count_must_be_greater_than_zero)
    |> check_constraint(:rate, name: :rate_must_be_greater_than_zero)
    |> calculate_total()
  end

  defp calculate_total(
         %Changeset{valid?: true, changes: %{word_count: word_count, rate: rate}} = changeset
       ) do
    total = Decimal.mult(word_count, rate)

    change(changeset, %{total: total})
  end
end
  • User migration:
defmodule Tradtrack.Repo.Migrations.CreateUsersTable do
  use Ecto.Migration

  def change do

    create table :users do
      add :name, :string
      add :last_name, :string
      add :email, :string
      add :nickname, :string
      add :password_hash, :string

      timestamps()
    end

    # ensures fields email and nickname are unique
    create unique_index(:users, [:email])
    create unique_index(:users, [:nickname])
  end
end
  • User schema
defmodule Tradtrack.User do
  use Ecto.Schema
  import Ecto.Changeset
  alias Ecto.Changeset
  alias Tradtrack.Project

  @primary_key {:id, :binary_id, autogenerate: true}
  @required_params [:name, :last_name, :email, :nickname, :password]
  @email_regex ~r/^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,4}$/

  schema "users" do
    field :name, :string
    field :last_name, :string
    field :email, :string
    field :nickname, :string
    field :password, :string, virtual: true
    field :password_hash, :string
    has_many :projects, Project

    timestamps()
  end

  def changeset(params) do
    %__MODULE__{}
    |> cast(params, @required_params)
    |> validate_required(@required_params)
    |> unique_constraint([:email])
    |> unique_constraint([:nickname])
    |> validate_format(:email, @email_regex)
    |> validate_length(:password, min: 6)
    |> put_password_hash()
  end

  defp put_password_hash(%Changeset{valid?: true, changes: %{password: password}} = changeset) do
    change(changeset, Bcrypt.add_hash(password))
  end

  defp put_password_hash(changeset), do: changeset
end

Any help will be much appreciated! :slight_smile:

Thank you very much!

This all looks correct. Can you show some parameters that result in this error, along with the code you’re using to pass them in?

1 Like

In @allowed that you used to cast does not contain user_id. Can that be the problem?

3 Likes

@valehelle Yes, that solved the problem! :slight_smile: Thanks a million!

1 Like