How to Reference Changeset Variable inside Pipeline for Custom Error Message

I’m trying to create custom error messages inside Ecto.Changeset so that when a unique_constraint is violated, for example, it says "Wallet [foo] already exists", rather than "Wallet already exists". To do this, I’ve been using Ecto.Changeset.get_change(), which provides a default value if needed.

My problem is how to reference the changeset that gets created by the first step in the pipeline, since I need to reference it twice in the unique_constraint step (e.g. once for unique_constraint itself, and again for the custom error message).

I couldn’t figure out how to do this, so for now, I’m splitting the pipeline apart and saving the changesset that is created after cast(), so I can reference it as often as needed in the following steps below. I’m curious if there is a better way to do this and would appreciate any advice. See code below for example. Thanks!

def changeset(wallet, attrs \\ %{}) do
    cs = cast(wallet, attrs, @optional_fields ++ @required_fields ++ @assoc_fields)

    |> validate_required(@required_fields)
    |> unique_constraint([:name], message: "Wallet [#{get_change(cs, :name, "")}] already exists.")
    |> foreign_key_constraint(:data_source_id,
      message: "Wallet [#{get_change(cs, :name, "")}] missing parent Data Source ID."

I think this code is pretty OK. You might consider writing a custom changeset function:

def changeset(wallet, attrs \\ %{}) do
  |> cast(attrs, @optional_fields ++ @required_fields ++ @assoc_fields)
  |> validate_required(@required_fields)
  |> unique_name()

defp unique_name(changeset) do ...

For the data_source_id, I would suggest passing the data source if that’s possible:

def changeset(wallet, %DataSource{} = data_source, attrs \\ %{}) do
  |> put_assoc(:data_source, data_source)
  |> ...
1 Like

I would take another route. Instead of hardcoding things into the error message use interpolation tags in the message to replace at the time of rendering the error. For example see validate_length + traverse_errors.

This is a good idea. thank you.

Thanks for the comment. I’m still very confused about best practices for CRUD operations on associations. Doesn’t put_assoc() require the association to be defined with on_replace: :delete or something? Is this dangerous or acceptable? I’m having a hard time finding good sample code that shows the way this should be done. Some people say "just work with the id’s and don’t worry about put_assoc. Others say these functions are great, particularly for many-to-many associations. I’ve even seen one person suggest to change the on_replace behaviour to set a boolean “deleted” flag so that the orphaned rows get tagged as deleted but are not physically removed from database. Would appreciate any pointers to github repo with good sample code or other resources. Thanks!

I personally use put_assoc mostly in contexts where I create an entity that has belongs_to. So if a post must have an author, I’d do something like:

def create_changset(%Author{} = author, post_attrs) do
  |> cast(...)
  |> put_assoc(:author, author)

This has a nice effect of translating business requirements into an API. But for cases where I have one-to-many or many-to-many relationships, I don’t find myself using put_assoc.

1 Like

Very helpful. Thanks.