Structify
Structify is an Elixir library that provides powerful functionality to coerce between maps, structs, and lists recursively.
Installation
Add structify to your list of dependencies in mix.exs:
def deps do
[
{:structify, "~> 0.1.0"}
]
endDocumentation
The docs can be found at https://hexdocs.pm/structify.
Features
Conversion and cleanup approaches
Structify.Coerce - Lossy conversions with simple return values:
- Direct results
- Optimized for performance when error handling isn't critical
- Ideal for trusted data transformations
Structify.Convert - Lossless conversions with explicit result tuples:
-
Returns
{:ok, result},{:error, reason}, or{:no_change, original} - Comprehensive error domains for debugging
- Ideal for untrusted input or when detailed error information is needed
Structify.Destruct - Deep cleanup and meta removal:
-
Removes internal/meta keys such as
:__struct__and:__meta__from maps and structs -
Recursively processes lists, maps, and structs; filters
nilentries from lists - Preserves well-known date/time structs (Date, Time, NaiveDateTime, DateTime)
- Useful for preparing data for JSON serialization or for callers that expect plain maps
Core Capabilities
- Type Conversions: Maps ↔ Structs, Lists of Maps ↔ Lists of Structs
- String Key Coercion: Automatic conversion of string keys to atoms when targeting structs
- Nested Transformations: Deep recursive processing with configurable rules
- Module Shorthand Syntax:
field: MyStructequivalent tofield: [__to__: MyStruct] - Well-known Type Preservation: Date, Time, NaiveDateTime, DateTime pass through unchanged
- List Processing: Automatic filtering of nil values
- Pass-through Behavior: Transform nested fields while preserving intermediate types
Quick Start
# Define your structs
defmodule User do
defstruct [:name, :email, :age]
end
defmodule Company do
defstruct [:name, :users, :address]
end
defmodule Address do
defstruct [:street, :city, :country]
end
# Basic conversions
iex> input = %{name: "Alice", email: "alice@example.com", age: 30}
iex> Structify.coerce(input, User)
%User{name: "Alice", email: "alice@example.com", age: 30}
iex> input = %{name: "Alice", email: "alice@example.com", age: 30}
iex> Structify.convert(input, User)
{:ok, %User{name: "Alice", email: "alice@example.com", age: 30}}String Key Coercion
Structify automatically handles string keys when converting to structs:
# String keys are converted to atoms when targeting structs
iex> input = %{"name" => "Alice", "email" => "alice@example.com", "age" => 30}
iex> Structify.coerce(input, User)
%User{name: "Alice", email: "alice@example.com", age: 30}
# String keys are preserved when targeting maps
iex> input = %{"name" => "Alice", "email" => "alice@example.com", "age" => 30}
iex> Structify.coerce(input, nil)
%{"name" => "Alice", "email" => "alice@example.com", "age" => 30}
# Mixed key types work gracefully
iex> input = %{:name => "Alice", "email" => "alice@example.com", "age" => 30}
iex> Structify.coerce(input, User)
%User{name: "Alice", email: "alice@example.com", age: 30}Nested Transformations
Full Syntax with :__to__ Key
input = %{
name: "TechCorp",
users: [
%{name: "Alice", email: "alice@example.com", age: 30},
%{name: "Bob", email: "bob@example.com", age: 25}
],
address: %{street: "123 Main St", city: "Anytown", country: "USA"}
}
nested = [
users: [__to__: User], # Convert each user map to User struct
address: [__to__: Address] # Convert address map to Address struct
]
Structify.coerce(input, Company, nested)
# => %Company{
# name: "TechCorp",
# users: [%User{...}, %User{...}],
# address: %Address{street: "123 Main St", city: "Anytown", country: "USA"}
# }Module Shorthand Syntax
# Shorthand syntax - much cleaner!
nested = [
users: User, # Equivalent to [__to__: User]
address: Address # Equivalent to [__to__: Address]
]
Structify.coerce(input, Company, nested)
# Same result as aboveDeep Nesting
input = %{
companies: [
%{
name: "TechCorp",
ceo: %{name: "CEO Alice", email: "ceo@techcorp.com", age: 45},
address: %{street: "456 Business Ave", city: "Metropolis", country: "USA"}
}
]
}
nested = [
companies: [
ceo: User, # Shorthand for CEO conversion
address: Address # Shorthand for address conversion
]
]
result = Structify.coerce(input, nil, nested)
# => %{
# companies: [
# %{
# name: "TechCorp",
# ceo: %User{name: "CEO Alice", email: "ceo@techcorp.com", age: 45},
# address: %Address{street: "456 Business Ave", city: "Metropolis", country: "USA"}
# }
# ]
# }Error Handling with Convert
# Convert provides detailed error information
iex> Structify.convert(%{invalid: "data"}, NonExistentModule)
{:error, {NonExistentModule, :not_struct}}
# No-change optimization
iex> user = %User{name: "Alice", email: "alice@example.com", age: 30}
iex> Structify.convert(user, User) # Same type, no nested rules
{:no_change, %User{name: "Alice", email: "alice@example.com", age: 30}}
# Use convert! to raise on error
iex> Structify.convert!(%{name: "Alice"}, User)
%User{name: "Alice", email: nil, age: nil}
iex> Structify.convert!(%{invalid: "data"}, NonExistentModule)
** (ArgumentError) NonExistentModule is not a structConfiguration Options
The :__to__ Key - Four Use Cases
- Convert to struct:
nested = [field: [__to__: MyStruct]] - Convert to map:
nested = [field: [__to__: nil]] - Pass-through with nested rules:
nested = [field: [nested_field: [__to__: MyStruct]]] - Module shorthand:
nested = [field: MyStruct](equivalent to case 1)
Mixed Syntax
nested = [
user: User, # Shorthand
address: [__to__: Address], # Full syntax
metadata: [ # Pass-through with nested rules
created_by: User,
updated_by: User
]
]List Processing
# Lists are processed element-wise, with nil filtering
input = [
%{name: "Alice", age: 30},
nil, # This will be filtered out
%{name: "Bob", age: 25}
]
Structify.coerce(input, User)
# => [%User{name: "Alice", age: 30}, %User{name: "Bob", age: 25}]Well-known Types
Date, Time, NaiveDateTime, and DateTime structs are preserved unchanged:
iex> date = ~D[2023-09-18]
iex> Structify.coerce(date, User)
~D[2023-09-18] # Returned unchanged
iex> date = ~D[2023-09-18]
iex> Structify.convert(date, nil)
{:no_change, ~D[2023-09-18]}Destruct (deep cleanup)
Structify.destruct/1 deeply cleans data structures by removing internal/meta keys and
recursively processing lists, maps, and structs while preserving common date/time structs.
Behavior and rules
-
Lists: each non-nil element is destructed recursively;
nilelements are filtered out. -
Maps and structs: all values are destructed recursively. Meta keys such as
:__struct__and:__meta__are removed from maps/structs during the cleanup. - Well-known structs (Date, Time, NaiveDateTime, DateTime) are returned unchanged.
- Other primitive values (numbers, strings, atoms, etc.) are returned as-is.
This is useful when you want to prepare data for JSON serialization or for returning plain maps from code that may produce structs with attached metadata.
Examples
iex> Structify.destruct(Map.put(%User{name: "Alice", email: "alice@example.com"}, :__meta__, :foo))
%{name: "Alice", email: "alice@example.com", age: nil}
iex> Structify.destruct([%User{name: "Alice"}, nil, 1])
[%{name: "Alice", email: nil, age: nil}, 1]
iex> Structify.destruct(%{"foo" => 1, :bar => 2, __meta__: :skip})
%{"foo" => 1, :bar => 2}
iex> Structify.destruct(~D[2020-01-01])
~D[2020-01-01]
iex> Structify.destruct(%{user: Map.put(%User{name: "Alice"}, :__meta__, :foo)})
%{user: %{name: "Alice", email: nil, age: nil}}
iex> Structify.destruct(nil)
nilAPI
@spec destruct(term()) :: term()Removes meta keys and returns a deeply cleaned structure suitable for serialization or for consumers that expect plain maps/lists rather than structs with metadata.
API Reference
Structify.coerce/3
@spec coerce(term(), module() | nil, keyword() | map()) :: term()Performs lossy conversion, returning the result directly.
Structify.convert/3
@spec convert(term(), module() | nil, keyword() | map()) ::
{:ok, term()} | {:error, {Exception.t(), module()}} | {:no_change, term()}Performs lossless conversion with explicit result tuples.
Structify.convert!/3
@spec convert!(term(), module() | nil, keyword() | map()) :: term()
Like convert/3 but raises the appropriate exception on error, returns result directly on success.