Joi (Joi v0.2.1) View Source

Background

The community already has a lot of verification-related libraries, such as skooma, vex, but why write a new one?

The api of vex is very much like Rails ActiveModel Validations, and it feels too complex for me, especially when it comes to customizing some validation modules, which is not convenient and flexible enough. Skooma, on the other hand, is very flexible and I find it particularly useful when validating non-map data structures.

So the goal of this repository is:

  1. Support most of the types supported by the native sideway/joi
  2. Nested validation support.
  3. Easy internationalization
  4. Easy to extend

Installation

def deps do
  [
    {:joi, "~> 0.2.0"},
  ]
end

Usage

Joi validates data against a predefined schema with the Joi.validate/2 function.

If the data is valid, the function returns {:ok, data}. The returned data will perform the transformation according to the provided schema.

if the passed data does not match the type defined in the schema, the function returns {:error, errors}, the errors is a list of Joi.Error, that a struct contains four fields:

  • context: map providing context of the error containing:
    • key: key of the value that erred, equivalent to the last element of path.
    • value: the value that failed validation.
    • other error specific properties as described for each error code.
  • message: string with a description of the error.
  • path: list where each element is the accessor to the value where the error happened.
  • type: type of the error.

, When a field is received that is not specified in the provided schema, it does nothing and returns {:ok, data}.


  iex> schema = %{a: [:integer]}
  %{a: [:integer]}
  iex> data1 = %{a: 1}
  %{a: 1}
  iex> Joi.validate(data1, schema)
  {:ok, %{a: 1}}
  iex> data2 = %{a: <<123>>}
  iex> Joi.validate(data2, schema)
  {:error,
  [
    %Joi.Error{
      context: %{key: :a, value: "{"},
      message: "a must be a integer",
      path: [:a],
      type: "integer.base"
    }
  ]}

Supported Types

Atom

error types

  • atom.base

  • atom.inclusion

    Additional local context properties:

    %{inclusion: list()}
  • atom.required

Boolean

error types

  • boolean.base
  • boolean.required

Date

error types

  • date.base

    Examples:

    iex> schema = %{day: [:date]}
    %{day: [:date]}
    iex> Joi.validate(%{day: "2021-07-20"}, schema)
    {:ok, %{day: ~D[2021-07-20]}}
    iex> Joi.validate(%{day: "20210720"}, schema)
    {:error,
    [
      %Joi.Error{
        context: %{key: :day, value: "20210720"},
        message: "day must be a valid ISO-8601 date",
        path: [:day],
        type: "date.base"
      }
    ]}
  • date.required

Datetime

error types

  • datetime.baseExamples:
    iex> schema = %{t: [:datetime]}
    %{t: [:datetime]}
    iex> d = %{t: ~U[2021-08-27 00:30:59.783833Z]}
    %{t: ~U[2021-08-27 00:30:59.783833Z]}
    iex> Joi.validate(d, schema)
    {:ok, %{t: ~U[2021-08-27 00:30:59.783833Z]}}
    iex> d2 = %{t: "2021-08-27 00:30:59.783833Z"}
    %{t: "2021-08-27 00:30:59.783833Z"}
    iex> Joi.validate(d2, schema)
    {:ok, %{t: ~U[2021-08-27 00:30:59.783833Z]}}
    iex> d3 = %{t: "2021-08-27 00:30:59"}
    %{t: "2021-08-27 00:30:59"}
    iex> Joi.validate(d3, schema)
    {:error,
     [
       %Joi.Error{
         context: %{key: :t, value: "2021-08-27 00:30:59"},
         message: "t must be a valid ISO-8601 datetime",
         path: [:t],
         type: "datetime.base"
       }
     ]}
    • datetime.required

Decimal

error types

  • decimal.base

  • decimal.inclusion

    Additional local context properties:

    %{inclusion: list()}

    Examples:

    iex> schema = %{d: [:decimal, inclusion: [Decimal.new(1)]]}
    %{d: [:decimal, {:inclusion, [Decimal.new(1)]}]}
    iex> Joi.validate(%{d: Decimal.new(2)}, schema)
    {:error,
    [
      %Joi.Error{
        context: %{inclusion: [Decimal.new(1)], key: :d, value: Decimal.new(2)},
        message: "d must be one of #{inspect [Decimal.new(1)]}",
        path: [:d],
        type: "decimal.inclusion"
      }
    ]}
  • decimal.max

    Additional local context properties:

    %{limit: Decimal.t()}

    Examples:

      iex> schema = %{n: [:decimal, max: 1]}
      %{n: [:decimal, {:max, 1}]}
      iex> Joi.validate(%{n: 0}, schema)
      {:ok, %{n: Decimal.new(0)}}
      iex> Joi.validate(%{n: "2"}, schema)
      {:error,
      [
        %Joi.Error{
          # string value will be converted to a float
          context: %{key: :n, limit: Decimal.new(1), value: Decimal.from_float(2.0)}, 
          message: "n must be less than or equal to 1",
          path: [:n],
          type: "decimal.max"
        }
      ]}
      iex> # max also support Decimal.t()
      iex> schema = %{n: [:decimal, max: Decimal.new(1)]}
      %{n: [:decimal, {:max, Decimal.new(1)}]}
      iex> Joi.validate(%{n: 2}, schema)
      {:error,
      [
        %Joi.Error{
          context: %{key: :n, limit: Decimal.new(1), value: Decimal.new(2)},
          message: "n must be less than or equal to 1",
          path: [:n],
          type: "decimal.max"
        }
      ]}
      iex> # max also support float
      iex> schema = %{n: [:decimal, max: 1.1]}
      %{n: [:decimal, {:max, 1.1}]}
      iex> Joi.validate(%{n: 2}, schema)
      {:error,
      [
        %Joi.Error{
          context: %{key: :n, limit: Decimal.from_float(1.1), value: Decimal.new(2)},
          message: "n must be less than or equal to 1.1",
          path: [:n],
          type: "decimal.max"
        }
      ]}
  • decimal.min

    Additional local context properties:

    %{limit: Decimal.t()}
  • decimal.greater

    Additional local context properties:

    %{limit: Decimal.t()}

    Examples:

      iex> schema = %{n: [:decimal, greater: 0]}
      %{n: [:decimal, {:greater, 0}]}
      iex> Joi.validate(%{n: 2}, schema)
      {:ok, %{n: Decimal.new(2)}}
      iex> Joi.validate(%{n: 0}, schema)
      {:error,
        [
          %Joi.Error{
            context: %{key: :n, limit: Decimal.new(0), value: Decimal.new(0)},
            message: "n must be greater than 0",
            path: [:n],
            type: "decimal.greater"
          }
      ]}
  • decimal.less

    Additional local context properties:

    %{limit: Decimal.t()}

    Examples:

      iex> schema = %{n: [:decimal, less: 0]}
      %{n: [:decimal, {:less, 0}]}
      iex> Joi.validate(%{n: -1}, schema)
      {:ok, %{n: Decimal.new(-1)}}
      iex> Joi.validate(%{n: 0}, schema)
      {:error,
        [
          %Joi.Error{
            context: %{key: :n, limit: Decimal.new(0), value: Decimal.new(0)},
            message: "n must be less than 0",
            path: [:n],
            type: "decimal.less"
          }
      ]}
  • decimal.required

Float

error types

  • float.base

  • float.inclusion

    Additional local context properties:

    %{inclusion: list()}
  • float.max

    Additional local context properties:

    %{limit: float() | integer()}
  • float.min

    Additional local context properties:

    %{limit: float() | integer()}
  • float.greater

    Additional local context properties:

    %{limit: float() | integer()}
  • float.less

    Additional local context properties:

    %{limit: float() | integer()}
  • float.required

Integer

error types

  • integer.base

  • integer.inclusion

    Additional local context properties:

    %{inclusion: list()}
  • integer.max Additional local context properties:

    %{limit: integer()}
  • integer.min Additional local context properties:

    %{limit: integer()}
  • integer.greater

    Additional local context properties:

    %{limit: integer()}
  • integer.less

    Additional local context properties:

    %{limit: integer()}
  • integer.required

List

list supports some special features, such as validation for each element:

iex> schema = %{l: [:list, type: :integer]}
%{l: [:list, {:type, :integer}]}
iex> Joi.validate(%{l: [<<123>>]}, schema)
{:error,
 [
   %Joi.Error{
     context: %{key: :l, value: ["{"]},
     message: "l must be a list of integer",
     path: [:l],
     type: "list.integer"
   }
 ]}

or a sub schema:

iex> sub_schema = %{key: [:integer]}
%{key: [:integer]}
iex> schema = %{l: [:list, schema: sub_schema]}
%{l: [:list, {:schema, %{key: [:integer]}}]}
iex> Joi.validate(%{l: [%{key: 1}, %{key: <<123>>}]}, schema)
{:error,
 [
   %Joi.Error{
     context: %{key: :key, value: "{"},
     message: "key must be a integer",
     path: [:l, 1, :key],
     type: "integer.base"
   }
 ]}

error types

  • list.base

  • list.length Additional local context properties:

    %{limit: integer()}
  • list.max_length

Additional local context properties:

%{limit: integer()}
  • list.min_length

Additional local context properties:

%{limit: integer()}
  • list.required
  • list.schema
  • list.type

Map

:map also supports validation with sub schema:

iex> sub_schema = %{sub_key: [:integer]}
%{sub_key: [:integer]}
iex> schema = %{m: [:map, schema: sub_schema]}
%{m: [:map, {:schema, %{sub_key: [:integer]}}]}
iex> Joi.validate(%{m: %{sub_key: <<123>>}}, schema)
{:error,
 [
   %Joi.Error{
     context: %{key: :sub_key, value: "{"},
     message: "sub_key must be a integer",
     path: [:m, :sub_key],
     type: "integer.base"
   }
 ]}

error types

  • map.base
  • map.required

String

error types

  • string.base

  • string.format

  • string.inclusion Additional local context properties:

    %{inclusion: list()}
  • string.length

    Additional local context properties:

    %{limit: integer()}
  • string.max_length

    Additional local context properties:

    %{limit: integer()}
  • string.min_length

    Additional local context properties:

    %{limit: integer()}
  • string.required

  • string.uuid

Custom functions

There is nothing magical about custom functions, you just need to return the same format as Joi's type, and then use :f as the key for the custom function in the schema, so you can use one or more custom functions inside a type.

iex> import Joi.Util
iex> func = fn field, data -> 
...>   case data[field] == 1 do
...>     true -> {:ok, data}
...>     false -> error("custom", path: [field], value: data[field], message: "does not match the custom function")
...>   end
...> end
iex> schema = %{id: [:integer, f: func]}
iex> data = %{id: 2}
iex> Joi.validate(data, schema)
{:error, [
  %Joi.Error{
    context: %{key: :id, message: "does not match the custom function", value: 2},
    message: "does not match the custom function",
    path: [:id],
    type: "custom"
  }
]}

Link to this section Summary

Functions

Validate the data by schema

Link to this section Functions

Specs

validate(map(), map()) :: {:error, [Joi.Error.t()]} | {:ok, map()}

Validate the data by schema

Examples:

iex> schema = %{
...>   id: [:string, uuid: true],
...>   username: [:string, min_length: 6],
...>   pin: [:integer, min: 1000, max: 9999],
...>   new_user: [:boolean, truthy: ["1"], falsy: ["0"], required: false],
...>   account_ids: [:list, type: :integer, max_length: 3],
...>   remember_me: [:boolean, required: false]
...> }
%{
  account_ids: [:list, {:type, :integer}, {:max_length, 3}],
  id: [:string, {:uuid, true}],
  new_user: [:boolean, {:truthy, ["1"]}, {:falsy, ["0"]}, {:required, false}],
  pin: [:integer, {:min, 1000}, {:max, 9999}],
  remember_me: [:boolean, {:required, false}],
  username: [:string, {:min_length, 6}]
}
iex> data = %{id: "c8ce4d74-fab8-44fc-90c2-736b8d27aa30", username: "user@123", pin: 1234, new_user: "1", account_ids: [1, 3, 9]}
%{
  account_ids: [1, 3, 9],
  id: "c8ce4d74-fab8-44fc-90c2-736b8d27aa30",
  new_user: "1",
  pin: 1234,
  username: "user@123"
}
iex> Joi.validate(data, schema)
{:ok,
 %{
   account_ids: [1, 3, 9],
   id: "c8ce4d74-fab8-44fc-90c2-736b8d27aa30",
   new_user: true,
   pin: 1234,
   username: "user@123"
 }}
iex> error_data = %{id: "1", username: "user", pin: 999, new_user: 1, account_ids: [1, 3, 9, 12]}
%{account_ids: [1, 3, 9, 12], id: "1", new_user: 1, pin: 999, username: "user"}
iex> Joi.validate(error_data, schema)
{:error,
 [
   %Joi.Error{
     context: %{key: :username, limit: 6, value: "user"},
     message: "username length must be at least 6 characters long",
     path: [:username],
     type: "string.min_length"
   },
   %Joi.Error{
     context: %{key: :pin, limit: 1000, value: 999},
     message: "pin must be greater than or equal to 1000",
     path: [:pin],
     type: "integer.min"
   },
   %Joi.Error{
     context: %{key: :new_user, value: 1},
     message: "new_user must be a boolean",
     path: [:new_user],
     type: "boolean.base"
   },
   %Joi.Error{
     context: %{key: :id, value: "1"},
     message: "id must be a uuid",
     path: [:id],
     type: "string.uuid"
   },
   %Joi.Error{
     context: %{key: :account_ids, limit: 3, value: [1, 3, 9, 12]},
     message: "account_ids must contain less than or equal to 3 items",
     path: [:account_ids],
     type: "list.max_length"
   }
 ]}