Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Cell Parsers

Parsers are only for cell-based inputs such as Excel and CSV. Most parsers tell Sora how to turn one cell into a typed value; projection parsers such as columns and tagged_columns tell Sora how one field maps to several input columns. String default values use the same parser path for single-cell parsers. TOML row data can usually use native TOML arrays and tables instead.

Use a parser when the default cell format is too verbose or ambiguous:

[[tables.fields]]
name = "tags"
type = "list<string>"
parser = { kind = "split", separator = "|" }

With that schema, the cell value is:

starter|melee|weapon

Parser options are string values. Unknown parser kinds, unsupported options, and empty option values fail during schema normalization. The exception is projection prefixes such as columns.prefix and tagged_columns.prefix, where "" is meaningful.

Custom Lua Parsers

Projects can load project-local Lua parser scripts from project.toml:

[parsers]
scripts = ["tools/parsers.lua"]

Script paths are resolved relative to the project file. After that, every command that reads the project can use the custom parsers without repeating command-line flags:

sora build --project project.toml
sora export --project project.toml --data-root data --format json --out generated/config.json

CLI commands can also load temporary parser scripts with the global --parser-script option:

sora --parser-script tools/parsers.lua build --project project.toml
sora --parser-script tools/parsers.lua export --project project.toml --data-root data --format json --out generated/config.json

The option can be repeated and is appended after project-configured scripts. Custom parsers are trusted project code. Sora loads them with a limited Lua standard library and does not expose io, os, package, or debug.

A parser script returns a table with parsers. Each parser must define parse(cell, ctx). options is the list of supported parser options. validate(field) is optional and runs during schema normalization.

return {
  parsers = {
    slug = {
      options = { "prefix" },
      validate = function(field)
        if field.type ~= "string" then
          error("slug parser requires string")
        end
      end,
      parse = function(cell, ctx)
        local text = string.lower(string.gsub(cell.text, "%s+", "-"))
        if ctx.options.prefix ~= nil then
          return ctx.options.prefix .. text
        end
        return text
      end,
    },
  },
}

Schema fields use the custom parser by name:

[[tables.fields]]
name = "tag"
type = "string"
parser = { kind = "slug", prefix = "item-" }

cell contains kind, text, and value where applicable. ctx contains field, type, options, path, and location fields such as row, column, and sheet for worksheets. Lua return values map to Sora data values: nil, booleans, integers, floats, strings, array-like tables, and string-keyed tables.

Custom Lua parsers are single-cell parsers. They do not replace projection parsers such as columns or tagged_columns, cannot read neighboring cells, and do not change schema, source loading, or generated runtime behavior.

Default Parsing

If a field has no parser, Sora uses type-aware default parsing:

TypeCell format
boolBoolean cells, true, false, or numeric cells where zero is false and non-zero is true.
i32, i64, ref<Table.key>Integer cells, integer text, or whole-number float cells.
durationDuration text using d, h, m, s, or ms, for example 500ms, 30s, or 1h 30m. Units must be ordered from largest to smallest.
f32, f64Numeric cells or numeric text.
string, enum<Name>Cell display text.
struct<Name>, union<Name>JSON object text.
list<T>, set<T>, array<T,N>Comma-separated text. Use json for JSON arrays.
map<K,V>JSON array of two-item pairs, for example [["atk",10],["hp",20]].
optional<T>Empty cell becomes null; otherwise the inner T is parsed.

Default collection parsing is intentionally simple. Primitive items are parsed by type. Struct and union collection items must be JSON object text. Nested collections cannot be represented safely with one separator; use parser = { kind = "json" }.

Parser Summary

ParserValid target typesCell shape
splitlist<T>, set<T>, array<T,N>, or optional around those typesa,b,c
tuplestruct<T> or optional<struct<T>>Gold,0,100
columnsstruct<T> or optional<struct<T>>Multiple columns
tuple_listlist<struct<T>>, set<struct<T>>, array<struct<T>,N>, or optional around those typesGold,0,100|Gem,0,5
mapmap<K,V> or optional<map<K,V>>atk,10|hp,20
tagged_columnsunion<T> onlyMultiple columns
jsonAny typeJSON value matching the field type

array<T,N> checks the parsed item count. tuple checks the value count against the referenced struct’s field count.

split

Use split for a flat collection of primitive values, enums, refs, or simple values that can be separated reliably.

[[tables.fields]]
name = "starter_items"
type = "list<ref<Item.id>>"
parser = { kind = "split" }

Cell:

1001,1002,1003

Parsed value:

[1001,1002,1003]

Use separator when comma is not a good separator:

[[tables.fields]]
name = "tags"
type = "set<string>"
parser = { kind = "split", separator = "|" }

Cell:

starter|melee|weapon

tuple

Use tuple when a single struct is small enough to fit naturally in one cell. Values follow the referenced struct’s field declaration order.

[[structs]]
name = "ResourceCost"

[[structs.fields]]
name = "kind"
type = "enum<ResourceKind>"

[[structs.fields]]
name = "id"
type = "i32"

[[structs.fields]]
name = "count"
type = "i32"

[[tables.fields]]
name = "price"
type = "struct<ResourceCost>"
parser = { kind = "tuple" }

Cell:

Gold,0,100

Parsed value:

{"kind":"Gold","id":0,"count":100}

Use separator if struct values themselves commonly contain commas:

parser = { kind = "tuple", separator = "|" }

Cell:

Gold|0|100

columns

Use columns when one struct should be edited as normal Excel or CSV columns instead of as JSON or one compact tuple cell. It is valid on struct<T> and optional<struct<T>> table fields.

[[structs]]
name = "ResourceCost"

[[structs.fields]]
name = "kind"
type = "enum<ResourceKind>"

[[structs.fields]]
name = "id"
type = "i32"

[[structs.fields]]
name = "count"
type = "i32"

[[tables.fields]]
name = "price"
type = "struct<ResourceCost>"
parser = { kind = "columns", prefix = "price_" }

CSV headers and row:

id,name,price_kind,price_id,price_count
1,Iron Sword,Gold,0,100

Parsed price value:

{"kind":"Gold","id":0,"count":100}

With the default prefix, a field named price projects columns such as price.kind, price.id, and price.count. Use prefix = "" only when the struct field names should live at the table’s top level. Sora rejects projected column name conflicts.

columns does not recursively project nested structs or unions. If a projected struct field is itself complex, either give that child field a single-cell parser such as tuple, split, map, or json, or move the nested data into a dedicated table and connect it with ref or a derived field. This keeps the spreadsheet narrow and keeps complex records reusable.

For generated XLSX templates, columns projected from the same columns field share the same header color.

tuple_list

Use tuple_list for a list of small structs. separator splits fields inside one struct item. item_separator splits items in the list.

[[tables.fields]]
name = "materials"
type = "list<struct<ResourceCost>>"
parser = { kind = "tuple_list" }

Cell:

Item,2003,4|Gold,0,1000

Parsed value:

[
  {"kind":"Item","id":2003,"count":4},
  {"kind":"Gold","id":0,"count":1000}
]

Custom separators:

parser = { kind = "tuple_list", separator = ":", item_separator = ";" }

Cell:

Item:2003:4;Gold:0:1000

map

Use map when a map is simple enough to write as repeated key/value pairs. separator splits key from value. item_separator splits map entries.

[[tables.fields]]
name = "attributes"
type = "map<string,i32>"
parser = { kind = "map" }

Cell:

atk,10|hp,20

Parsed value:

[["atk",10],["hp",20]]

Sora exports maps as pair arrays so non-string keys remain unambiguous. If you prefer JSON cell syntax, use parser = { kind = "json" } and write the same pair-array shape:

[["atk",10],["hp",20]]

tagged_columns

Use tagged_columns when one union<T> value should be edited across multiple Excel or CSV columns. It is only valid on a table field whose type is exactly union<T>. It is intentionally not valid for optional<union<T>>, list<union<T>>, set<union<T>>, or other containers.

[[unions]]
name = "EventCondition"
tag = "type"

[[unions.variants]]
name = "QuestCompleted"

[[unions.variants.fields]]
name = "quest_id"
type = "ref<Quest.id>"

[[unions.variants]]
name = "HasItem"

[[unions.variants.fields]]
name = "item_id"
type = "ref<Item.id>"

[[unions.variants.fields]]
name = "count"
type = "i32"

[[tables.fields]]
name = "value"
type = "union<EventCondition>"
parser = { kind = "tagged_columns", prefix = "" }

CSV headers and rows:

id,type,quest_id,item_id,count
1,QuestCompleted,5002,,
2,HasItem,,1001,2

The tag column contains the union variant name. Only fields for the selected variant may contain values. With the default prefix, a field named condition projects columns such as condition.type, condition.quest_id, and condition.item_id. Use prefix = "" only when the projected columns should live at the table’s top level.

Sora rejects projected column name conflicts, for example a normal table field named type plus prefix = "" for a union whose tag is also type.

tagged_columns also does not recursively project nested structs or nested unions inside variant fields. Variant fields can still use single-cell parsers such as tuple, split, map, or json. If a variant needs a large nested object or repeated nested objects, model that data as a dedicated table and reference or derive it instead of widening the union row.

For generated XLSX templates, columns projected from the same tagged_columns field share the same header color. The tag column uses the same color group with stronger emphasis.

json

Use json for nested values, unions inside containers, nested collections, and any shape that needs explicit escaping.

[[tables.fields]]
name = "actions"
type = "list<union<RewardAction>>"
parser = { kind = "json" }

Cell:

[
  {"type":"AddItem","item_id":1007,"count":3},
  {"type":"UnlockStage","stage_id":9002}
]

For one union value:

[[tables.fields]]
name = "condition"
type = "union<EventCondition>"
parser = { kind = "json" }

Cell:

{"type":"QuestCompleted","quest_id":5002}

For map<K,V>, JSON uses an array of pairs, not a JSON object:

[["atk",10],["hp",20]]

Choosing a Parser

NeedPrefer
Flat list of primitive valuessplit
One compact structtuple
One struct spread across columnscolumns
Repeated compact structstuple_list
Simple key/value pairsmap
One union spread across columnstagged_columns
Nested values, unions in containers, escaping, or JSON-shaped cellsjson