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
- Macros
- Builtin types
- Custom types
- Custom validators
- Nested types
- Builtin validators
- Errors
- Known limitations
Example usage
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
# ...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. |
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 |
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
# ...
endDetailed 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
StringIntegerFloatBooleanDate- 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
# ...
endDetailed 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
endDetailed 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 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
endExamples here.
Known limitations
They will hopefully be addressed in a future version:
-
No more than one validator per param is supported (including builtin validators).<br/>Workaround: call any extra validators inside a custom validator function. Builtin validators are called like so:<br/>
run_builtin_validation(:numericality, opts, value) - Builtin validators can't be instructed to to work on individual list elements.<br/>Workaround: call builtin validators inside a custom validator function (see above note).
-
There is no
Anytype for param values of an unknown nature.<br/>Workaround: omit those in the request definition and access them in the controller viaconn.body_paramsandconn.query_params. -
There is no plain
Listtype, for lists containing non-homegenic values (of different types).<br/>Workaround: same as above -
Types that are list (eg.
type: [Integer]) allownilelements.<br/>Workaround: ensure your custom validator (if any) handles those.