Parsing and Validating Data in Elixir

zoedsoupe

Zoey de Souza Pessanha

Posted on June 18, 2024

Parsing and Validating Data in Elixir

In the enchanting world of Elixir programming, data validation is a quest every developer embarks on. It's a journey through the land of schemas, types, and constraints, ensuring data integrity and correctness. Today, we'll explore four powerful artifacts: Ecto, Norm, Drops, and Peri. Each of these tools offers unique powers for taming your data. We'll delve into their strengths, use cases, and compare them to help you choose the right one for your quest.

The Quest: Parse, Don't Validate

Before we embark on our journey, let's discuss a guiding principle in functional programming: Parse, Don't Validate. This pattern emphasizes transforming data into a well-defined structure as early as possible. By doing so, you avoid scattered, ad-hoc validation throughout your codebase, leading to clearer, more maintainable code. It's like casting a spell to organize the chaos of raw data into a neat, structured form.

The Artifacts

1. Ecto

Ecto is a robust toolkit primarily designed for interacting with databases. However, it also offers powerful capabilities for embedded schemas and schemaless changesets, making it versatile for data validation.

Embedded Schemas

Ecto allows defining schemas that don't map to a database table, ideal for validating nested data structures.

defmodule User do
  use Ecto.Schema

  embedded_schema do
    field :name, :string
    field :email, :string
  end
end

def changeset(data) do
  %User{}
  |> Ecto.Changeset.cast(data, [:name, :email])
  |> Ecto.Changeset.validate_required([:name, :email])
end
Enter fullscreen mode Exit fullscreen mode

Schemaless Changesets

For dynamic data, Ecto provides schemaless changesets, offering flexibility at the cost of increased complexity.

def changeset(data) do
  Ecto.Changeset.cast({%{}, %{name: :string, email: :string}}, data, [:name, :email])
  |> Ecto.Changeset.validate_required([:name, :email])
end
Enter fullscreen mode Exit fullscreen mode

2. Norm

Norm focuses on defining and conforming to data structures with custom predicates, offering a clean syntax and powerful validation.

defmodule User do
  import Norm

  defschema do
    schema(%{
      name: spec(is_binary()),
      age: spec(is_integer() and &(&1 > 18))
    })
  end
end

Norm.conform(%{name: "Jane", age: 25}, User.schema())
# => {:ok, %{name: "Jane", age: 25}}
Enter fullscreen mode Exit fullscreen mode

3. Drops

Drops is a newer library that provides a rich set of tools for defining and validating schemas, leveraging Elixir's type system.

defmodule UserContract do
  use Drops.Contract

  schema do
    %{
      required(:name) => string(:filled?),
      required(:age) => integer(gt?: 18)
    }
  end
end

UserContract.conform(%{name: "Jane", age: 21})
# => {:ok, %{name: "Jane", age: 21}}
Enter fullscreen mode Exit fullscreen mode

4. Peri

Peri is inspired by Clojure's Plumatic Schema, focusing on validating raw maps with nested schemas and optional fields. It's designed to be powerful yet simple, embracing the "Parse, Don't Validate" pattern.

defmodule MySchemas do
  import Peri

  defschema :user, %{
    name: :string,
    age: :integer,
    email: {:required, :string},
    role: {:enum, [:admin, :user]}
  }

  defschema :profile, %{
    user: {:custom, &MySchemas.user/1},
    bio: :string
  }
end

MySchemas.user(%{name: "John", age: 30, email: "john@example.com", role: :admin})
# => {:ok, %{name: "John", age: 30, email: "john@example.com", role: :admin}}

MySchemas.user(%{name: "John", age: "thirty", email: "john@example.com"})
# => {:error, [%Peri.Error{path: [:age], message: "expected integer received \"thirty\""}]}
Enter fullscreen mode Exit fullscreen mode

Conditional and Composable Types in Peri

Peri shines with its support for conditional and composable types, making it a powerful tool for complex validation scenarios.

defmodule AdvancedSchemas do
  import Peri

  defschema :user, %{
    name: :string,
    age: {:cond, &(&1 >= 18), :integer, :nil},
    email: {:either, {:string, :nil}},
    preferences: {:list, {:oneof, [:string, :atom]}}
  }
end

AdvancedSchemas.user(%{name: "Alice", age: 25, email: nil, preferences: ["coding", :reading]})
# => {:ok, %{name: "Alice", age: 25, email: nil, preferences: ["coding", :reading]}}

AdvancedSchemas.user(%{name: "Bob", age: 17})
# => {:ok, %{name: "Bob", age: 17, email: nil, preferences: nil}}
Enter fullscreen mode Exit fullscreen mode

Conclusion

Each of these tools offers unique advantages and caters to different needs:

  • Ecto is great for data associated with databases but can handle schemaless and embedded data structures too.
  • Norm provides a clean and powerful way to define and validate data structures.
  • Drops leverages Elixir's type system and offers rich schema definitions and validations.
  • Peri emphasizes simplicity and power, supporting complex types and conditional validations.

By understanding the strengths and weaknesses of each, you can choose the right tool for your data validation needs in Elixir. Happy coding, fellow sorcerers of Elixiria!

References

Feel free to dive into the source code and contribute to these projects to make Elixiria an even more magical place!

💖 💪 🙅 🚩
zoedsoupe
Zoey de Souza Pessanha

Posted on June 18, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related