Discard invalid embed entries during casting

I am wondering if there is an idiomatic way to discard invalid embed entries during casting, i.e. when Ecto.Changeset.cast_embed/3 is called, as opposed to marking them as invalid, which will in turn make the parent changeset invalid.

As an example - I have a number of nested embedded schemas that are used to cast incoming webhook data, e.g. a Post that embeds_many :comments. These comments can either be active or inactive (e.g. a deleted comment).

The application only cares about comments that are active, which I can identify by using Ecto.Changeset.validate_inclusion/4 on the comment’s :status. However, this will cause the inactive comments to be marked as invalid, which will then make the parent post invalid.

I was thinking it would be nice if I could discard the invalid embeds (for this particular field), and send the post (with only active comments) through, as it would simplify the business logic in multiple areas.

Does this functionality exist in Ecto? Or would this be a kind of anti-pattern / bad approach? Would love to hear any thoughts.

2 Likes

Let’s approach SOLID, FP and philosophically

Changeset as correctly named, set of changes. Should only carry and work on the changes. Its scope should start and end with the changes [validation, casting]. It should not mutate the changes.

If the consumer pushes n changes, I have two options, filter before or after I provide it as a change.

then depending on my use case I would validate changes accordingly.

  • I could ask my consumer to send only active records since I am the owner of the contract to decrease payloads.
  • Depending on my payload, I would filter() |> validate() as early as possible before operate()
  • I would have a very specific use case changeset to satisfy the needs.
payload
|> filter_out_inactive_comments(..)
|> Post.bulk_update_changeset(..)
|> update_post(..)

bulk_update_changeset(...) do
  ...
  |> validate_inclusion(...)
  |> validate_length(..)
  |> validate_assoc(..)
  ...
end
2 Likes

I think that changesets are not meant to be used like that.

You may be better served by calling the classic Enum.filter/2 before Ecto.Changeset.cast_embed/3, because all you want is just that, filter specific elements from a set of elements! :smile:

2 Likes

Thank you for such a detailed explanation and example! :pray: That all makes sense and is very helpful.

I had been filtering in the operate() stage, which was complicating things and causing unnecessary duplication.

I think I will want to filter “before” then…for some reason I wasn’t considering that, and thinking that the casting should come first. Thanks again!

2 Likes

Thank you! I suspected that might be the case :sweat_smile:I think you’re spot on with filtering before.

1 Like