| title | Structs vs Embedded Schemas in Elixir | ||||||
|---|---|---|---|---|---|---|---|
| date | 2025-04-08 | ||||||
| tags |
|
||||||
| draft | false | ||||||
| summary | A comprehensive guide to choosing between structs and embedded schemas in Elixir, with practical examples and best practices | ||||||
| images |
|
||||||
| type | Blog |
When working with Elixir, one of the most common misconceptions I've encountered is that Ecto is simply a database ORM (Object-Relational Mapping) tool. However, Elixir is not an object-oriented language, and this fundamental difference means we need to approach data modeling differently than in languages like Ruby or Python.
In Elixir, when developers need to group related data together, they often default to using structs, which is indeed the idiomatic way in Elixir. However, in a dynamic language like Elixir where field types aren't enforced at compile time, this can lead to code that's difficult to maintain and prone to runtime errors. This is where validation becomes crucial, and you might find yourself writing a lot of manual validation code.
Enter Ecto's embedded schemas - a powerful feature that provides a structured way to group and validate data without the need for database persistence. Whether you're working with JSON APIs, form submissions, or complex data transformations, embedded schemas offer a robust solution for data validation and manipulation.
In this post, we'll dive deep into both approaches, explore their use cases, and help you make informed decisions about when to use each.
Structs in Elixir are lightweight data structures that provide a way to create new types with predefined fields. Think of them as enhanced maps with compile-time guarantees and default values. They're perfect for simple data grouping when you don't need complex validation or database interactions.
- Compile-time Safety: Fields are guaranteed to exist at compile time, preventing accidental access to undefined fields
- Default Values: Easy initialization with predefined default values
- Required Fields: Use
@enforce_keysto ensure critical fields are always present - Pattern Matching: Seamless integration with Elixir's pattern matching capabilities
- Lightweight: No runtime overhead compared to more complex solutions
defmodule User do
@enforce_keys [:name]
defstruct [:name, age: 0, email: nil]
end
# Creating a user
user = %User{name: "Alice", age: 30, email: "alice@example.com"}
# Pattern matching
case user do
%User{age: age} when age >= 18 -> :adult
_ -> :minor
endEmbedded schemas are part of Ecto's powerful toolkit for data mapping and validation. They're particularly useful when you need to:
- Validate data from external sources (APIs, forms)
- Transform data between different formats
- Ensure data consistency without database persistence
- Work with complex nested data structures
- Built-in Validation: Comprehensive validation rules out of the box
- Type Casting: Automatic conversion between different data types
- Schema Composition: Create complex data structures by composing simpler ones
- Ecto Integration: Seamless work with Ecto's query interface when needed
- Flexible Persistence: Can be used with or without database storage
defmodule User do
use Ecto.Schema
import Ecto.Changeset
@fields [:name, :age, :email]
@required_fields [:name, :email]
embedded_schema do
field :name, :string
field :age, :integer, default: 0
field :email, :string
end
def new(attrs) do
%__MODULE__{}
|> cast(attrs, @fields)
|> validate_required(@required_fields)
|> validate_format(:email, ~r/^[^\s]+@[^\s]+$/)
|> validate_number(:age, greater_than_or_equal_to: 0)
end
end
# Usage example
{:ok, user} = User.new(%{
name: "Alice",
age: 30,
email: "alice@example.com"
})- You need simple data grouping without validation
- Performance is critical (structs have less overhead)
- You're working with internal data that you control
- You need compile-time guarantees
- You're building simple data transfer objects (DTOs)
- You need robust data validation
- You're working with external data sources
- You need to transform data between formats
- You want to leverage Ecto's powerful features
- You're building complex nested data structures
Let's see how both approaches work in a real-world scenario - handling API responses:
defmodule APIResponse do
defstruct [:status, :data, :message]
end
# Simple parsing
response = %APIResponse{
status: 200,
data: %{"user" => %{"name" => "Alice"}},
message: "Success"
}defmodule APIResponse do
use Ecto.Schema
import Ecto.Changeset
embedded_schema do
field :status, :integer
field :message, :string
embeds_one :data, Data do
embedded_schema do
embeds_one :user, User do
embedded_schema do
field :name, :string
field :email, :string
end
end
end
end
end
def parse(json) do
%__MODULE__{}
|> cast(json, [:status, :message, :data])
|> validate_required([:status])
|> validate_number(:status, greater_than_or_equal_to: 100, less_than: 600)
end
endWhile structs and embedded schemas might seem similar at first glance, they serve different purposes in Elixir applications. Structs are perfect for simple, internal data structures where you need compile-time guarantees and minimal overhead. Embedded schemas, on the other hand, provide a robust solution for data validation, transformation, and complex data structures.
The key to making the right choice lies in understanding your specific needs:
- Do you need validation? → Embedded Schema
- Is performance critical? → Struct
- Are you working with external data? → Embedded Schema
- Do you need simple data grouping? → Struct
Remember, Ecto is not just a database ORM - it's a powerful toolkit for data mapping and validation. By choosing the right tool for your specific use case, you can write more maintainable and reliable Elixir code.
