NimbleOptions vs Zoi Schemas in Jido: Key Differences and Migration Guide
Jido supports both NimbleOptions for backward compatibility and Zoi for modern compile-time type safety, with Zoi providing automatic struct enforcement and richer validation while NimbleOptions remains available for legacy modules.
Jido is an open-source Elixir framework for building autonomous agents that validates configuration and runtime data using two interchangeable schema systems. Understanding the difference between NimbleOptions and Zoi schemas in Jido is essential for developers maintaining legacy code or building new agent components, as each system offers distinct trade-offs between familiarity and advanced type safety.
Origins and Design Philosophy
NimbleOptions is a widely-used Elixir library (:nimble_options) that describes option maps with a simple DSL and returns a validated keyword list. In lib/jido/util.ex, Jido uses NimbleOptions for custom validators that predate the Zoi integration.
Zoi is a newer, Jido-specific schema DSL built on the Zoi library, offering richer type-spec generation, introspection, and default handling. As implemented in lib/jido/thread/entry.ex, Zoi schemas generate compile-time types and enforce struct fields automatically.
Schema Definition Syntax Comparison
The two systems use fundamentally different syntaxes for defining validation rules.
NimbleOptions Style
In lib/jido/util.ex, NimbleOptions schemas are defined as keyword lists with type specifications:
# lib/jido/util.ex – custom validator example
@spec compile_order(NimbleOptions.schema()) :: NimbleOptions.schema()
def compile_order(opts) do
NimbleOptions.validate!(opts, [
order: [type: :atom, default: :default],
enabled: [type: :boolean, default: true]
])
end
Zoi Style
In lib/jido/thread/entry.ex, Zoi uses composable primitives for schema construction:
# lib/jido/thread/entry.ex – a struct validated by Zoi
@schema Zoi.struct(
id: Zoi.string(description: "Unique entry identifier"),
seq: Zoi.integer(description: "Monotonic sequence within thread"),
at: Zoi.integer(description: "Timestamp (ms)"),
kind: Zoi.atom(description: "Entry type – open, any atom accepted"),
payload: Zoi.map(description: "Kind‑specific data") |> Zoi.default(%{})
)
@type t :: unquote(Zoi.type_spec(@schema))
@enforce_keys Zoi.Struct.enforce_keys(@schema)
defstruct Zoi.Struct.struct_fields(@schema)
Validation Behavior and Return Types
The systems differ in their runtime behavior and return structures.
NimbleOptions returns a keyword list or raises NimbleOptions.ValidationError. Error formatting is handled in lib/jido/error.ex.
Zoi returns a map (or the struct) or raises Zoi.ValidationError. It supports nested objects, refinements, optional fields, and custom validation functions directly in the DSL.
Compile-Time Guarantees and Type Safety
Zoi provides significant advantages for compile-time safety that NimbleOptions cannot match.
In lib/jido/sensor/spec.ex, Zoi generates type specifications and enforcement keys automatically:
# lib/jido/sensor/spec.ex
@schema Zoi.struct(
module: Zoi.atom(description: "Sensor module"),
name: Zoi.string(description: "Sensor name"),
description: Zoi.string() |> Zoi.optional(),
config: Zoi.map() |> Zoi.default(%{}),
schema: Zoi.any() |> Zoi.optional()
)
@type t :: unquote(Zoi.type_spec(@schema))
defstruct Zoi.Struct.struct_fields(@schema)
This generates @type t specifications and @enforce_keys automatically, whereas NimbleOptions offers no compile-time guarantees beyond runtime validation.
Usage Patterns in the Jido Codebase
Legacy NimbleOptions Usage
According to the documentation in lib/jido/agent.ex, NimbleOptions appears in "familiar, legacy" modules and public APIs that were written before Zoi existed. It remains supported for backward compatibility.
Modern Zoi Usage
Zoi is preferred for internal structs including threads, sensors, plugins, and agents. In lib/jido/agent/state.ex, the runtime validation accepts either schema type:
# lib/jido/agent/state.ex – validates against NimbleOptions **or** Zoi
@doc false
def validate(state, schema) do
case schema do
%NimbleOptions{} -> NimbleOptions.validate!(state, schema)
%Zoi{} -> Zoi.parse(schema, state)
end
end
Migration Considerations
When choosing between the two systems:
- Use NimbleOptions when maintaining legacy modules or when integrating with external libraries that expect keyword-list options.
- Use Zoi for new struct definitions requiring compile-time type safety, automatic enforcement of required fields, and richer validation logic.
Both approaches are first-class in Jido; developers can migrate legacy code to Zoi gradually without breaking existing functionality.
Summary
- NimbleOptions provides familiar, runtime-only validation using keyword-list schemas, primarily used in legacy Jido modules.
- Zoi offers modern, composable schemas with compile-time type generation, automatic struct enforcement, and richer validation capabilities.
- Runtime flexibility:
lib/jido/agent/state.exaccepts both schema types, allowing gradual migration. - New development: Prefer Zoi for structs, sensors, plugins, and agents to leverage compile-time guarantees.
Frequently Asked Questions
What is the primary advantage of Zoi over NimbleOptions in Jido?
Zoi generates compile-time type specifications (@type t) and automatically enforces struct keys through @enforce_keys, whereas NimbleOptions only provides runtime validation. This allows Jido to catch type errors at compilation and provides better IDE support for struct fields.
Can I mix NimbleOptions and Zoi schemas in the same Jido application?
Yes. According to lib/jido/agent/state.ex, Jido's validation layer accepts both schema types at runtime and dispatches to the appropriate validator. This allows gradual migration from NimbleOptions to Zoi without breaking existing modules.
Where does Jido use NimbleOptions instead of Zoi?
NimbleOptions appears primarily in legacy modules and public APIs that predate Zoi's introduction, as noted in lib/jido/agent.ex. You will also find it in lib/jido/util.ex for custom validators and lib/jido/error.ex for error formatting.
How do I define default values in Zoi compared to NimbleOptions?
In NimbleOptions, defaults are specified inline within the schema definition using [key: [type: :atom, default: :value]]. In Zoi, defaults are applied using the pipe operator with Zoi.default/1, such as Zoi.map() |> Zoi.default(%{}), allowing for composable schema construction.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →