ExEssentials.Web.Plugs.RequestValidator (ExEssentials v0.2.4)

View Source

A Plug for validating request parameters using Ecto.Changeset.

This plug enables modular and scalable validation of incoming request parameters by delegating the validation logic to a specific module (the :validator). It dynamically calls a function named after the current :phoenix_action, passing the request params as an argument. The function must return an %Ecto.Changeset{}.

If the changeset is valid, the request proceeds as usual. If invalid, a 400 Bad Request response is sent in the application/problem+json format.

Options

  • :validator(required) The module responsible for validation. It must implement a function for each action, with the same name as the :phoenix_action and an arity of 1 (accepting the params).

Example Usage

Given a controller with the :create action and a validator module:

defmodule MyAppWeb.Validators.Types.User.Preferences do
  use Ecto.Schema

  import Ecto.Changeset

  @themes ~w(light dark)
  @languages ~w(en pt-br)
  @fields ~w(theme language)a

  @primary_key false
  embedded_schema do
    field(:theme, :string)
    field(:language, :string)
  end

  def changeset(schema, params) do
    schema
    |> cast(params, @fields)
    |> validate_required(@fields)
    |> validate_inclusion(:theme, @themes)
    |> validate_inclusion(:language, @languages)
  end
end

defmodule MyAppWeb.Validators.Types.User.Course do
  use Ecto.Schema

  import Ecto.Changeset

  @fields ~w(name duration level)a
  @levels ~w(beginner intermediary advanced)

  @primary_key false
  embedded_schema do
    field(:name, :string)
    field(:duration, :integer)
    field(:level, :string)
  end

  def changeset(schema, params) do
    schema
    |> cast(params, @fields)
    |> validate_required(@fields)
    |> validate_number(:duration, greater_than: 0)
    |> validate_inclusion(:level, @levels)
  end
end

defmodule MyAppWeb.Validators.User do
  use Ecto.Schema

  import Ecto.Changeset

  alias MyAppWeb.Validators.Types.User.Course
  alias MyAppWeb.Validators.Types.User.Preferences
  alias MyAppWeb.Validators.User

  @fields ~w(name email)a

  schema "users" do
    field(:name, :string)
    field(:email, :string)

    embeds_one(:preferences, Preferences)
    embeds_many(:courses, Course)
  end

  def create(params) do
    %User{}
    |> cast(params, @fields)
    |> validate_required(@fields)
    |> cast_embed(:preferences, required: true)
    |> cast_embed(:courses, required: true)
  end
end

You can plug the validator into your controller or router pipeline:

plug ExEssentials.Web.Plugs.RequestValidator, validator: MyAppWeb.Validators.User

Error Response Format

When validation fails, the response follows the structure inspired by RFC 7807 - Problem Details for HTTP APIs:

{
  "errors": [
    {
      "code": "invalid_parameter",
      "detail": "The 'name' field can't be blank",
      "title": "Invalid request parameters"
    },
    {
      "code": "invalid_parameter",
      "detail": "The 'courses.[1].level' field is invalid",
      "title": "Invalid request parameters"
    },
    {
      "code": "invalid_parameter",
      "detail": "The 'preferences.language' field can't be blank",
      "title": "Invalid request parameters"
    },
    {
      "code": "invalid_parameter",
      "detail": "The 'preferences.theme' field is invalid",
      "title": "Invalid request parameters"
    }
  ],
  "status_code": 400
}

Each validation error includes:

  • code – configurable error code.
  • detail – human-readable description of the error.
  • title – general error title.

Configuration

You can customize certain aspects via your config.exs:

config :ex_essentials, :web_request_validator,
  json_library: Jason,
  error_code: :invalid_parameter,
  error_title: "Invalid request parameters"

By default, the module uses Jason to encode JSON responses, assigning the error code :invalid_parameter and the error title "Invalid request parameters".

Notes

  • This plug assumes Phoenix is being used and requires :phoenix_action to be set in conn.private.
  • Ecto is required for working with changesets.