ZoiForge

ZoiForge is inspired by Schema-Driven Development: define your data in JSON Schema, derive the code from it. ZoiForge handles the Elixir side — typed modules, validators, and docs generated from your schema files.

Generate typed, validated Elixir modules from JSON Schema files.

ZoiForge scans a directory of *.schema.json files and writes Elixir modules backed by Zoi. Each module gets compile-time @type t, a runtime parse/1 validator, and docs derived from the schema. You define the shape once in JSON Schema; ZoiForge generates the rest.

Why ZoiForge

Installation

Add zoi_forge to your dependencies:

def deps do
[
{:zoi_forge, "~> 0.1.0"}
]
end

Docs: hexdocs.pm/zoi_forge

Quick start

With your own schema files

  1. Add schema files under priv/schemas/ (any nested layout; files must end in .schema.json):
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"name": { "type": "string" }
},
"required": ["name"]
}

Save as priv/schemas/user.schema.json.

  1. Configure defaults (optional — these match ZoiForge's built-in defaults):
# config/config.exs
config :zoi_forge,
source_dir: "priv/schemas",
output_dir: "lib/schemas"
  1. Generate modules:
mix zoi_forge.gen

By default, module names use your Mix project app name as a prefix and mirror the schema path. For an app named :my_app and priv/schemas/user.schema.json, you get MyApp.Schemas.User in lib/schemas/user.ex.

  1. Use the generated module:
alias MyApp.Schemas.User
User.parse(%{"name" => "alice"})
#=> {:ok, %{name: "alice"}}
User.parse(%{})
#=> {:error, _}

If using Rusl

Rusl is a package manager for JSON Schema — like npm or Hex, but for schemas. You declare dependencies in a manifest, pull pinned versions from the Rusl registry, and vendor them into your project (typically priv/schemas/).

ZoiForge does not require Rusl. If your schemas already come from Rusl, the flow is:

rusl install # vendor *.schema.json files into priv/schemas/
mix zoi_forge.gen # generate Elixir modules from those files

Add or update schema dependencies with the Rusl CLI first (e.g. rusl add schema rusl/schemas/common), then run the commands above. In this repo, mix schemas:sync runs both steps.

Using with Rusl

Rusl solves schema sourcing — discovery, versioning, and vendoring shared JSON Schema across teams. ZoiForge solves schema codegen — turning those files into typed Elixir modules with parse/1. They are separate tools with no hard dependency between them, but the fit is natural: Rusl lands the schemas in priv/schemas/; ZoiForge reads them and writes lib/schemas/.

See rusl.com for the registry, CLI, and how to set up rusl.bundle.toml and rusl.config.toml in your project.

Generated module API

Every generated schema module (except $defs-only parents) exposes:

FunctionPurpose
@type tElixir type spec from the JSON Schema
schema/0Parsed Zoi schema structure
raw_schema/0Original JSON Schema string
parse/1Validate and coerce input data

Schemas with $defs (or legacy definitions) also emit nested modules for each definition — e.g. MyApp.Schemas.Common.AccountSlug — so you can import and validate individual defs without a root document validator.

Configuration

Set defaults in config/config.exs:

config :zoi_forge,
source_dir: "priv/schemas",
output_dir: "lib/schemas",
prefix: "MyApp", # optional; defaults to your Mix project app name
auto_prefix: false # optional; derive module names from paths only

CLI switches override config when present:

mix zoi_forge.gen --prefix MyApp --source priv/schemas --output lib/schemas

Mix tasks

TaskDescription
mix zoi_forge.genGenerate modules from JSON Schema files
mix zoi_forge.verifyFail if generated output is stale
mix zoi_forge.cleanRemove generated modules from the output directory

Programmatic API:

ZoiForge.Generator.run(
prefix: "MyApp",
source_dir: "priv/schemas",
output_dir: "lib/schemas"
)

CI tip

Run verification before tests to catch stale generated code:

mix zoi_forge.verify
mix test

Or wire mix zoi_forge.verify into your CI pipeline after schema changes.

License

Apache-2.0 — see LICENSE.