Validating list of emails in the changeset

I am trying to validate list of emails through ecto changeset. The field is defined in the schema as:

    field(:cc_email, {:array, :string})

It was :string before and I was validating it like this:

defp validate_email_format(changeset, field) do
    changeset
    |> validate_format(field, MyApp.Accounts.User.email_regex(), message: "is invalid")
    |> validate_length(field, max: 160)
end

How can I modify this function to validate email list now?

One way is to use the Ecto.Changeset.validate_change/3 function instead of validate_format. You will have to figure out how to write the callback function yourself, but there is an example in the documentation :slight_smile:

https://hexdocs.pm/ecto/Ecto.Changeset.html#validate_change/3

To add to this: changeset validations (and their errors) are on a per field basis. So you either need to have a custom validation for checking the individual items of an array of them as suggested, or if you want more fine grained errors/validations consider embeds_many over arrays of something.

1 Like

Thank you, this is what I needed.

You’d definitely have to create a custom validator using Ecto.Changeset.validate_change/3.
If you want to use the existing API for a single element, then you can use schemaless changeset with a single element like this:

defmodule MyHelper do
  def validate_array(changeset, field, map_single_element_changeset_callback) do
    changeset = Ecto.Changeset.change(changeset)
    {:array, single_element_type} = changeset.types[field]

    changeset
    |> Ecto.Changeset.validate_change(field, fn _, values ->
      error =
        Enum.find_value(values, fn value ->
          {%{}, %{value: single_element_type}}
          |> Ecto.Changeset.cast(%{value: value}, [:value])
          |> map_single_element_changeset_callback.()
          |> Ecto.Changeset.apply_action(:insert)
          |> case do
            {:ok, _} ->
              nil

            {:error, changeset} ->
              {message, _meta} = changeset.errors[:value]
              {message, value}
          end
        end)

      case error do
        nil ->
          []

        {message, value} ->
          [{field, "contains invalid element #{value}: #{message}"}]
      end
    end)
  end
end

usage:


> {%{}, %{emails: {:array, :string}}}
|> Ecto.Changeset.cast(%{emails: ["valid@example.com", "invalid[at]example.com"]}, [:emails])
|> MyHelper.validate_array(:emails, fn changeset -> changeset |> Ecto.Changeset.validate_format(:value, ~r/@/) end)

#Ecto.Changeset<
  action: nil,
  changes: %{emails: ["valid@example.com", "invalid[at]example.com"]},
  errors: [
    emails: {"contains invalid element invalid[at]example.com: has invalid format",
     []}
  ],
  data: %{},
  valid?: false
>
1 Like