Field is Declared As :binary_id in Migration but Appears as :id to Changeset

Upon insert the error returns as:([id: {"is invalid", [type: :id, validation: :cast]}],)

CreateChannel.run error: #Ecto.Changeset<
  action: :insert,
  changes: %{
    description: "Officia iure ea occaecati omnis modi.",
    name: "Penance",
    org_id: 1,
    owner_id: 8,
    private: false
  },
  errors: [id: {"is invalid", [type: :id, validation: :cast]}],
  data: #FaithfulWord.Channels.Channel<>,
  valid?: false
>

I’m just not clear why this is happening, since I ensure that the type is binary_id in the migration(gist below). Any way to hint to Ecto that I want to treat it as a binary_id? Regards, Michael

You need to tell your Ecto schema that you want the id to be a UUID (just putting it in the migration is not enough as you are seeing).

To do so you need to set the @primary_key module attribute (docs)

@primary_key - configures the schema primary key. It expects a tuple {field_name, type, options} with the primary key field name, type (typically :id or :binary_id, but can be any type) and options. It also accepts false to disable the generation of a primary key field. Defaults to {:id, :id, autogenerate: true}.

For you it will look like:

@primary_key {:id, :binary_id}
schema "channels" do
    belongs_to :owner, User

    field :name, :string
    field :description, :string
    # rest of schema
  end
end

Note: you may also want to set autogenerate: true and if your other schemas are also using UUIDs as primary keys then you will probably want @foreign_key_type :binary_id.

You may also want to define a common schema file that will set those for each of your schema (there’s an example of this in the docs).

To second @axelson, I have something like this:

  ...
  @primary_key {:id, :binary_id, autogenerate: true}
  @foreign_key_type :binary_id
  schema "people" do
    ...
  end
  ...

And your migration might look like:

  ...
  create table(:people, primary_key: false) do
    add :id, :binary_id, primary_key: true
    ...
  end
  ...

You can check out this guide from Elixir Casts on UUIDs too.

To complete the answer once you’ve fixed your existing modules with the previous advice, mix help phx.gen.schema will also help out getting the settings right. In particular:

binary_id

Generated migration can use binary_id for schema’s primary key and its
references with option --binary-id.

Default options

This generator uses default options provided in the :generators configuration
of your application. These are the defaults:

config :your_app, :generators,
  migration: true,
  binary_id: false,
  sample_binary_id: "11111111-1111-1111-1111-111111111111"

You can override those options per invocation by providing corresponding
switches, e.g. --no-binary-id to use normal ids despite the default
configuration or --migration to force generation of the migration.

So you can change your generators setting in config.exs to binary_id: true and it will ensure all your generated schemas and migrations will work with UUIDs properly from then on.

Awesome answers, thanks guys.

I settled on:

@primary_key {:id, :binary_id, autogenerate: true}
schema "channels" do
  belongs_to :owner, User
...

And changeset succeeds in inserting.

So I’m migrating my User schema to binary_id and I’m hitting a similar issue when I’m attempting to confirm a registered user with the UserToken schema:

** (Ecto.Query.CastError) lib/faithful_word/accounts.ex:421: value `"a172f965-454b-47be-b246-35864825c646"` in `where` cannot be cast to type :id in query:

from u0 in FaithfulWord.Schema.UserToken,
  where: u0.user_id == ^"a172f965-454b-47be-b246-35864825c646",
  where: u0.org_id == ^1

here is the code where it’s failing:

  def build_user_confirmation_token(%User{} = user, %Org{} = org) do
    if org.id in user.orgs_confirmed do
      {:error, :already_confirmed}
    else
      # remove previous attempts to register
      Repo.delete_all(from(ut in UserToken, where: ut.user_id == ^user.id, where: ut.org_id == ^org.id)) # user.id mis-cast
      {encoded_token, user_token} = UserToken.build_email_token(user, "confirm", org.id)
      Repo.insert!(user_token)
      {:ok, encoded_token, user_token}
    end
  end

Ecto seems to think the User :binary_id is just :id? Here is my User and UserToken:

I’m pretty sure it’s something I’m missing in user.ex but I’m not sure what …

You have to replicate the same process on your user_token schema and "users_tokens" table as you did for your users.

It looks like you haven’t let it know that it is expecting a foreign key with a binary id?

Here’s what mine looks like for reference, schema:

  @primary_key {:id, :binary_id, autogenerate: true}
  @foreign_key_type :binary_id
  schema "people_tokens" do
    ...
  end
  ...

Edit: I think you just need to let your schema on your user_token know that it is expecting the :user_id to be a :binary_id by the @foreign_key_type :binary_id.

@foreign_key_type :binary_id

did the trick, thanks.