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).

3 Likes

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.

3 Likes

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.

4 Likes

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.

2 Likes

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.

1 Like

@foreign_key_type :binary_id

did the trick, thanks.

1 Like