This week I ventured into new territory and wrote a custom schema validation for the Elixir/Phoenix project I've been working on.
Ecto.Changeset has a good amount of prebuilt validators that will accomplish most tasks, but if its necessary to validate in a manner outside of the established validations, that can be done with a custom validation. A custom validation function can be just about anything, the only requirement is that it returns a changeset, just like a built-in validation does. This example deals with three fields:
net_gain. Ultimately, the validation is that
This function utilizes two helper functions:
add_error/4. These are
Ecto.Changeset helpers and can be accessed by adding
import Ecto.Changeset to the top of the module. While looking around the docs there are a few other functions that could be convenient in different scenarios, like
fetch_change/2, but for this purpose
get_field/3 fits the bill.
In both cases of the
do block, changeset is returned. If the match checks out, the validation is true and the changeset is simply passed on. If the math does not match up, an
error is tacked onto the
:error field of the changeset and returned.
With the custom validation function taking in -and ultimately returning- the changeset, it can be piped anywhere into a schema validation, like any other Ecto-provided validation:
One caveat I found is that preceding validations in the pipeline are not automatically recognized by a custom validation. For instance, if a field was empty in the
validate_required/3 function, an error would be added to the changeset, and it would keep working down the pipeline of validations. When that empty field gets to the custom validation function, it needs to be handled in some way, or it could throw unexpected errors.
This can be dealt with by incorporating a
changeset.valid? call into the function, which checks the
:errors field of the changeset and return
false if any exist:
The logic shifts from a
case statement to a
with expression, to better handle the multiple checks. If you're not familiar with the
with expression, I wrote about it a pretty detailed explanation of it. Back to the logic, the catch with simply incorporating
changeset.valid? is that it's a broad check, if there are any errors, it will return
false. It works when piped into the function like this:
But, if an engineer were to come in later and add another field to the
validate_required check, one that has nothing to do with the custom validation after it, it could result in a false positive in the first bit of logic of
mathematical_validation/1. For example:
In the above example, if the
first_name field was empty, an error would be added to the changeset via
validate_required. The first check of
mathematical_validation/1 would fail, resulting in the logic portion of the validation not being checked at all, even though the error was not in the any of the three fields that function is concerned about.
A more verbose strategy would be to check for the presence of the fields explicitly before stepping into the logic to be performed, like this:
Now, before the mathematical validation is performed, it is checking that all the necessary fields are present. There is some redundancy with the
validate_required function that is called before this custom function in the pipeline. This is where a rabbit-hole could start. Either the redundancy is accepted, or steps could be taken to remove the
validate_required check entirely and add appropriate error messages to each of the
nil checks in
Properly handling the incoming errors is probably the trickiest part to writing a custom validation. But after the error handling has been considered, literally any logic can be used as a validation as long as a changeset is returned, and that's pretty powerful.
This post is part of an ongoing This Week I Learned series. I welcome any critique, feedback, or suggestions in the comments.