ExParamsSchema converts and validates string-oriented params received by LiveView, Phoenix controllers, JSON APIs, and similar boundaries into typed Elixir values.

日本語版

Features

  • Converts string integers, booleans, dates, and datetimes into Elixir values
  • Generates a params struct, t/0, and parse/1 from a schema
  • Converts and validates arbitrary params with parse/1 and parse/2
  • Provides an integration that converts params immediately before LiveView callbacks
  • Supports nested maps and lists, defaults, custom types, and JSON Schema Draft 7 constraints

Installation

Add the dependency to mix.exs and fetch it:

def deps do
  [
    {:ex_params_schema, "~> 0.1.0"}
  ]
end

Update your dependencies with:

mix deps.get

Basic usage

Define a params module for each event or HTTP request input. field/3 declarations inside defschema generate a struct and parse/1.

defmodule MyAppWeb.Params.Update do
  use ExParamsSchema

  defschema do
    field :id, :integer, minimum: 1, error: :invalid_id
    field :value, :integer, minimum: 0, maximum: 255, error: :invalid_value
    field :enabled, :boolean, default: false
  end
end

Pass received params to parse/1 to obtain a converted and validated struct:

iex> MyAppWeb.Params.Update.parse(%{"id" => "2", "value" => "128", "enabled" => "on"})
{:ok, %MyAppWeb.Params.Update{id: 2, value: 128, enabled: true}}

iex> MyAppWeb.Params.Update.parse(%{"id" => "2", "value" => "256"})
{:error, :invalid_value}

When error: is omitted, failures return {:invalid_param, field_name}.

defmodule MyAppWeb.Params.CreateUser do
  use ExParamsSchema

  defschema do
    field :name, :string, min_length: 1
  end
end

iex> MyAppWeb.Params.CreateUser.parse(%{"name" => ""})
{:error, {:invalid_param, :name}}

For UIs that need to handle multiple field errors, use parse_detailed/1 to receive each error's path, validation keyword, and details.

iex> {:error, errors} = MyAppWeb.Params.Update.parse_detailed(%{"id" => "0", "value" => "256"})
iex> Enum.map(errors, &{&1.path, &1.keyword, &1.reason, &1.details})
[
  {["id"], :minimum, :invalid_id, %{expected: 1, exclusive?: false}},
  {["value"], :maximum, :invalid_value, %{expected: 255, exclusive?: false}}
]

Use ExParamsSchema.parse_detailed/2 with a compiled schema:

iex> schema = ExParamsSchema.compile!(%{id: {:integer, minimum: 1}, value: {:integer, minimum: 0, maximum: 255}})
iex> {:error, errors} = ExParamsSchema.parse_detailed(%{"id" => "0", "value" => "256"}, schema)
iex> Enum.map(errors, &{&1.path, &1.keyword, &1.reason})
[
  {["id"], :minimum, {:invalid_param, :id}},
  {["value"], :maximum, {:invalid_param, :value}}
]

Main options

OptionPurpose
source: "input-id"Maps an input key to a field name
default: valueUses a default only when the field is absent
optional: trueAllows an absent field and returns nil
nullable: trueAllows an explicit nil
strict: trueRejects input keys that are not in the schema
error: :reasonSets the reason returned on conversion or validation failure

See parsing semantics for the meanings of optional:, nullable:, and default:, including how empty strings are handled.

LiveView integration

ExParamsSchema.Handler converts params immediately before callbacks that have an @params_schema. On success, the callback receives a struct as params; on failure, the function specified by on_error: is called.

defmodule MyAppWeb.ExampleLive do
  use Phoenix.LiveView
  use ExParamsSchema.Handler, on_error: :handle_params_error

  @params_schema MyAppWeb.Params.Update
  def handle_event("update", params, socket) do
    {:noreply, assign(socket, id: params.id, value: params.value)}
  end

  defp handle_params_error(event, reason, socket) do
    {:noreply, put_flash(socket, :error, "#{event}: #{inspect(reason)}")}
  end
end

handle_params/3 and handle_info/2 are supported in addition to handle_event/3. An @params_schema applies only to the callback clause immediately following it. The first on_error: argument is the event name for handle_event/3, and the original params for the other callbacks. See the ExParamsSchema.Handler moduledoc for supported callbacks and error-handler behavior.

In Phoenix controllers and JSON APIs, call a params module's parse/1 inside the action.

def create(conn, params) do
  with {:ok, input} <- MyAppWeb.Params.Update.parse(params) do
    # input is a converted and validated struct
    json(conn, %{id: input.id, value: input.value})
  end
end

Without generating a struct

For shared validations and similar use cases, compile a map definition once and pass it to parse/2.

schema = ExParamsSchema.compile!(%{
  page: {:integer, minimum: 1, default: 1},
  query: {:string, optional: true}
})

iex> ExParamsSchema.parse(%{"page" => "2"}, schema)
{:ok, %{page: 2, query: nil}}

Documentation