
Build your own library to render json response in Phoenix
In my previous article, I introduced my library call JsonView to render json response easier. You can read it here: Render Ecto schema to json with …

Validation is a must have part of web application. You have to validate request parameter before processing, you validate data before inserting to database, and many more.
Normally, I use Ecto.Changeset to do validation job. But it comes with changeset, I have to build schema, changeset then do the validation. Sometime you just don’t need too much thing like that.
So today we are going to build a simple validation module to use without changeset, or in some project you don’t use Ecto, or just for learning.
map, list, stringThat’s is quite enough, you can define more if you want. And the module will support a simple API to validate a value
1validate(value::any(), validations::keyword()) :: :ok | {:error, String.t()}
Let’s start.
Let’s call our module Checky. Type check is quite straight forward. Elixir support most of type check guard that we need:
 1defmodule Checky do
 2  def validate_type(value, :boolean) when is_boolean(value), do: :ok
 3  def validate_type(value, :integer) when is_integer(value), do: :ok
 4  def validate_type(value, :float) when is_float(value), do: :ok
 5  def validate_type(value, :number) when is_number(value), do: :ok
 6  def validate_type(value, :string) when is_binary(value), do: :ok
 7  def validate_type(value, :binary) when is_binary(value), do: :ok
 8  def validate_type(value, :tuple) when is_tuple(value), do: :ok
 9  def validate_type(value, :array) when is_list(value), do: :ok
10  def validate_type(value, :list) when is_list(value), do: :ok
11  def validate_type(value, :atom) when is_atom(value), do: :ok
12  def validate_type(value, :function) when is_function(value), do: :ok
13  def validate_type(value, :map) when is_map(value), do: :ok
14  # we will add some more validation here
15  def validate_type(_, type), do: {:error, "is not a #{type}"}
16end
Easy, right? Now let’s support checking for struct:
1defmodule Checky do
2
3  ...
4  # from Elixir 1.12 you can do this
5  def validate_type(value, struct_name) when is_struct(value, struct_name), do: :ok
6  # this is for Elixir before 1.12
7  def validate_type(%{__struct__: struct}, struct_name) when struct == struct_name, do: :ok
8  ...
9end
keyword 1defmodule Checky do
 2  ...
 3  # empty list is also a empty keyword
 4  def validate_type([] = _check_item, :keyword), do: :ok
 5  # if list item is a tuple of 2 and first element is atom then it is a keyword list
 6  def validate_type(items, :keyword) when is_list(items) do
 7    valid? = Enum.all(item, fn 
 8        {key, _} when is_atom(key) -> true
 9        _ -> false 
10    end)
11    
12    if valid? do
13      :ok
14    else
15      {:error, "is not a keyword"}
16    end
17  end
18  ...
19end
{:array, type} which is similar to Ecto.Schema. 1defmodule Checky do
 2  ...
 3  def validate_type(value, {:array, type}) when is_list(value) do
 4    # We will check type for each value in the list
 5    array(value, &validate_type(&1, type))
 6  end
 7  ...
 8   # loop and validate element in array using `validate_func`
 9  defp array(data, validate_func)
10
11  defp array([], _) do
12    :ok
13  end
14
15  # validate recursively, and return error if any vadation failed
16  defp array([h | t], validate_func) do
17    case validate_func.(h) do
18      :ok ->
19        array(t, validate_func)
20      err ->
21        err
22    end
23  end
24end
Phew! We have done with type validation. You can add more type validation if you want.
This validation is super easy, Regex do that for us:
1defmodule Checky end
2  def validate_format(value, check) when is_binary(value) do
3    if Regex.match?(check, value), do: :ok, else: {:error, "does not match format"}
4  end
5
6  def validate_format(_value, _check) do
7    {:error, "format check only support string"}
8  end
9end
These are trivial checks too. Just make sure it is implement Enumerable protocol.
 1defmodule Checky do
 2  def validate_inclusion(value, enum) do
 3    if Enumerable.impl_for(enum) do
 4      if Enum.member?(enum, value) do
 5        :ok
 6      else
 7        {:error, "not be in the inclusion list"}
 8      end
 9    else
10      {:error, "given condition does not implement protocol Enumerable"}
11    end
12  end
13
14  @doc """
15  Check if value is **not** included in the given enumerable. Similar to `validate_inclusion/2`
16  """
17  def validate_exclusion(value, enum) do
18    if Enumerable.impl_for(enum) do
19      if Enum.member?(enum, value) do
20        {:error, "must not be in the exclusion list"}
21      else
22        :ok
23      end
24    else
25      {:error, "given condition does not implement protocol Enumerable"}
26    end
27  end
28end
This is one of the most complicated part of our module. It’s not difficult, it’s just long. We will support following checks:
equal_togreater_than_or_equal_to | mingreater_thanless_thanless_than_or_equal_to | maxAnd it should support multiple check like this:
1 validate_number(x, [min: 10, max: 20])
First we code validation function for single condition like this
1  def validate_number(number, {:equal_to, check_value}) do
2    if number == check_value do
3      :ok
4    else
5      {:error, "must be equal to #{check_value}"}
6    end
7  end
As I said, it’s so simple. You can fill the remaining check right? Or you can check the final code at the end of the post. After implementing all validation fucntion for number, it’s time to support multiple condtion check.
 1  @spec validate_number(integer() | float(), keyword()) :: :ok | error
 2  def validate_number(value, checks) when is_list(checks) do
 3    if is_number(value) do
 4      checks
 5      |> Enum.reduce(:ok, fn
 6        check, :ok ->
 7          validate_number(value, check)
 8
 9        _, error ->
10          error
11      end)
12    else
13      {:error, "must be a number"}
14    end
15  end
Length is just a number, so we can reuse number validation. We just have to check if given value is one of support types: list, map, string, and tuple
We will implement get_length/1 function to get data length first.
1  @spec get_length(any) :: pos_integer() | {:error, :wrong_type}
2  defp get_length(param) when is_list(param), do: length(param)
3  defp get_length(param) when is_binary(param), do: String.length(param)
4  defp get_length(param) when is_map(param), do: param |> Map.keys() |> get_length()
5  defp get_length(param) when is_tuple(param), do: tuple_size(param)
6  defp get_length(_param), do: {:error, :wrong_type}
Then we do number validation on the length value
 1  @spec validate_length(support_length_types, keyword()) :: :ok | error
 2  def validate_length(value, checks) do
 3    with length when is_integer(length) <- get_length(value),
 4         # validation length number
 5         :ok <- validate_number(length, checks) do
 6      :ok
 7    else
 8      {:error, :wrong_type} ->
 9        {:error, "length check supports only lists, binaries, maps and tuples"}
10
11      {:error, msg} ->
12        # we prepend length to message return by validation number to get full message
13        # like: "length must be equal to x"
14        {:error, "length #{msg}"}
15    end
16  end
Most of time you want to use multiple valitions on the data. So we will add a function that do multiple validation
We define a simple structure for validation first. This is our validate function spec
1 @spec validate(any(), keyword()) :: :ok | {:error, messages}
Then we can use it like this:
1Checky.validate(value, type: :string, format: ~r/\d\d.+/, length: [min: 8, max: 20])
Validations is a keyword list with short name for validation:
:type -> validate_type:format -> validate_format:in -> validate_inclusion:not_in -> validate_exclusion:number -> validate_number:length -> validate_lengthDefine mapping function:
1  defp get_validator(:type), do: &validate_type/2
2  defp get_validator(:format), do: &validate_format/2
3  defp get_validator(:number), do: &validate_number/2
4  defp get_validator(:length), do: &validate_length/2
5  defp get_validator(:in), do: &validate_inclusion/2
6  defp get_validator(:not_in), do: &validate_exclusion/2
7  defp get_validator(name), do: {:error, "validate_#{name} is not support"}
Go checking validations one by one
 1  def validate(value, validators) do
 2    do_validate(value, validators, :ok)
 3  end
 4
 5  defp do_validate(_, [], acc), do: acc
 6
 7  # check validations one by one
 8  defp do_validate(value, [h | t] = _validators, acc) do
 9    case do_validate(value, h) do
10      :ok -> do_validate(value, t, acc)
11      error -> error
12    end
13  end
14
15  # validate single validation
16  defp do_validate(value, {validator, opts}) do
17    case get_validator(validator) do
18      {:error, _} = err -> err
19      validate_func -> validate_func.(value, opts)
20    end
21  end
Writing a validation module is not so hard. Now you can add more validations to fit your need. As I promised, this is the full source of the module with custom validation fucntion. https://github.com/bluzky/valdi/blob/main/lib/valdi.ex
Thank you for reading to the end of this post. Please leave me a comment.

In my previous article, I introduced my library call JsonView to render json response easier. You can read it here: Render Ecto schema to json with …

When writing API with Phoenix and render json to client, For some fields I want to keep it original value. For some fields, I want to do some …