:binary_id validating as :string for changeset cast

I was trying to validate a :binary_id taken from a url string prior to passing on to queries and ran into what seems like a bug where Ecto.Type :binary_id accepts :string with no issue (as opposed to the a string formatted UUID).

Code:

defmodule TestBinaryId do
  use Ecto.Schema

  embedded_schema do
    field(:test_uuid, :binary_id)
  end

  def changeset(attrs) do
    Ecto.Changeset.cast(%TestBinaryId{}, attrs, [:test_uuid])
  end
end
iex(11)> TestBinaryId.changeset(%{test_uuid: "invalid"})
#Ecto.Changeset<
  action: nil,
  changes: %{test_uuid: "invalid"},
  errors: [],
  data: #TestBinaryId<>,
  valid?: true
>

I’ve even tried replacing :binary_id with Ecto.UUID ( field(:test_uuid, Ecto.UUID)) and got the same result.

My expectation is that a :binary_id type would fail for normal strings and only accept valid UUIDs (like “e3e53ebb-a5c5-4bcd-a33f-26716cf1b0b2”)

Am I doing something wrong here? Is this intended behavior?

1 Like

Did some more digging, looks like it’s expected behavior.
From Ecto.Type
defp cast_fun(:binary_id), do: &cast_binary/1

Seems like @josevalim mentioned that the decision was to allow the repository to add errors (if I’m interpreting things correctly).

We have decided to treat team as binaries in the changeset and do the dump check when writing to the repository. We are going to introduce Repo.insert! and allow some errors to be added by the repository.

Where “treat team” I believe was meant to be “treat them (binary_id)”.

I’m using the params library for param sanitization at the controller level, and the fact that :binary_id can accept :binary values presents an issue at the repo level (as mentioned).

I suppose the answer here is to define my own custom Ecto.Type that will check for a UUID like pattern with binary matching?
I guess I just don’t understand the decision to not have more stringent type checks at the Ecto.Type.cast/2 level to allow for better sanitization prior to hitting the repo.

Might do some more digging in the Ecto.Query side of things to understand the logic for how the error is caught there and then try a custom Ecto.Type. Although I’ve already spent too much time down this rabbit hole on something that was intended to be a 10 min fix.

1 Like

What are you using for the underlying type? PostgreSQL has an UUID type so it will do the validation for you, though you might need to translate the error into a changeset error.

1 Like

I’m using :uuid for the database. I’m actually getting the error on a query when trying to fetch the record.
I was trying to validate the type in the controller in a defparams validation (params library). The example above is just demonstrating the underlying issue that a :binary_id type is cast as a :binary. This you have to hit the db to get an error (as you pointed out). I was trying to avoid hitting the db by finding the mismatched type at the web interface layer.

Crap… nevermind. Replacing :binary_id with Ecto.UUID does work. I swear it didn’t yesterday… I guess I got bit by the Friday afternoon “my brain shut down but I thought it was still working” situation.

defmodule TestBinaryId do
  use Ecto.Schema

  embedded_schema do
    field(:test_uuid, Ecto.UUID)
  end

  def changeset(attrs) do
    Ecto.Changeset.cast(%TestBinaryId{}, attrs, [:test_uuid])
  end
end
iex(1)> TestBinaryId.changeset(%{test_uuid: "invalid"})
#Ecto.Changeset<
  action: nil,
  changes: %{},
  errors: [test_uuid: {"is invalid", [type: Ecto.UUID, validation: :cast]}],
  data: #TestBinaryId<>,
  valid?: false
>
iex(2)> TestBinaryId.changeset(%{test_uuid: "2a4c90f9-bdd8-40b9-be70-f9c8edea9230"})
#Ecto.Changeset<
  action: nil,
  changes: %{test_uuid: "2a4c90f9-bdd8-40b9-be70-f9c8edea9230"},
  errors: [],
  data: #TestBinaryId<>,
  valid?: true
>
2 Likes