Ecto fields validations

Hello, guys!
I have coded a validation and I am here to ask if there is another way to do it.

My question is about “must_be_sup_or_eql” in changeset
I am checking if date_end is < then date_start.

Seems it is working but my question is: Do ecto have a default way to do it?


 defmodule Matthews.Schemas.Events do
        use Ecto.Schema

        import Ecto.Changeset

        schema "events" do
            field :name,        :string, null: false
            field :location,    :string, null: false        
            field :description, :string
            field :date_start, Ecto.DateTime, null: false
            field :date_end,   Ecto.DateTime, null: false


        @required_fields ~w(name location date_start date_end)a
        @optional_fields ~w(description)a

        def changeset(event, params \\ %{}) do
            |> cast(params, @required_fields ++ @optional_fields)              
            |> validate_required(@required_fields) 
            |> validate_change(:date_start, &must_be_future/2)   
            |> must_be_sup_or_eql        

        def must_be_future(_, date_start) do          
  , Ecto.DateTime.utc)        
            |> get_error

        defp get_error(comparison) when comparison == :lt, do: [ date: "Event start date can't be in the past. #{Ecto.DateTime.utc}" ]
        defp get_error(_), do: []

        def must_be_sup_or_eql(changeset) do                 
            case (changeset.valid?) do

                true -> date_end = changeset.changes.date_end
                        date_start = changeset.changes.date_start    
              , date_start) 
                        |> get_error1(changeset)  
                false -> changeset 

        defp get_error1(comparison, changeset) when comparison == :lt, do: %{changeset | errors: "Event end date can't be < than event start date", valid?: false} 
        defp get_error1(_, _), do: []


I don’t think so because it doesn’t seem like a job for Ecto. It is a custom validation tailored to your apps needs.

If you don’t mind few comments:

  • In the function must_be_sup_or_eql/1 you check if the changeset is valid and then you do your work accordingly. What if the changeset is invalid because of a missing field but your date is actually valid?

  • Maybe it would be better to use Ecto.Changeset.add_error/4 for adding errors in the changeset. It takes care setting the valid? value for you.

Here is how I did something similar. It is old code and I will probably return back to it so maybe can be improved. Also I use Timex for it but it can be adopted to use Ecto.Date.

  def registration_changeset(struct,  params \\ %{}) do
    |> cast(params, [:first_name, :last_name, :birth_date, :area_of_residence])
    |> validate_required([:birth_date, :country, :area_of_residence])
    |> validate_over_18()
   # other things...

  def validate_over_18(changeset, _opts \\ []) do
    validate_change(changeset, :birth_date, fn :birth_date, birth_date ->
      today = Timex.local
      with result when result == -1 <-, today),
           age when age >= 18 <- Timex.diff(today, birth_date, :years)
        1 ->
          [birth_date: "cannot-be-in-the-future"]
        0 ->
          [birth_date: "cannot-be-today"]
        _age ->
          [birth_date: "must-be-18-years-or-older"]

Timex uses -1, 0, 1 instead of :lt, :eq, :gt. Also in the with clause the happy path returns no errors, otherwise sets the appropriate error. validate_change takes care adding the errors in the changeset.

Personally I typically do this in the database with a constraint on the appropriate column and then let the database provide an error. This is usually faster, and ensures that no matter what happens in the application, the database will never allow bad data in.

The downside is that it always tries to insert/update in the database, rather than short-circuit in the application itself. But hopefully your database connectivity is blazing fast so it’s not a big issue :slight_smile: The other annoyance is that you then need to check the error returned from the database; naming the constraint (e.g. end_data_after_start_date) at least allows you to very easily match on the constraint error and return something pretty to the user.

IMHO the upside outweighs the downsides, and for constraints like this I don’t try to implement it in the application.

You could create a validation layer in your application. Simple field-level validations can be executed client-side also (when using input forms), this saves roundtrips to the server. You don’t want to maintain the same validation in multiple places (f.e. javascript on the client + elixir on the server) so you could use a DSL to generate the same validation in different languages (or you can write DSL interpreters for the languages you use). is interesting on the subject. Maybe examples for validation DSL’s can be found with google.
Previous discussion on this forum: Code.eval_something .
Of course validations can be hard-coded in the db also. That means at least a vendor-lockin. What if you want another kind of db?

1 Like

The last reply to this topic was over 5 years ago, yet it ranks substantially high in searches and some things have changed. Adding a perhaps more up-to-date approach, which also draws some inspiration from @LostKobrakai’s custom validation example, in case others find it useful:

@doc """
Validates that the duration is positive, that `start_at` is before `end_at`.
def validate_duration(changeset, start_at_key, end_at_key)
    when is_atom(start_at_key) and is_atom(end_at_key) do
  with {_, start_at} when is_struct(start_at, DateTime) <- fetch_field(changeset, start_at_key),
        {_, end_at} when is_struct(end_at, DateTime) <- fetch_field(changeset, end_at_key) do
    if, end_at) == :lt do
      add_error(changeset, :end_at, "cannot be before the start date/time")
    _error ->
      |> add_error(:start_at, "needs to be a valid date/time")
      |> add_error(:end_at, "needs to be a valid date/time")

Some changes I’d personally make:

  • Leave the requirement that the fields are set to another validation, e.g. validate_required(changeset, [:start_at, :end_at])
  • Clean up some of the pattern matching
@doc """
Validates that the value of `start_at_key` is before `end_at_key`, if present.
def validate_duration(changeset, start_at_key, end_at_key)
    when is_atom(start_at_key) and is_atom(end_at_key) do
  with {_, {_, %DateTime{} = start_at}} <- {start_at_key, fetch_field(changeset, start_at_key)},
       {_, {_, %DateTime{} = end_at}} <- {end_at_key, fetch_field(changeset, end_at_key)},
       :lt <-, end_at) do
    {_, :error} ->

    {key, {_, other}} ->
      |> add_error(key, "must be a valid datetime")

    _error ->
      |> add_error(end_at_key, "must be after the start time")