How It Works¶
avro-datagen reads an Avro .avsc schema and generates realistic fake records.
This page explains exactly what happens for each field.
The big picture¶
.avsc file
|
v
load_schema() parse JSON
|
v
RecordResolver(schema) index named types, init pools
|
v
resolver.generate() for each field, top to bottom:
| resolve_field(field, record_so_far)
v
{ record dict } one complete record
Fields are resolved in declaration order. This matters because later fields
can reference earlier ones via ref, template, and rules.
Field resolution priority¶
For each field, the resolver checks these in order. First match wins.
_resolve_field(field)
|
+-- 1. rules? evaluate conditions, first match wins
|
+-- 2. ref? copy from another field (with type conversion)
|
+-- 3. hints? arg.properties (see sub-order below)
|
+-- 4. default? Avro default value
|
+-- 5. type generate from Avro type / logicalType
Hint sub-order (priority 3)¶
When arg.properties is present but doesn't contain rules or ref:
| Order | Hint | What it does |
|---|---|---|
| 3a | ref |
Copy from another field |
| 3b | template |
String interpolation: "Purchase at {merchantName}" |
| 3c | faker |
Faker provider: "name", "email", {"method": "bothify", "args": ["##-??"]} |
| 3d | options |
Random choice from list (duplicates = weighting) |
| 3e | pool |
Pick from N pre-generated unique values |
| 3f | range |
Numeric or timestamp bounds |
| 3g | pattern |
Regex-like: "[A-Z]{3}-[0-9]{4}" |
Type resolution¶
When no hints match, the resolver generates based on the Avro type:
_resolve_type(avro_type)
|
+-- union list? ["null", "string"]
| null_probability (default 0.2), then pick a non-null branch
|
+-- complex dict?
| +-- record recursive (new resolver, shared pools)
| +-- array length hint or default 1-5
| +-- map key_0, key_1, ...
| +-- enum random choice from symbols
| +-- fixed random bytes (hex)
| +-- logical uuid, timestamp-millis, etc.
| +-- primitive string, int, long, etc.
|
+-- primitive str? generate default value for type
Walkthrough: Transaction schema¶
Given this schema (simplified):
{
"fields": [
{ "name": "correlationId", "type": {"logicalType": "uuid"} },
{ "name": "customerId", "type": {"logicalType": "uuid"}, "arg.properties": {"pool": 50} },
{ "name": "category", "arg.properties": {"options": ["GROCERIES", "TRANSPORT"]} },
{ "name": "merchantName", "arg.properties": {"rules": [
{"when": {"field": "category", "equals": "GROCERIES"}, "then": {"options": ["Pick n Pay"]}}
]}},
{ "name": "amount", "arg.properties": {"rules": [
{"when": {"field": "category", "equals": "GROCERIES"}, "then": {"range": {"min": 35, "max": 4500}}}
]}},
{ "name": "description", "arg.properties": {"template": "Purchase at {merchantName}"} },
{ "name": "timestamp", "type": {"logicalType": "timestamp-millis"}, "arg.properties": {"range": {"min": "-30d", "max": "now"}} },
{ "name": "createdAt", "type": {"logicalType": "iso-timestamp"}, "arg.properties": {"ref": "timestamp"} }
]
}
Here's how each field resolves:
| # | Field | Path taken | Result |
|---|---|---|---|
| 1 | correlationId |
no hints, no default -> type -> logicalType uuid |
"a1b2c3d4-..." |
| 2 | customerId |
hint: pool: 50 -> generate 50 UUIDs once, pick one |
"f8e7d6c5-..." |
| 3 | category |
hint: options -> random choice |
"GROCERIES" |
| 4 | merchantName |
rules -> check category == "GROCERIES" -> match -> options |
"Pick n Pay" |
| 5 | amount |
rules -> check category == "GROCERIES" -> match -> range |
1247.50 |
| 6 | description |
hint: template -> interpolate {merchantName} from record |
"Purchase at Pick n Pay" |
| 7 | timestamp |
hint: range with time offsets -> random epoch ms in last 30 days |
1744123456000 |
| 8 | createdAt |
ref: "timestamp" -> copy + convert epoch ms to ISO string |
"2025-04-08T..." |
Notice how fields 4-6 depend on fields 3 and earlier. This is why declaration order matters.
Shared state¶
These are shared across all records in a single generate() run:
| State | Purpose | Scope |
|---|---|---|
| Pools | Pre-generated unique values (e.g. 50 customer IDs) | Shared with nested records |
| Named types | Index of record types for nested resolution | Shared with nested records |
| now_ts | Timestamp reference (current time or fixed epoch) | Shared with nested records |
| Locale Fakers | Cached per-locale Faker instances | Per resolver |
Nested records¶
When a field's type is a record:
- A new
RecordResolveris created for the nested schema - It shares
pools,named_types, andnow_tswith the parent - The nested record's fields are resolved independently
- The result is a nested dict in the parent record
Seed behaviour¶
| With seed | Without seed |
|---|---|
random.seed(seed) — all randomness controlled |
Random each run |
_faker.seed_instance(seed) — Faker output controlled |
Faker random each run |
now_ts pinned to 2026-01-01 |
now_ts = current time |
| Fully deterministic output | Different every time |