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.length in JS, len(value) in Python) — used for every individual string field: description, basicInfo, hiddenInfo, narratorStyle , death.instructions, aiInstructions leaf strings, storySettings.worldBackground, storySettings.questGenerationGuidance, trigger condition/effect text and value fields, 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 for storyStarts per-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.