Validation & Size Limits
How the io-ts codec validates JSON, the hard size caps the editor enforces, and what the local validator script checks beyond the codec.
Last updated:
Schema Validation Engine
The editor validates the JSON using io-ts (not Zod). This has two important consequences:
Non-strict validation - extra fields are permitted by the codec. The schema uses InterfaceType, which validates that required fields are present and correctly typed but does not strip or reject additional properties. Extra fields are not stripped — they remain in the stored JSON. The Validator Script flags them as warnings so you know they fall outside the formal schema. Information that doesn’t have a dedicated field belongs in the section’s narrative slot — basicInfo, hiddenInfo, description, or the equivalent — where retrieval is reliable and consistent.
Error path notation. Validator error paths like narratorStyle.0 or regions.Name.0.fieldName use .0/.1 as union branch indices, not array indices. When a field is defined as string | undefined, .0 means “the string branch failed” and .1 means “the undefined branch also failed” - which together mean the value was neither a string nor absent. A path like locations.key.0.basicInfo means the field basicInfo failed validation in the required-fields branch (index 0) of the location intersection type.
Size Limits
Hard caps enforced by the Voyage editor’s validation. Exceeding these causes the wand validator to reject the document. All values below come from a single source of truth: wiki-app/src/data/size-limits.ts. Per-section “Size limits” rows in each section hero are generated from the same data.
Trigger Budgets
| Field / Pattern | Limit |
|---|---|
| Mechanical triggers (count) | 500 |
| Semantic triggers (count) | 200 |
| Per-trigger size (compact JSON) | 10,000 chars (nominal; engine first fails at 10,028 compact chars) |
| Per-trigger conditions (count) | 5 |
| Per-trigger effects (count) | 5 |
Trigger condition .text
|
1,000 chars |
Trigger condition .value
|
100 chars |
Trigger effect .text
|
1,000 chars |
Trigger effect .value
|
100 chars |
Trigger script field |
string — size counted toward the per-trigger limit; no separate char cap |
Narrative & Story
| Field / Pattern | Limit |
|---|---|
storySettings.worldBackground
|
5,000 chars |
storySettings.questGenerationGuidance
|
5,000 chars |
narratorStyle
|
2,000 chars |
death.instructions
|
4,000 chars |
worldLore (entire section) |
500,000 chars |
worldLore.*.text
|
4,000 chars |
storyStarts (entry count) |
100 entries |
storyStarts.* (each entry, pretty JSON) |
4,000 chars (nominal; engine first fails at 4,008 pretty chars) |
Catalogs
| Field / Pattern | Limit |
|---|---|
items.*.description
|
4,000 chars |
factions.*.basicInfo
|
4,000 chars |
factions.*.hiddenInfo
|
4,000 chars |
npcTypes.*.description
|
8,000 chars |
npcs.* (each entry, compact JSON) |
8,000 chars (nominal; engine first fails at 7,996 compact chars) |
premadeCharacters (entry count) |
100 entries |
premadeCharacters.* (each, compact JSON) |
20,000 chars |
Geography
| Field / Pattern | Limit |
|---|---|
realms.*.basicInfo
|
100,000 chars |
regions.*.basicInfo
|
4,000 chars |
regions.*.hiddenInfo
|
4,000 chars |
locations.*.basicInfo
|
4,000 chars |
locations.*.hiddenInfo
|
4,000 chars |
locations.*.areas.*.description
|
4,000 chars |
realms (entire section) |
100,000 chars |
Mechanics Limits
| Field / Pattern | Limit |
|---|---|
traits.*.description
|
4,000 chars |
locations (entire section) |
1,000,000 chars |
npcs (entire section) |
1,000,000 chars |
npcTypes (entire section) |
500,000 chars |
factions (entire section) |
100,000 chars |
regions (entire section) |
500,000 chars |
items (entire section) |
100,000 chars |
traitCategories (entire section) |
100,000 chars |
itemSettings (entire section) |
5,000 chars |
abilities (entry count) |
1,000 entries |
abilities.*.description
|
2,000 chars |
abilities.*.requirements (array length) |
10 entries |
abilities.*.bonus
|
must not exceed skillSettings.maxSkillSuccessLevel (balanced default 999) — Voyage clamps the applied contribution to that cap, so any bonus above it is silently wasted |
AI Instructions Limits
| Field / Pattern | Limit |
|---|---|
Each string leaf under a task (aiInstructions.<task>.<key>) |
5,000 chars |
Per task (aiInstructions.<task> total, sum of instruction chars) |
20,000 chars |
Image Prompts
| Field / Pattern | Limit |
|---|---|
imagePromptConfiguration.npcs / .locations / .regions
|
5,000 chars |
imagePromptConfiguration (combined npcs+locations+regions) |
15,000 chars |
Note (How limits are measured):
- Raw character length (
value.lengthin JS,len(value)in Python) — used for every individual string field:description,basicInfo,hiddenInfo,narratorStyle,death.instructions,aiInstructionsleaf strings,storySettings.worldBackground,storySettings.questGenerationGuidance, trigger condition/effecttextandvaluefields, and similar. Counts every codepoint as 1 — newlines count as 1 char, em dashes count as 1 char. This is what the Voyage editor’s character counter reports and what the engine enforces.- Pretty-printed JSON (
json.dumps(obj, indent=2)) — used for all section totals (items,factions,regions,npcs,npcTypes,locations,worldLore,traitCategories,itemSettings) and forstoryStartsper-entry. The structure is serialized with 2-space indentation, and that indentation counts toward the limit — every nesting level adds 2 spaces per line against your budget, so deeply nested entries cost more than their text alone.- Compact JSON (
json.dumps(obj)) — used for individual NPC entries and individual trigger entries.