endymion
Cleanly add custom errors for Ecto.Changeset.cast/4
Found a clean solution for adding custom error messages when using Ecto.Changeset.cast/4.
The issue presented is cast/4 does not allow :message for an opts unlike other changeset functions like validate_format/4 and other custom validator functions.
This presented a problem for us as we have a number of custom types and messages that we use to validate our inputs. This works fine when validating things like a phone number because we can simply pass our custom error message directly to validate_format/4, but for our :integer and :float values we simply rely on the base cast/4 for those types without the need for more validation. When given "foo" to input type :float the changeset error message returns "is invalid". Using Gettext and traverse_errors/2 we can fix this error message on the UI, but that ends up in needing to have branching error message logic and makes it more difficult with a dozen types to clearly see where the error message definitions exist.
Instead we have a function custom_cast_error_message(changeset, field, message) that replaces the given field error only for cast/4 validations to be our custom message
@doc """
Overwrites the error message for `Ecto.Changeset.cast/4` from `"is invalid"` to the
custom supplied error message.
"""
def custom_cast_error_message(changeset, field, message) do
errors = Enum.map(changeset.errors, fn {error_field, {_msg, opts}} = error ->
if field == error_field && opts[:validation] == :cast do
{field, {message, opts}}
else
error
end
end)
%{changeset | errors: errors}
end
A sample usage of this looks like
message = "This is our custom error message for cast/4"
Ecto.Changeset.cast(answer, params, [:value])
|> Aw.Changeset.custom_cast_error_message(:value, message)
Posting this because I saw numerous posts about this same issues and didn’t love the solutions found.
Most Liked
cjbottaro
My initial thought was to do this in translate_errors using the error metadata, but I like this better since it keeps the error messages all in one place and not spread out… ![]()
LostKobrakai
defmodule PhoneNumberType do
use Ecto.Type
def type, do: :string
def cast(value) do
case Ecto.Type.cast(:string, value) do
{:ok, value} -> {:ok, value}
:error -> {:error, "Our custom phone number error message here"}
end
end
def dump(value), do: Ecto.Type.dump(:string, value)
def load(value), do: Ecto.Type.load(:string, value)
end
defmodule UnionStruct.PhoneNumber do
use Ecto.Schema
embedded_schema do
field :other_data, :any
field :value, PhoneNumberType
end
def changeset(changeset, params, opts) do
Ecto.Changeset.cast(changeset, params, [:value], opts)
|> validate_phone_number(:value)
end
end
You can wrap generic types like this to get more specific types. If you have many similar ones you can even abstract this using Ecto.ParameterizedType.







