What is PDL?
PDL (Phony Definition Language) is the declarative, JSON-based language you use to describe the data you want. You don't write how to generate each value — you declare what the data is (a table of users; an id; a name that sounds Turkish; an email built from that name) and Phony produces it, reproducibly, from a seed.
PDL is to synthetic data what a schema migration is to a database: a precise, versionable, reviewable description that any runtime can execute identically.
- Declarative — describe the shape, not the procedure.
- JSON — editable by hand, by tools, and by agents; validated by a JSON Schema; diffable in Git.
- Deterministic —
(schema, seed, reference-time)fully determines the output (see How generation works).
The shape of a schema
Every PDL document has up to four top-level sections:
{
"generators": { /* reusable value producers */ },
"entities": { /* tables: named fields wired to generators */ },
"relationships": [ /* optional explicit FK declarations */ ],
"scenarios": { /* how many rows of each entity to generate */ }
}| Section | What it declares | Required |
|---|---|---|
generators | named, reusable producers (an id, a name, an email…) | for anything non-trivial |
entities | tables — a set of named fields, each wired to a generator | to produce rows |
relationships | explicit foreign-key edges (cardinality, on-delete) | optional |
scenarios | named row counts per entity (dev, staging, …) | to run a scenario |
A complete, annotated example
{
"generators": {
"id": { "type": "logic", "algorithm": "uuid_v7" },
"first_name": { "type": "model", "source": "person.first_names",
"generation": { "mode": "word" } },
"last_name": { "type": "model", "source": "person.last_names",
"generation": { "mode": "word" } },
"email": {
"type": "template",
"pattern": "{{lowercase(first_name)}}.{{lowercase(last_name)}}@example.com",
"unique": true
},
"age": { "type": "logic", "algorithm": "int_between",
"params": { "min": 18, "max": 80 } }
},
"entities": {
"User": {
"fields": {
"id": { "generator": "id", "primary_key": true },
"first_name": { "generator": "first_name" },
"last_name": { "generator": "last_name" },
"email": { "generator": "email" },
"age": { "generator": "age" },
"full_name": { "type": "template", "pattern": "{{self.first_name}} {{self.last_name}}" }
}
},
"Order": {
"fields": {
"id": { "generator": "id", "primary_key": true },
"user_id": { "ref": "User.id" },
"total": { "type": "logic", "algorithm": "float_between",
"params": { "min": 10, "max": 500, "precision": 2 } }
}
}
},
"scenarios": { "dev": { "User": 100, "Order": 300 } }
}What this declares, precisely:
- Five reusable generators.
first_name/last_nameare N-gram models (trained on samples, producing novel locale-appropriate names).emailis a template that composes those names;unique: trueguarantees no duplicates. - Two entities (tables).
User.emailreuses theemailgenerator;User.full_nameis an inline template that reads sibling fields viaself.;Order.user_idis a foreign key toUser.id. - One scenario,
dev: 100 users and 300 orders. Each order'suser_idpoints at a real, already-generated user — joins are always valid.
Run it with a seed and you get 100 + 300 rows; run it again with the same seed and you get the same rows.
The two kinds of "generator"
You'll see generators in two places, and it's worth being precise:
- Schema generators (
"generators": { … }) — named, reusable, referenced by many fields. Define once, use everywhere. - Inline field generators — a field can carry a generator definition directly (
"full_name": { "type": "template", … }) when it isn't reused.
Both compile to the same internal form (see How generation works).
How to read the rest of these docs
| You want… | Read |
|---|---|
| Every generator type + its parameters | Generators in depth |
| Fields, foreign keys, computed columns, scenarios | Entities & relationships |
The {{ … }} expression language and its functions | Expressions (PEL) |
| Compilation, seeds, determinism, running a schema | How generation works |
| Install & run a schema (Rust) | The engine (Rust) |
The exhaustive design specification — including parts not yet implemented — lives under Spec → PDL Specification.