phcurado

phcurado

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

First 4 of 4 Posts! Switch mode

phcurado

phcurado

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.

phcurado

phcurado

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

phcurado

phcurado

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"}}
phcurado

phcurado

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

Where Next?

Trending in Announcing Top

bluzky
You may know https://ui.shadcn.com/, a UI component library for React. I really love it’s design style and components. I’ve built some co...
385 14863 120
New
JesseHerrick
Hey, I’m Jesse and I’m the main contributor behind Dexter, a full-featured, lightning-fast Elixir LSP optimized for large codebases. It s...
New
shahryarjb
The Chelekom project is a library of Phoenix and LiveView components generated via Mix tasks to fit developer needs seamlessly. One of i...
New
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
Damirados
Hello everyone. After busy few months I am happy to announce v0.1.0 of Emerge &amp; Solve. They are GUI (Emerge) and State management (S...
New
ausimian
Emily is an Elixir library that runs Nx computations on Apple’s MLX. Install it as the default Nx backend and Nx, defn, Axon, Nx.Serving,...
New
zachdaniel
Introducing AshStorage! Attachment and file management that slots directly into your resources :smiling_face_with_sunglasses: I had hope...
New

Other Trending Topics Top

type1fool
I just stumbled on a newly redesigned elixir-lang.org. :tada: It looks like @Software_Mansion did the work, and I think it is generally a...
New
akoutmos
@hugobarauna and I (Alex Koutmos) have been hard at work on writing a book on Nerves that takes you from simply blinking LEDs to building...
New
juhalehtonen
There has been a thread to discuss the Stack Overflow Developer Survey on this forum every year since 2018, so here’s yet another one for...
New
bjorng
We want to introduce a new native datatype to Erlang: native records. Although replacing all tuple records with native records is not our...
New
spammy
I’m looking to build a personal workflow to quickly deploy web applications written in elixir/phoenix, for local consumption (ie not on t...
New
yureehuh
Introduction Founded in 2017 by landscape ecologist and fire mitigation expert Harry Statter, Frontline developed the first fully integra...
New

We're in Beta

About us Mission Statement