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:

optiontypedescription
typeatommandatory. Example: type: Integer. See Builtin types
requiredbooleanoptional. Defaults to false. When true, a validation error is returned whenever the param is missing or its value is nil.
nestedbooleanoptional. Defaults to false. Denotes the param's type is a nested request.
defaultanyoptional. Value to set if the current value is nil. If a function is given, use its result. Default values are set before validation.
sourceatomoptional. Possible values :path, :query, :body, :auto (default)
validatorfunctionoptional. A custom validator in the format &Mod.fun/arity.
regexregexoptional. A builtin validator
lengthmapoptional. A builtin validator
sizemapoptional. A builtin validator
inlistoptional. A builtin validator
numericalitymapoptional. 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

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

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:

keyvalue typemeaning
:gtintegermin valid value (non-inclusive)
:gteintegermin valid value (inclusive)
:ltintegermax valid value (non-inclusive)
:lteintegermax valid value (inclusive)
:eqintegerexact 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:

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: