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.
A simple phoenix application that illustrates basic usage via example can be found here.
- Example usage
- Macros
- Builtin types
- Custom types
- Custom validators
- Nested types
- Builtin validators
- Errors
- Known limitations
Detailed examples can be found in the sample app.
- Define a request:
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
- Set up the controller:
# ...
plug MyAppWeb.Requests.User.Create when action == :create
def create(conn, params) do
params.date_of_birth
# => ~D[1986-03-27]
# ...
end
- Set up the error view:
# ...
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
# ...
Request can be defined via the macros provided by PhoenixParams
.
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. |
default |
any | optional. Value to set if the current value is nil. If a function is given, use its result. Default values are set before validation. |
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 |
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.
Defines a custom param type, see custom 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]
- ...
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.
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.
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
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).
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.
Validates string lengths. Same options as the numericality
validator.
Example:
param :email,
type: String,
length: %{gt: 5, lt: 100}
Detailed examples here.
Validates list size (ie. the number of elements). Same options as the numericality
validator.
Example:
param :hobbies,
type: [String],
size: %{eq: 5}
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.
Validates against a regular expression. Accepts a pattern.
Example:
param :email,
type: String,
regex: ~r/[a-z_.]+@[a-z_.]+/
Detailed examples here.
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 presenterror_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.
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 viaconn.body_params
andconn.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]
) allownil
elements.
Workaround: ensure your custom validator (if any) handles those.