A plug for Phoenix that validates and transforms HTTP request params.


License
MIT

Documentation

PhoenixParams

A plug for Phoenix applications for validating and transforming HTTP request params.

Define a request schema, validate and transform the input before the controller is called: this allows you to write clean and assertive controller code.

Sample app

A simple phoenix application that illustrates basic usage via example can be found here.

Table of contents

Example usage

Detailed examples can be found in the sample app.

  use PhoenixParams,
    error_view: MyAppWeb.ErrorView
    input_key_type: :atom           # :atom | :string (default)

  param :email,
        type: String,
        regex: ~r/[a-z_.]+@[a-z_.]+/

  param :date_of_birth,
        type: Date,
        required: true,
        validator: &__MODULE__.validate_dob/1

  # ...

  def validate_dob(date) do
    date < Date.utc_today || {:error, "can't be in the future"}
  end

  # ...
end
  # ...

  plug MyAppWeb.Requests.User.Create when action == :create

  def create(conn, params) do
    params.date_of_birth
    # => ~D[1986-03-27]

  # ...
end
  # ...

  def render("400.json", %{conn: %Plug.Conn{assigns: %{validation_failed: errors}}}) do
    errors
    # => [
    #   {
    #     "param": "email",
    #     "message": "Validation error: has invalid format",
    #     "error_code": "INVALID"
    #   },
    #   {
    #     "param": "date_of_birth",
    #     "message": "Validation error: invalid date",
    #     "error_code": "INVALID"
    #   }
    # ]
  end

  # ...

Macros

Request can be defined via the macros provided by PhoenixParams.

The param macro

Defines an input parameter to be coerced/validated.

Example:

param :email,
      type: String,
      source: :body,
      regex: ~r/[a-z_.]+@[a-z_.]+/

Detailed examples here

Accepts two arguments: name and options

Allowed options:

option type description
type atom mandatory. Example: type: Integer. See Builtin types
required boolean optional. Defaults to false. When true, a validation error is returned whenever the param is missing or its value is nil.
nested boolean optional. Defaults to false. Denotes the param's type is a nested request.
source atom optional. Possible values :path, :query, :body, :auto (default)
validator function optional. A custom validator in the format &Mod.fun/arity.
regex regex optional. A builtin validator
length map optional. A builtin validator
size map optional. A builtin validator
in list optional. A builtin validator
numericality map optional. A builtin validator

The global_validator macro

Defines a global validation to be applied.

Example:

global_validator &__MODULE__.my_global_validator/1

def my_global_validator(params) do
  # ...
end

Detailed examples here.

Accepts one argument: a remote function in the format &Mod.fun/arity, which will be called with one argument - the coerced params (map).

The function will not be called unless all individual coercions and validations on the params have passed.

The return value is ignored, unless it's a {:error, reason}, which signals a validation failure.

The typedef macro

Defines a custom param type, see custom types.

Builtin types

  • String
  • Integer
  • Float
  • Boolean
  • Date - expects a ISO8601 date and coerces it to a Date struct.
  • DateTime - expects a ISO8601 date with time and coerces it to a DateTime struct.

Types can be wrapped in [], indicating the value is a list. Example:

  • [String]
  • [Integer]
  • ...

Custom types

Defined via the typedef macro.

Useful when the builtin types are not enough to represent the input data.

Example:

typedef Locale, &__MODULE__.coerce_locale/1

def coerce_locale(l) do
  # ...
end

Detailed examples here.

Accepts two arguments: a name and a coercer.

The function will always be called, even if the param is missing (value would be nil in this case).

The return value will replace the original one, unless it's a {:error, reason}, which signals a coercion failure.

Custom validators

Functions which will be called with one argument - the param value - when (if) all params are successfully coerced.

The function's return value is ignored, unless it matches {:error, reason}, which signals a validation failure.

Example:

param :date_of_birth,
      type: Date,
      required: true,
      validator: &__MODULE__.validate_dob/1

def validate_dob(date) do
  date < Date.utc_today || {:error, "can't be in the future"}
end

If the type is a list, in order to validate each element, manually call the validate_each/2 function inside your custom validator. This function expects the list and a function (in the format &Mod.fun/arity) which will validate separate elements.

Example:

param :hobbies,
      type: [String],
      validation: &__MODULE__.validate_hobbies/1


def validate_hobbies(list), do: validate_each(list, &validate_hobby/1)
def validate_hobby(value), do: String.length(hobby) > 3 || {:error, "too short"}

Detailed examples here.

Nested types

Consider the following JSON request:

{
  "name": "Hans Zimmer",
  "age": 31,
  "address": {
    "country": "Germany",
    "city": "Frankfurt AM",
    "street_no": 26
  }
}

The address param is a whole new structure which can be expressed via a nested request definition.

Example:

defmodule UserRequest do
  # ...
  param :name, type: String
  param :age, type: Integer
  param :address, type: AddressRequest, nested: true
end

defmodule AddressRequest do
  # ...
  param :country, type: String
  param :city, type: String
  param :street_no, type: Integer
end

Detailed examples here and here

Builtin validators

Validators for some common use-cases are provided OOTB. Note that, in case the value is a list, those validators are applied to the entire list (not its elements).

numericality

Validates numbers. Accepts the following options:

key value type meaning
:gt integer min valid value (non-inclusive)
:gte integer min valid value (inclusive)
:lt integer max valid value (non-inclusive)
:lte integer max valid value (inclusive)
:eq integer exact valid value

Example:

param :age,
      type: Integer,
      numericality: %{gte: 18}

Detailed examples here.

length

Validates string lengths. Same options as the numericality validator.

Example:

param :email,
      type: String,
      length: %{gt: 5, lt: 100}

Detailed examples here.

size

Validates list size (ie. the number of elements). Same options as the numericality validator.

Example:

param :hobbies,
      type: [String],
      size: %{eq: 5}

in

Validates against a list of valid values. Accepts a list with the allowed values.

Example:

param :language,
      type: String,
      in: ["Elixir", "Ruby", "Python", "Java", "Other"]

Detailed examples here.

regex

Validates against a regular expression. Accepts a pattern.

Example:

param :email,
      type: String,
      regex: ~r/[a-z_.]+@[a-z_.]+/

Detailed examples here.

Errors

Each error is represented by a map and passed to the error view as a validation_failed assign.

The assigned value is either a list (many validation errors) or a map (one error). Example:

[
  %{
    param: "email",
    message: "Validation error: invalid format",
    error_code: "INVALID"
  },
  %{
    param: "date",
    message: "Validation error: required",
    error_code: "MISSING"
  }
]

Each error is a map with the following keys:

  • param - optional. It is omitted if the error is due to a global validation (which usually is used to validate a combination of several params)
  • message - always present
  • error_code - always present. Either "INVALID" or "MISSING"

If the error occurred within a list's element (as reported by validate_each/2) the message value will be "element at index <i>: <error>". Example: "element at index 0: invalid format"

If the error occurred within a nested param, the param value will be "<parent_param>.<nested_param>". Example: "address.street_number: not an integer"

If the error occurred within a list's element, which is also a nested param, the param value will be "<parent_param>.[<i>].<nested_param>". Example: "address.[0].street_number: not an integer"

If you don't want to perform any transformation to those results, just return them as-is in your error view:

  def render("400.json", %{conn: %Plug.Conn{assigns: %{validation_failed: errors}}}) do
    errors
  end

Examples here.

Known limitations

They will hopefully be addressed in a future version:

  • No more than one validator per param is supported (including builtin validators).
    Workaround: call any extra validators inside a custom validator function. Builtin validators are called like so:
    run_builtin_validation(:numericality, opts, value)
  • Builtin validators can't be instructed to to work on individual list elements.
    Workaround: call builtin validators inside a custom validator function (see above note).
  • There is no Any type for param values of an unknown nature.
    Workaround: omit those in the request definition and access them in the controller via conn.body_params and conn.query_params.
  • There is no plain List type, for lists containing non-homegenic values (of different types).
    Workaround: same as above
  • Types that are list (eg. type: [Integer]) allow nil elements.
    Workaround: ensure your custom validator (if any) handles those.