Parameter - Simplified definition, serialization, deserialization and validation of params

Parameter is a library for dealing with parameter serialization, deserialization and validation following a structure very similar to Ecto schema.

The main motivation for creating this is to have more options when loading external parameters and convert it to plain elixir maps or structure. It can load the data to a map or struct, transform external data keys to elixir keys, perform validations and dump the data.

The idea is to create a schema that models the data you need to load on your elixir application. The “key” field on the schema is useful to map keys that comes with a different format of your schema keys. For example when external data have camelCase keys and you want to use snake_case.

As an example imagine you are making and http request to a rest API to fetch a single user, this is how you can model the User schema:

defmodule User do
  use Parameter.Schema
  alias Parameter.Validators

  param do
    field :first_name, :string, key: "firstName", required: true
    field :last_name, :string, key: "lastName"
    field :email, :string, validator: &Validators.email(&1)
    has_one :address, Address do
      field :city, :string, required: true
      field :street, :string
      field :number, :integer
    end
  end
end

And this is how the external data could look like:

params = %{
  "firstName" => "John",
  "lastName" => "Doe",
  "email" => "john@email.com",
  "address" => %{"city" => "New York", "street" => "Broadway"}
}

Now this params can be deserialized using the load/2 function

iex> Parameter.load(User, params)
{:ok, %{
  first_name: "John",
  last_name: "Doe",
  email: "john@email.com",
  address: %{city: "New York", street: "Broadway"}
}}

the camelCase keys were converted to snake_case keys as per schema definition. If the external data already have the same key type as the schema definition this param can be ignored.
It also possible to load the schema as structure:

iex> Parameter.load(User, params, struct: true)
{:ok, %User{
  first_name: "John",
  last_name: "Doe",
  email: "john@email.com",
  address: %User.Address{city: "New York", street: "Broadway", number: nil}
}}

Also can be used on Phoenix APIs that needs to validate parameters before sending it to the context module.

defmodule MyProjectWeb.UserController do
  use MyProjectWeb, :controller
  import Parameter.Schema

  alias MyProject.Users

  param UserParams do
    field :first_name, :string, required: true
    field :last_name, :string, required: true
  end

  def create(conn, params) do
    with {:ok, user_params} <- Parameter.load(__MODULE__.UserParams, params),
         {:ok, user} <- Users.create_user(user_params) do
      render(conn, "user.json", %{user: user})
    end
  end
end

There are plenty of more options available as built in validators (or possibility for creating custom ones), custom types, error handling, unknown options that comes from external data and serializing (dump) the data.

Check it out

11 Likes

Parameter 0.5.0 version released.

New types introduced to offer more options when loading/dumping data:

0.5.0 - Enum type

Enum type is now supported. This type is useful for translating external data enum definitions to internal values.

Example of having a UserParam schema:

defmodule MyApp.UserParam do
  use Parameter.Schema

  enum Status do
    value "active", as: :active
    value "pendingRequest", as: :pending_request
  end

  param do
    field :first_name, :string, key: "firstName"
    field :status, MyApp.UserParam.Status
  end
end

We can load or dump the data with the enum value, automatically converted to it’s definition:

iex> Parameter.load(MyApp.UserParam, %{"firstName" => "John", "status" => "active"})
{:ok, %{first_name: "John", status: :active}}
# or dumping data the atom value will be converted:
...> Parameter.dump(MyApp.UserParam, %{first_name: "John", status: :pending_request})
{:ok, %{"firstName" => "John", "status" => "pendingRequest"}}

It can work also with integer values since some APIs use numbers for enum representation:

enum Status do
  value 1, as: :active
  value 2, as: :pending_request
end

and load/dump will give the following results:

iex> Parameter.load(MyApp.UserParam, %{"firstName" => "John", "status" => 1})
{:ok, %{first_name: "John", status: :active}}
# or dumping data the atom value will be converted:
...> Parameter.dump(MyApp.UserParam, %{first_name: "John", status: :pending_request})
{:ok, %{"firstName" => "John", "status" => 2}}

check all the available options for declaring and using enums here.

0.4.0 - Decimal type

decimal type supported, it’s very simple to add on your schema.
For example having a Payment schema with amount field that should be parsed as decimal:

defmodule MyApp.Payment do
  use Parameter.Schema

  param do
    field :amount, :decimal
  end
end

load the data using the schema definition:

iex> Parameter.load(Payment, %{"amount" => 10.50})
{:ok, %{amount: #Decimal<10.5>}}

And add the optional decimal dependency to be able to work with this type.

2 Likes

Parameter 0.6.1 version released.

  • Supports many flag on load/3 and dump/3 options to load and dump lists.
    Reusing the same user example in the first post:
params = %{"firstName" => "John", "lastName" => "Doe"}
Parameter.load(MyApp.UserParam, [params, params], many: true)
{:ok, 
[
  %{first_name: "John", last_name: "Doe"}, 
  %{first_name: "John", last_name: "Doe"}
]}
  • Improved error handler when dealing with list fields:
params = %{"lastName" => "Doe"}
Parameter.load(MyApp .UserParam, [params, params], many: true)
# index shown on errors
{:error,
%{
  0 => %{first_name: "is required"},
  1 => %{first_name: "is required"}
}}
  • New options for fields in the schema:
    • load_default: default value when none is passed when loading a field.
    • dump_default: default value when none is passed when dumping a field.
defmodule MyApp.UserParam do
  use Parameter.Schema

  param do
    field :name, :string, load_default: "loadName", dump_default: "dumpName"
  end
end

Parameter.load(MyApp.UserParam, %{})
{:ok, %{name: "loadName"}}

Parameter.dump(MyApp.UserParam, %{})
{:ok, %{"name" => "dumpName"}}
  • Enum api change to use key option instead of as to be more semantic to the field param definition (the older version still works with a deprecation warning).
defmodule MyApp.UserParam do
  use Parameter.Schema

  enum Status do
    value :user_online, key: "userOnline"
    value :user_offline, key: "userOffline"
  end

  param do
    field :first_name, :string, key: "firstName"
    field :status, MyApp.UserParam.Status
  end
end

Parameter.load(MyApp.UserParam, %{"firstName" => "John", "status" => "userOnline"})
{:ok, %{first_name: "John", status: :user_online}}
# or dump
Parameter.dump(MyApp.UserParam, %{first_name: "John", status: :user_offline})
{:ok, %{"firstName" => "John", "status" => "userOffline"}}

Docs for the new version here

Parameter 0.8.0 version released :tada:

Now supports creating schemas without relying on any macro API.

Example on how it works:

## Create your runtime schema
schema = %{
  first_name: [key: "firstName", type: :string, required: true],
  address: [required: true, type: {:has_one, %{street: [type: :string, required: true]}}],
  phones: [type: {:has_many, %{number: [type: :string, required: true], country_code: [key: "countryCode", type: :integer, required: true]}}]
}
# Compile the schema (it also performs validation)
compiled_schema = Parameter.Schema.compile!(schema)

# Now suppose these are the parameters coming from an external data
params = %{
  "firstName" => "John", 
  "address" => %{"street" => "some street"},
  "phones" => [
     %{"number" => "123456789", "countryCode" => 123}
  ]
}

# Load the parameters against the schema
Parameter.load(compiled_schema, params)
{:ok,
 %{
   address: %{street: "some street"},
   first_name: "John",
   phones: [%{country_code: 123, number: "123456789"}]
 }}

It’s also possible to have the same compile time guarantees from the Macro API by using module attributes:

defmodule UserParams do
  alias Parameter.Schema

  @schema %{
    first_name: [key: "firstName", type: :string, required: true],
    last_name: [key: "lastName", type: :string, required: true]
  } |> Schema.compile!()

  def load(params) do
    Parameter.load(@schema, params)
  end
end

params = %{"firstName" => "John", "lastName" => "Doe"}
UserParams.load(params)
{:ok, %{first_name: "John", last_name: "Doe"}}
4 Likes

Parameter 0.11.0 version released :tada:

Some small improvements:

  • Support for on_load/2 and on_dump/2 functions in field definition which gives the possibility to customize load and dump behaviour.
  • ignore_nil and ignore_empty option added for customizing the behaviour when receiving nil or empty "" fields.
  • Supports for deep nested types when using map or array composite types.
  • Test coverage is now 100% :tada:

Changelog here

2 Likes