Skip to content

The ChatMapper JSON format

This is the canonical interchange format for ChatMapper branching-dialogue projects. It is what the desktop ChatMapper app exports, what ChatMapper Cloud stores, and what every importer and exporter in the ecosystem is measured against.

This document is normative for producers. If you generate ChatMapper JSON, a conforming consumer should be able to read it without knowing which tool wrote it.

Alongside this document:

  • example-scenario.json — a complete, minimal, entirely fictional project. It is covered by a test in the ChatMapper repository that asserts it validates with zero errors, so it cannot silently rot. It is reproduced in full at the foot of this page.
  • reference-parser.ts — a single dependency-free TypeScript file that parses and walks the format. MIT licensed; compiles standalone under tsc --strict. See Building a custom runtime.

A project is one JSON object:

{
"Language": "en-US",
"Title": "The Ferry at Dusk",
"Version": "1.5.1.0",
"Author": "",
"Description": "",
"UserScript": "",
"Assets": {
"Actors": [],
"Items": [],
"Locations": [],
"Conversations": [],
"UserVariables": []
}
}
KeyTypeNotes
LanguagestringBCP-47-ish locale of the primary text, e.g. en-US.
TitlestringProject title.
VersionstringThe ChatMapper schema version that wrote the file, e.g. 1.5.1.0. Informational.
AuthorstringFree text.
DescriptionstringFree text.
UserScriptstringLua run once at the start of a playthrough, after the asset tables are built.
AssetsobjectThe five asset collections. Required — a document without it is not a ChatMapper project.

All five collections must be present in a well-formed document, but consumers should treat a missing collection as empty rather than failing.

Actors, Items, Locations and UserVariables all share one shape:

{ "ID": 1, "Fields": { "Name": "Ferryman" } }
KeyTypeNotes
IDintegerUnique within its own collection.
FieldsobjectField title → value. Values are always strings, whatever the field’s declared type: "True", "30", "[]".

Conventional field titles, by collection:

CollectionFields
ActorsName, IsPlayer ("True"/"False"), Description, Pictures, Age, Gender
ItemsName, Description, Pictures, Purpose, Scene
LocationsName, Description, Pictures
UserVariablesName, Initial Value, Description

A project should declare at least one actor with IsPlayer set to "True".

Lua only ever says Variable["has_token"]. The ID exists for editor bookkeeping. This matters in practice: ChatMapper XML has no ID attribute on <UserVariable> at all, so a project that has been through XML comes back with no variable IDs. Consumers should key variables by Fields.Name and treat the ID as advisory.

{
"ID": 1,
"NodeColor": "White",
"Fields": { "Title": "Asking for passage", "Actor": "1", "Conversant": "2" },
"DialogNodes": []
}
KeyTypeNotes
IDintegerUnique across conversations.
NodeColorstring | integerCanonical form is a colour name (below).
FieldsobjectTitle, Description, Actor, Conversant, Primary Location, plus whatever the project adds.
DialogNodesarrayThe nodes. Exactly one must have IsRoot: true.

Actor and Conversant hold a stringified actor ID — "2", not "Ferryman". "" and "-1" both mean unset.

{
"ID": 3,
"ConversationID": 1,
"IsRoot": false,
"IsGroup": false,
"ConditionsString": "Variable[\"has_token\"] == true",
"UserScript": "Variable[\"has_token\"] = true",
"NodeColor": "White",
"DelaySimStatus": false,
"FalseConditionAction": 0,
"ConditionPriority": 3,
"Fields": { "Title": "Accept", "Actor": "1", "Menu Text": "I'll take the crossing." },
"OutgoingLinks": []
}
KeyTypeNotes
IDintegerUnique within its conversation. The root is conventionally 0.
ConversationIDintegerMust equal the owning conversation’s ID.
IsRootbooleanThe conversation’s start marker. Exactly one per conversation.
IsGroupbooleanA structural node: not played, presents its children as the choice set.
ConditionsStringstringLua expression. Empty means always true.
UserScriptstringLua statements run when the node is entered.
NodeColorstring | integerCanvas tint.
DelaySimStatusbooleanHold the node’s status change until the next choices are built.
FalseConditionActioninteger | string0/"Block" — drop the node when its condition fails. 1/"Passthrough" — offer its children in its place.
ConditionPriorityinteger | string1 High … 5 Low, 3 Normal. Lower wins: within a group, only candidates at the best priority present are offered.
FieldsobjectText and metadata (below).
OutgoingLinksarrayLinks out of this node.
FieldNotes
TitleAuthor-facing label. Not shown to players.
ActorStringified actor ID — who speaks this line.
ConversantStringified actor ID — who is spoken to.
Dialogue TextThe spoken line.
Menu TextPresent ⇒ this node is a player choice; the value is the button label. Absent/empty ⇒ the node plays automatically.
ParentheticalDelivery note, e.g. wearily.
Audio Files, Video File, Animation Files, PicturesMedia references.
MarkupMeaning
|Sentence split. "One.|Two." is shown as two successive lines, not one paragraph.
[f]On Menu Text: force a menu even when this is the only option.
[a]On Menu Text: an action line rather than a spoken one.
[var=Name]Substituted with the variable’s current value at display time.
[?Name], [x=?Name]Prompt the player for input and store it in Name. Consumed from the displayed line.
[pic=N], [pica=N], [picc=N]Swap the displayed image mid-line.
[em1]…[/em1] … [em4]…[/em4]Author emphasis spans. The tags are part of the text and round-trip as plain text.

Markup is part of the canonical text. Consumers that cannot honour a tag should strip it for display rather than rewriting the stored value.

{
"Filename": null,
"OriginConvoID": 1,
"DestinationConvoID": 1,
"OriginDialogID": 3,
"DestinationDialogID": 5
}

OriginConvoID/OriginDialogID must match the node the link lives on. DestinationConvoID may name a different conversation — that is a cross-conversation jump, and it is well supported.

Filename is non-null only for a link into a different project file. Most hosts, including Unity’s Dialogue System, cannot follow those. Producers should leave it null.

Canonical form is the name. The integers are the legacy desktop enum and are accepted on read.

NameLegacy int
Red0
Orange1
Yellow2
Green3
LightBlue4
DarkBlue5
Pink6
Purple7
White(no int — it is the serialized default)

What a player is offered from a node, in order:

  1. Collect every link destination.
  2. Filter by condition. A node whose ConditionsString evaluates false is dropped — unless its FalseConditionAction is Passthrough, in which case its own children join the candidate pool in its place.
  3. Suppress by priority. Within a group, only candidates at the best (numerically lowest) ConditionPriority present survive.
  4. Expand groups. A surviving IsGroup node is replaced by its own children, run through the same three steps. Its UserScript runs before the chosen child’s.

Then:

  • No survivors ⇒ end of conversation.
  • Survivors without Menu Text ⇒ play immediately (a line, not a choice).
  • Exactly one survivor with Menu Text, not [f] ⇒ play it straight through.
  • Otherwise ⇒ present a menu.

The root node is a start marker: it is never spoken, even if it carries text.

ConditionsString and UserScript are Lua. The host exposes:

  • Variable["Name"] — user variables.
  • Actor["Name"], Item["Name"], Location["Name"] — asset field tables.
  • Conversation[id], Conversation[id].Dialog[id] with a SimStatus of "Untouched" / "WasOffered" / "WasDisplayed".
  • Dialog[id] — shorthand for the evaluating node’s own conversation.

Names are cleaned before use as Lua keys: " [ ] . \ / are removed, and -, spaces and newlines become _. So a field titled Menu Text is Menu_Text, and an actor named Harbour Master is Actor["Harbour_Master"].

Two different assets whose names clean to the same key will collide. Some hosts, Unity’s Dialogue System among them, will silently let the second overwrite the first.

A condition that cannot be evaluated should be treated as true and logged, not as false. Failing open keeps content reachable.

The format is extended additively. Do not fork it.

  1. Custom fields. Add whatever you need to any Fields object. Consumers that do not know a field must preserve it, not drop it. This is how the LearnBrite runtime fields, the Unity Sequence field and every importer’s *_raw provenance fields work.
  2. _editor. A single top-level object for editor-only metadata — canvas positions, reviewer notes, field type hints, collapsed nodes. It is stripped on export and any consumer may ignore it entirely. Never put anything in _editor that the project’s meaning depends on.
  3. Graceful degradation. A consumer that does not understand an extension must still produce a working project without it.

What this rules out: renaming canonical keys, changing a field value’s type away from string, or adding a top-level sibling to Assets that carries meaning.

The desktop app also reads and writes an XML serialization of the same model. Differences worth knowing when moving between them:

  • XML is the format Unity’s Pixel Crushers Dialogue System imports natively.
  • The XML attribute is spelled FalseCondtionAction — the typo is part of the format.
  • Enum-valued properties are names in XML ("Passthrough", "Normal", "Green") and often integers in JSON.
  • <UserVariable> has no ID attribute, so variable IDs do not survive a JSON → XML → JSON trip. Names do.
  • Reviewer notes and per-field Hint/Type metadata live inline in XML, but in _editor in JSON.
  • Empty asset collections are omitted entirely in XML.

At minimum, a producer should check:

  • Every collection has unique IDs.
  • Every conversation has exactly one IsRoot node and at least one node.
  • Every node’s ConversationID matches its conversation.
  • Every link’s origin matches the node it sits on, and its destination exists.
  • Every Actor/Conversant value is "", "-1", or an existing actor ID.
  • Every variable named in a ConditionsString is declared in UserVariables.

ChatMapper Cloud’s validator implements these plus reachability and content warnings — the full list is on Validation rules.

example-scenario.json — two actors, one item, one location, one variable, one branching conversation with a group node, a script and a condition. Download it, or see the same project as XML, Yarn and a playable HTML file.

{
"Language": "en-US",
"Title": "The Ferry at Dusk",
"Version": "1.5.1.0",
"Author": "ChatMapper format documentation",
"Description": "A minimal but complete example: two actors, one item, one location, one variable, and one branching conversation. Entirely fictional.",
"UserScript": "Variable[\"has_token\"] = false",
"Assets": {
"Actors": [
{
"ID": 1,
"Fields": {
"Name": "Traveller",
"IsPlayer": "True",
"Description": "The player character.",
"Pictures": "[]"
}
},
{
"ID": 2,
"Fields": {
"Name": "Ferryman",
"IsPlayer": "False",
"Description": "Works the last crossing of the day.",
"Pictures": "[]"
}
}
],
"Items": [
{
"ID": 1,
"Fields": {
"Name": "Brass Token",
"Description": "Buys one crossing.",
"Pictures": "[]"
}
}
],
"Locations": [
{
"ID": 1,
"Fields": {
"Name": "The Jetty",
"Description": "Weathered boards over slack water.",
"Pictures": "[]"
}
}
],
"UserVariables": [
{
"ID": 1,
"Fields": {
"Name": "has_token",
"Initial Value": "False",
"Description": "Set when the traveller accepts the ferryman's token."
}
}
],
"Conversations": [
{
"ID": 1,
"NodeColor": "White",
"Fields": {
"Title": "Asking for passage",
"Description": "The only conversation in this example.",
"Actor": "1",
"Conversant": "2",
"Primary Location": "1"
},
"DialogNodes": [
{
"ID": 0,
"ConversationID": 1,
"IsRoot": true,
"IsGroup": false,
"ConditionsString": "",
"UserScript": "",
"NodeColor": "Green",
"DelaySimStatus": false,
"FalseConditionAction": 0,
"ConditionPriority": 3,
"Fields": {
"Title": "START",
"Actor": "1",
"Conversant": "2"
},
"OutgoingLinks": [
{
"Filename": null,
"OriginConvoID": 1,
"DestinationConvoID": 1,
"OriginDialogID": 0,
"DestinationDialogID": 1
}
]
},
{
"ID": 1,
"ConversationID": 1,
"IsRoot": false,
"IsGroup": false,
"ConditionsString": "",
"UserScript": "",
"NodeColor": "White",
"DelaySimStatus": false,
"FalseConditionAction": 0,
"ConditionPriority": 3,
"Fields": {
"Title": "Opening line",
"Actor": "2",
"Conversant": "1",
"Dialogue Text": "Last crossing of the day.|Coming aboard, or staying with the rain?"
},
"OutgoingLinks": [
{
"Filename": null,
"OriginConvoID": 1,
"DestinationConvoID": 1,
"OriginDialogID": 1,
"DestinationDialogID": 2
}
]
},
{
"ID": 2,
"ConversationID": 1,
"IsRoot": false,
"IsGroup": true,
"ConditionsString": "",
"UserScript": "",
"NodeColor": "White",
"DelaySimStatus": false,
"FalseConditionAction": 0,
"ConditionPriority": 3,
"Fields": {
"Title": "Traveller's options"
},
"OutgoingLinks": [
{
"Filename": null,
"OriginConvoID": 1,
"DestinationConvoID": 1,
"OriginDialogID": 2,
"DestinationDialogID": 3
},
{
"Filename": null,
"OriginConvoID": 1,
"DestinationConvoID": 1,
"OriginDialogID": 2,
"DestinationDialogID": 4
}
]
},
{
"ID": 3,
"ConversationID": 1,
"IsRoot": false,
"IsGroup": false,
"ConditionsString": "",
"UserScript": "Variable[\"has_token\"] = true",
"NodeColor": "White",
"DelaySimStatus": false,
"FalseConditionAction": 0,
"ConditionPriority": 3,
"Fields": {
"Title": "Accept the token",
"Actor": "1",
"Conversant": "2",
"Menu Text": "I'll take the crossing.",
"Dialogue Text": "I'll take the crossing."
},
"OutgoingLinks": [
{
"Filename": null,
"OriginConvoID": 1,
"DestinationConvoID": 1,
"OriginDialogID": 3,
"DestinationDialogID": 5
}
]
},
{
"ID": 4,
"ConversationID": 1,
"IsRoot": false,
"IsGroup": false,
"ConditionsString": "",
"UserScript": "",
"NodeColor": "White",
"DelaySimStatus": false,
"FalseConditionAction": 0,
"ConditionPriority": 3,
"Fields": {
"Title": "Stay ashore",
"Actor": "1",
"Conversant": "2",
"Menu Text": "I'll wait for morning.",
"Dialogue Text": "I'll wait for morning."
},
"OutgoingLinks": [
{
"Filename": null,
"OriginConvoID": 1,
"DestinationConvoID": 1,
"OriginDialogID": 4,
"DestinationDialogID": 6
}
]
},
{
"ID": 5,
"ConversationID": 1,
"IsRoot": false,
"IsGroup": false,
"ConditionsString": "Variable[\"has_token\"] == true",
"UserScript": "",
"NodeColor": "White",
"DelaySimStatus": false,
"FalseConditionAction": 0,
"ConditionPriority": 3,
"Fields": {
"Title": "Ending - crossing",
"Actor": "2",
"Conversant": "1",
"Dialogue Text": "Mind the step. She rolls once we clear the piling."
},
"OutgoingLinks": []
},
{
"ID": 6,
"ConversationID": 1,
"IsRoot": false,
"IsGroup": false,
"ConditionsString": "",
"UserScript": "",
"NodeColor": "White",
"DelaySimStatus": false,
"FalseConditionAction": 0,
"ConditionPriority": 3,
"Fields": {
"Title": "Ending - ashore",
"Actor": "2",
"Conversant": "1",
"Dialogue Text": "Suit yourself. The rain is not in a hurry either."
},
"OutgoingLinks": []
}
]
}
]
}
}