100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Programming

Structs in Elixir

Learn how Elixir structs extend maps with defined fields, compile-time checks, default values, and protocol-based behavior.

Core ConceptsIntermediate8 min readJul 10, 2026
Analogies

What Is a Struct?

A struct is declared with defstruct inside a defmodule block and defines a fixed, named set of fields, unlike a plain map, which accepts any arbitrary key. Internally, a struct is just a map carrying a special __struct__ key whose value is the defining module's name, which is how Elixir tags a value as belonging to that specific struct type.

🏏

Cricket analogy: A struct is like an official ICC scorecard template with fixed fields -- batter, runs, balls faced, fours, sixes -- that every entry must follow, unlike a scribbled press-box note where anyone could jot down any random field like 'lucky charm' without structure.

Defining and Creating Structs

Inside a module, defstruct name: nil, age: 0, active: true declares the struct's fields along with default values used whenever a field is omitted at creation time; %User{name: "Ada", age: 30} builds one, leaving active at its default of true. Referencing a field that wasn't declared in defstruct -- whether creating or updating -- is caught as a compile-time error, which catches typos that a plain map would silently accept as a brand-new key.

🏏

Cricket analogy: %Player{name: "Kohli", role: :batter} is like filling an official BCCI registration form -- leave batting_style blank and it defaults per the template, but write an unrecognized field like lucky_charm and the registration is rejected outright before the season starts.

elixir
defmodule User do
  @enforce_keys [:name]
  defstruct name: nil, age: 0, active: true
end

user = %User{name: "Ada", age: 30}
older_user = %{user | age: 31}

%User{name: name} = older_user
name  # "Ada"

defimpl String.Chars, for: User do
  def to_string(u), do: "#{u.name} (#{u.age})"
end

IO.puts(older_user)  # "Ada (31)"

# %User{naem: "typo"} raises a compile-time error:
# ** (KeyError) key :naem not found in: %User{...}

defstruct alone lets every field default to nil (or whatever you specify) if the caller omits it, which can hide a genuinely required field as a silent nil. Add @enforce_keys [:name] above defstruct to make Elixir raise ArgumentError at struct-creation time if :name isn't supplied, catching missing required data immediately instead of letting a nil propagate silently.

Updating and Pattern Matching Structs

The update syntax %{struct | field: value} returns a new struct with the given field(s) changed and every other field preserved, without needing to restate the whole struct. Pattern matching with %User{name: n} = value checks two things at once: that value is a map shaped like a User, and that its __struct__ tag specifically equals User -- so it won't match an %Admin{} struct even if Admin happens to also have a name field.

🏏

Cricket analogy: %{player | runs: 50} is like updating just the runs field on an existing scorecard without retyping the whole card, and matching %Batter{name: n} = entry only succeeds if the card is truly tagged as a Batter record, not a Bowler record sharing a similarly shaped map.

Structs and Protocols

Structs don't automatically gain polymorphic behavior just because they're maps -- functions dispatched through a protocol, like Enum.map/2 (via the Enumerable protocol) or to_string/1 (via String.Chars), require an explicit defimpl SomeProtocol, for: YourStruct before they'll work on a custom struct. This keeps a struct's behavior opt-in and explicit rather than silently inherited from whatever protocol implementations happen to exist for maps in general.

🏏

Cricket analogy: A struct without a protocol implementation is like a new overseas signing who hasn't learned the team's fielding signals yet -- you must explicitly coach them (defimpl) before they join the squad's standard communication, just as a struct needs Enumerable before Enum functions work on it.

Because a struct is fundamentally a map with a __struct__ key, ordinary Map functions like Map.get/2, Map.keys/1, and Map.fetch/2 work on a struct directly without any protocol implementation -- it's specifically the protocol-dispatched functions like those in Enum and String.Chars that require an explicit defimpl.

  • defstruct inside a module defines a struct's fixed set of fields and their default values.
  • A struct is internally a map with a special __struct__ key identifying which module defined it.
  • Creating or updating a struct with an unrecognized field name is a compile-time error, unlike plain maps.
  • @enforce_keys makes Elixir raise ArgumentError at creation time if a required field is missing.
  • The update syntax %{struct | field: value} changes specific fields without retyping the whole struct.
  • Pattern matching %ModuleName{} = value only succeeds if value is tagged as that exact struct.
  • Structs need an explicit defimpl to support protocol-dispatched behavior like Enum functions or to_string, though plain Map functions work on them directly.

Practice what you learned

Was this page helpful?

Topics covered

#Programming#ElixirStudyNotes#StructsInElixir#Structs#Elixir#Struct#Defining#StudyNotes#SkillVeris#ExamPrep