endymion

endymion

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.

First 6 of 6 Posts Switch mode

cjbottaro

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… :+1:

LostKobrakai

LostKobrakai

Ecto types can already return custom error messages. For cast customizing the error doesn‘t really make sense as you‘d never really use it for individual fields.

endymion

endymion OP

That doesn’t work in our case. We have a union type that contains multiple embeds_one which delegates the specific validation logic for each type.

defmodule UnionStruct do
  use Ecto.Schema
  embedded_schema do
    embeds_one :url, UnionStruct.Url
    embeds_one :ssn, UnionStruct.Ssn
    embeds_one :phone_numer, UnionStruct.PhoneNumer
    embeds_one :date, UnionStruct.Date
    embeds_one :float, UnionStruct.Float
    embeds_one :Integer, UnionStruct.Integer

    field :other_data, :any
  end
end

# Example definition for PhoneNumber
defmodule UnionStruct.PhoneNumber do
  use Ecto.Schema

  embedded_schema do
    field :other_data, :any
    field :value, :string
  end

  def changeset(changeset, params, opts) do
    Ecto.Changeset.cast(changeset, params, [:value], opts)
    |> App.Changeset.custom_cast_error_message(:value, "Our custom phone number error message here")
    |> validate_phone_number(:value)
  end
end

For the fields like [:url, :phone_number, :ssn] those are all :string Ecto types. Hence why we can not have a single custom error message for :string when casting a value to :url, as the message we want to present with the input :url or :phone_number is different.

LostKobrakai

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.

endymion

endymion OP

This was my first thought at addressing this issue, we have several custom Ecto types that such as our own date format that we can do this for. But our total number of types we would need to do this for is around 12. Maintaining those custom 12 types just to set a cast message didn’t feel like a good trade off for the long run.

LostKobrakai

LostKobrakai

defmodule CustomString do
  use Ecto.ParameterizedType

  def init(opts) do
    %{error: opts[:validation_message]}
  end

  def type(_), do: :string

  def cast(value, params) do
    case Ecto.Type.cast(:string, value) do
      {:ok, value} -> {:ok, value}
      :error -> {:error, params.error}
    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 :value, CustomString, validation_message: "abc"
  end

  def changeset(changeset, params, opts) do
    Ecto.Changeset.cast(changeset, params, [:value], opts)
    |> validate_phone_number(:value)
  end
end
— All posts loaded —

Where Next? Top

Trending in Discussions Top

AstonJ
As the title says, please share what you’ve been up to with Elixir. Whether that’s been learning it, looking into it, making stuff with i...
2977 91898 914
New
AstonJ
The obligatory hello world thread! Who are you and where are you from? :stuck_out_tongue:
4616 55835 594
New
byu
@chrismccord : I just saw the Extract AGENTS.md from Phoenix.new into phx.new generator commit to the phoenix project. My initial shotgu...
New
arcanemachine
I was working on an Ecto migration and I needed a timestamp. So, for the nth time, I looked up the different data types for timestamps, a...
New
AstonJ
Just a general thread to post chat/news/info relating to AI/ML stuff that may be relevant for Nx now or in the future. Got anything to sh...
New
juhalehtonen
There has been a thread to discuss the Stack Overflow Developer Survey on this forum every year since 2018, so here’s yet another one for...
New
type1fool
I just stumbled on a newly redesigned elixir-lang.org. :tada: It looks like @Software_Mansion did the work, and I think it is generally a...
New

Other Trending Topics Top

JesseHerrick
Hey, I’m Jesse and I’m the main contributor behind Dexter, a full-featured, lightning-fast Elixir LSP optimized for large codebases. It s...
New
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
Damirados
Hello everyone. After busy few months I am happy to announce v0.1.0 of Emerge & Solve. They are GUI (Emerge) and State management (S...
New
netoum
Corex is an accessible, unstyled UI component library for Phoenix that integrates Zag.js state machines using Vanilla JavaScript and Live...
New
ausimian
Emily is an Elixir library that runs Nx computations on Apple’s MLX. Install it as the default Nx backend and Nx, defn, Axon, Nx.Serving,...
New
mudasobwa
While I am working on the Language Agnostic Code Audit SaaS, which uses MetaAST (spoiler: I am expecting it to be in a good shape for ann...
New

We're in Beta

About us Mission Statement