martinthenth
Goal - A parameter validation library based on Ecto
Hi fellow Elixirists! ![]()
About 8 months ago, I wrote a blog post on validating Phoenix controller parameters using Ecto Changesets in combination with Ecto Embedded Schemas (Using Ecto changesets for API request parameter validation).
Since then, I found that writing embedded schemas for validating parameters to be quite tedious. It required setting up new modules, a lot of boilerplate code, and complex file organisation.
So I decided to write a library that replaces the boilerplate code with a simple syntax, that can be written inside controllers, and is still based on Ecto Changesets to leverage its validation capabilities.
It’s called Goal, and it offers those capabilities with a nice syntax:
defmodule MyApp.SomeController do
import Goal
import Goal.Syntax
def create(conn, params) do
with {:ok, attrs} <- validate_params(params, schema()) do
...
end
end
defp schema do
defschema do
required :uuid, :string, format: :uuid
required :name, :string, min: 3, max: 3
optional :age, :integer, min: 0, max: 120
optional :gender, :enum, values: ["female", "male", "non-binary"]
optional :data, :map do
required :color, :string
optional :money, :decimal
optional :height, :float
end
end
end
end
It features:
- Compatibility with all primitive types from Ecto.Schema
- Customisable regexes for
email,password, andurlformats, so you can maintain compatibility with your production system - Unlimited nested maps and lists of maps
-
trimandsquishto trim and collapse whitespaces -
optionalandrequiredarguments for defining optional and required fields - An extended version of
Ecto.Changeset.traverse_errors/2(aptly calledGoal.Changeset.traverse_errors/2) that works with Goal and Ecto’s existing functionality
You can use Goal for validating Phoenix controller action parameters, but it will work with any data source as long as it’s represented in a map.
I aim for Goal to cover all parameter validations that Ecto.Changeset offers for database fields. I think I covered most with the 0.1.1 version; but if you’d like another validation, then please contribute
or open an issue on GitHub.
It was inspired by Ruby’s dry-schema, and I had to borrow code from Ecto. Thank you for making such awesome libraries. ![]()
Feedback and suggestions are very welcome
. For example, I was thinking about generating the chain of Ecto.Changeset functions at compile-time, to crunch out some additional usecs ![]()
Most Liked
stefanchrobot
This looks great! How about supporting this syntax:
defschema schema do
required ...
end
I’d love to see the functionality of Goal incorporated into Ecto. Did you consider that? If so, there’s one thing to think about: the Goal schema seems to be controller-specific so it makes sense to have required, min, max, etc. as options. On the other hand Ecto schemas are reused across the changeset functions (each function has its own set of required/optional/validations). I still think it would make sense to support what Goal is using, maybe with a different macro (there’s schema and embedded_schema, so maybe validation_schema?).
fuelen
required :uuid, :string, format: :uuid
optional :gender, :enum, values: ["female", "male", "non-binary"]
if the library is based on Ecto, then why not to use built-in types? I mean this
required :uuid, Ecto.UUID
optional :gender, Ecto.Enum, values: ["female", "male", "non-binary"]
martinthenth
Goal 0.1.3 released ![]()
I recently switched to LiveViews and found that I have the same need to validate incoming parameters as with JSON APIs. Since LiveViews depend on changesets for validation, I decided to expose the Goal.build_changeset(params, schema) function in the library.
I find using database Ecto.Schemas in LiveViews, like the examples in the LiveView docs do, a bit problematic because I often need fields that aren’t defined in the Ecto.Schema. Embedded schemas are great, but it’s a lot of boilerplate…
Plus, often my changesets have required fields like foreign keys that I cannot validate in the LiveView so the changeset is never valid; a state that I use in my forms to enable/disable form submit buttons. ![]()
Goal solves all that with a sweet syntax.
Using Goal for LiveViews is super easy:
defmodule MyApp.SomeLiveView do
import Goal.Syntax
def handle_event("validate", %{"some" => some_params}, socket) do
changeset =
some_params
|> Goal.build_changeset(schema())
|> Map.put(:action, :validate)
{:noreply, assign(socket, :changeset, changeset)}
end
defp schema do
defschema do
required :id, :uuid
required :name, :string, max: 20
optional :age, :integer, min: 0, max: 120
optional :gender, :enum, values: ["female", "male", "non-binary"]
optional :address, :string
optional :data, :map do
required :color, :string
optional :money, :decimal
optional :height, :float
end
end
end
end







