# NimbleOptions vs Zoi Schemas in Jido: Key Differences and Migration Guide

> Discover NimbleOptions vs Zoi schemas in Jido. Learn their key differences, Zoi's type safety, and how to migrate from NimbleOptions to Zoi for improved validation.

- Repository: [agentjido/jido](https://github.com/agentjido/jido)
- Tags: migration-guide
- Published: 2026-03-09

---

**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`](https://github.com/agentjido/jido/blob/main/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`](https://github.com/agentjido/jido/blob/main/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`](https://github.com/agentjido/jido/blob/main/lib/jido/util.ex), NimbleOptions schemas are defined as keyword lists with type specifications:

```elixir

# 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`](https://github.com/agentjido/jido/blob/main/lib/jido/thread/entry.ex), Zoi uses composable primitives for schema construction:

```elixir

# 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`](https://github.com/agentjido/jido/blob/main/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`](https://github.com/agentjido/jido/blob/main/lib/jido/sensor/spec.ex), Zoi generates type specifications and enforcement keys automatically:

```elixir

# 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`](https://github.com/agentjido/jido/blob/main/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`](https://github.com/agentjido/jido/blob/main/lib/jido/agent/state.ex), the runtime validation accepts either schema type:

```elixir

# 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.ex`](https://github.com/agentjido/jido/blob/main/lib/jido/agent/state.ex) accepts 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`](https://github.com/agentjido/jido/blob/main/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`](https://github.com/agentjido/jido/blob/main/lib/jido/agent.ex). You will also find it in [`lib/jido/util.ex`](https://github.com/agentjido/jido/blob/main/lib/jido/util.ex) for custom validators and [`lib/jido/error.ex`](https://github.com/agentjido/jido/blob/main/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.