paradox-ecto
Generate Ecto schemas and database migrations from Paradox specifications.
Define your types and URI endpoints in .dox, and paradox-ecto generates Ecto schema modules with changesets, JSON serialization, and Dox validation bridging — plus database migrations that evolve incrementally as your spec changes.
Usage
paradox-ecto --title "My App" --uris createUser --uris listUsers
All flags:
| Flag | Default | Description |
|---|---|---|
|
(required) |
The name of your app (used as the Elixir project name) |
|
(repeatable, required) |
URI constant name from the |
|
(optional) |
Persistent directory for incremental migration files (requires |
|
(optional) |
Path to baseline spec JSON for migration diffing (requires |
Example
Given this .dox specification:
import std
type Pet
name: Text
species: Text
age: Integer?
createPet: URI
http://localhost/pets POST (Pet. Pet)
listPets: URI
http://localhost/pets GET (Unit. [Pet])
Run:
paradox-ecto --title "PetShop" --uris createPet --uris listPets
This generates an Ecto schema and migration in .dox/:
Generated Schema
defmodule Dox.Schemas.Pet do
use Ecto.Schema
import Ecto.Changeset
schema "pet" do
field :name, :string
field :species, :string
field :age, :integer, default: nil
timestamps()
end
def changeset(struct, params \\ %{}) do
struct
|> cast(params, [:name, :species, :age])
|> validate_required([:name, :species])
|> validate_with_dox()
end
def to_json(struct) do
%{
"id" => struct.id,
"name" => struct.name,
"species" => struct.species,
"age" => struct.age,
}
end
def from_params(params) do
%__MODULE__{}
|> changeset(params)
end
defp validate_with_dox(changeset), do: changeset
end
How It Works
paradox-ecto operates in four stages:
-
Parse specification — Reads
specification.jsonproduced byparadox generate --spec -
Scan Dox modules — Finds
valid_<type>/1validator functions in generated Dox Elixir modules -
Resolve URIs — Looks up each
--urisname in the spec, extracts input/output types -
Generate code — Produces Ecto schemas and migrations for all types referenced in URI io fields
Generated Output
Schemas and migrations are generated into .dox/ as library code:
.dox/
lib/
dox/
schemas/
<type>.ex # Ecto schema module per URI-referenced type
priv/
repo/
migrations/
20240101000000_create_tables.exs # Database migration
The downstream Mix project includes .dox/lib in its elixirc_paths:
# mix.exs
defp elixirc_paths(_env), do: ["lib", ".dox/lib"]
Type Mapping
Paradox types map to Ecto column types in migrations and schema field types:
| Paradox Type | Migration Column | Schema Field |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Optional ( |
Column without |
Field with |
Union types |
|
|
Wrap types |
Resolves to inner type |
Resolves to inner type |
Foreign Keys
Fields typed as Key OtherType in .dox become foreign key references in migrations and integer fields with source: mapping in schemas:
type Post
title: Text
author: Key User
Migration:
add :author, references(:user), null: false
Schema:
field :author_id, :integer, source: :author
Dox Validation Bridging
When a .dox specification defines valid rules for a type, paradox generate --elixir emits a valid_<type>/1 function. paradox-ecto detects these and bridges them into Ecto changesets:
-
After standard Ecto validations pass, changeset fields are mapped back to a Dox struct
-
The
valid_<type>/1function is called -
Errors are parsed and added to the changeset as field-specific errors
valid Pet
age > 0 | Age must be positive
This produces a schema where changeset/2 calls Dox.Pets.valid_pet/1 after standard validations.
Incremental Migrations
By default, paradox-ecto generates a single create_tables migration. For production use, pass --migrations-dir and --baseline to enable incremental migration generation that preserves existing data when the spec evolves.
Setup
paradox-ecto --title "My App" \
--migrations-dir priv/repo/migrations \
--baseline priv/repo/baseline_spec.json \
--uris createUser --uris listUsers
On the first run, this generates:
-
priv/repo/migrations/20240101000000_create_tables.exs— initial migration -
priv/repo/baseline_spec.json— snapshot of the current schema
Both files should be committed to version control.
Schema Evolution
When types.dox changes and paradox-ecto runs again, it diffs the current spec against the baseline and generates a new timestamped ALTER migration:
defmodule Dox.Repo.Migrations.Alter20260326120000 do
use Ecto.Migration
def change do
alter table(:"pet") do
add :color, :text, null: false
remove :species, :text, null: false
end
end
end
The baseline is updated after each successful generation. All generated migrations are reversible — remove includes the column type for rollback, and modify includes from: for the original type.
Supported Operations
| Change | Generated Migration |
|---|---|
New type added |
|
Type removed |
|
Field added |
|
Field removed |
|
Field type changed |
|
Nullability changed |
|
FK target changed |
|
How Diffing Works
The diff pipeline is three pure functions:
-
Normalize — Convert parsed spec types into a table map of
%{table_name ⇒ [%{name, type, nullable, reference}]} -
Diff — Compare old and new table maps, producing a list of migration operations
-
Render — Convert operations into valid Ecto migration source code
The baseline stores the normalized table map as JSON, not the raw spec. This decouples the diff from spec format changes.
See Also
-
Elixir Code Generation — The Elixir modules and validation functions used by schemas
-
SQL Code Generation — SQL DDL generation
-
paradox-phoenix — Full Phoenix application generator (uses paradox-ecto internally)