Goal - A parameter validation library based on Ecto

Hi fellow Elixirists! :wave:

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:

  1. Compatibility with all primitive types from Ecto.Schema
  2. Customisable regexes for email, password, and url formats, so you can maintain compatibility with your production system
  3. Unlimited nested maps and lists of maps
  4. trim and squish to trim and collapse whitespaces
  5. optional and required arguments for defining optional and required fields
  6. An extended version of Ecto.Changeset.traverse_errors/2 (aptly called Goal.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 :pray: 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. :bow:

Feedback and suggestions are very welcome :pray:. For example, I was thinking about generating the chain of Ecto.Changeset functions at compile-time, to crunch out some additional usecs :racing_car:

10 Likes

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?).

2 Likes
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"]
2 Likes

I felt it would be more intuitive to decouple the controller context from the database context (like, most people know uuid, but do they know Ecto.UUID?). That said, it can definitely be made to work like that in the library :thinking:

Edit: I originally said using Ecto.UUID works, but it only works when using the validation syntax directly; with defschema it doesn’t work automatically.

Cool idea! Maybe the argument could be a function reference so the defp schema do wouldn’t be required :thinking:

With Ecto I often find I am using custom validations and parsing, like with update_change/2. I suppose the schema fields could receive a function as an argument, it’s worth a thought

to decouple the controller context from the database context

Ecto is not only about the database :slight_smile:

people know uuid, but do they know Ecto.UUID?

people will know if guide them

Let me offer you a different opinion:

Let’s say, next to the controllers, you’d need to create something else, a GraphQL API for example. With this approach you’d have to implement the validating logic twice if I’m not mistaken.

What I usually do, in the controllers, is just check for the required fields and filter out fields that should not be there, and keep the validation like length and what not in the context. More of a normalize instead of validation.

That way your controllers and GraphQL schemas and etc, just need to check that the fields exists and the rest of the validation is shared.

Saša explains it better here: Towards Maintainable Elixir: The Core and the Interface | by Saša Jurić | Very Big Things | Medium

1 Like