A repository of tips and tricks (in both English and French) curated by Mirego’s engineering team.
  • ecto
  • elixir

Using Ecto.Changeset to parse JSON params map

In my previous post, I wrote:

I recently had to deal with camelCase-formated JSON while working on REST payloads. I am using an embedded schema and Ecto.Changeset to standardize the params map into an internal struct. However, I prefer to use the language snake_case convention for fields.

This is a perfect example of how amazing Ecto.Schema and Ecto.Changeset are; not only to interact with a data store!

Let’s say we have the following JSON body to handle:

{
  "email": "john.dow@email.com",
  "firstName": "John",
  "lastName": "Doe",
  "dateOfBirth": null
}

Plug.Parsers.JSON will automatically convert the content into a map. And asumming the Plug from my previous post is in place, we have the following map with snake_case keys:

%{
  "email" => "john.dow@email.com",
  "first_name" => "John",
  "last_name" => "Doe",
  "date_of_birth" => null
}

However, my context is expecting a struct!

defmodule Foo.Accounts.UserRequest do
  defstruct email: nil, first_name: nil, last_name: nil, date_of_birth: nil
end

Thus I have to manually convert the "generic" map to an internal struct…

iex(1)> params = %{"email" => "john.doe@email.com", "first_name" => "John", "last_name" => "Doe"}
iex(2)> %UserRequest{
...(2)>   email: params["email"],
...(2)>   first_name: params["first_name"],
...(2)>   last_name: params["last_name"],
...(2)>   date_of_birth: params["date_of_birth"]
...(2)> }
%Foo.Accounts.UserRequest{
  date_of_birth: nil,
  email: "john.doe@email.com",
  first_name: "John",
  last_name: "Doe"
}

Enters Ecto.Schema and Ecto.Changeset

Let’s rewrite the struct using embedded_schema/1 and leverage cast/3 to handle the conversion!

defmodule Foo.Accounts.UserRequest do
  use Ecto.Schema

  import Ecto.Changeset

  @primary_key false
  embedded_schema do
    field :email, :string
    field :first_name, :string
    field :last_name, :string
    field :date_of_birth, :date
  end

  def parse(params) do
    %__MODULE__{}
    |> cast(params, ~w(email first_name last_name date_of_birth)a)
    |> validate_required(~w(email first_name last_name)a)
    |> apply_changes()
  end
end

Which gives us a solid internal structure, validation and error handling if needed!

Parsing the params map into a struct becomes as simple as:

iex(3)> UserRequest.parse(params)
%Foo.Accounts.UserRequest{
  date_of_birth: nil,
  email: "john.doe@email.com",
  first_name: "John",
  last_name: "Doe"
}

Happy coding 🛠