luadata
A Lua data parser with Rust, Go, Python, CLI, and WebAssembly interfaces. Useful for working with game addon data files like World of Warcraft SavedVariables.
Luadata by Example — A guided tour of all features with interactive examples.
Live Converter — Try it in your browser. Paste Lua data and get JSON instantly.
Usage
Rust
cargo add luadata
use ;
// From a string
let json = text_to_json?;
// From a file
let json = file_to_json?;
// With options
let mut config = new;
config.array_mode = Some;
config.empty_table_mode = Array;
let json = text_to_json?;
Go
go get github.com/mmobeus/luadata/go
import luadata "github.com/mmobeus/luadata/go"
// From a string
reader, err := luadata.TextToJSON("input", luaString)
// From a file
reader, err := luadata.FileToJSON("config.lua")
// From bytes
reader, err := luadata.ToJSON(luaBytes)
// From an io.Reader
reader, err := luadata.ReaderToJSON("input", r)
// With options
reader, err := luadata.TextToJSON("input", luaString,
luadata.WithArrayMode("sparse", 10),
luadata.WithEmptyTableMode("array"),
luadata.WithStringTransform(1024, "truncate"),
)
All functions return an io.Reader containing JSON.
Python
pip install mmobeus-luadata
# Get JSON string
=
# Get Python dict
=
# With options
=
CLI
# Convert a file
# Read from stdin
|
# Validate without converting
# With options
Node.js / TypeScript
npm install mmobeus-luadata
import { init, convert } from "mmobeus-luadata";
await init();
// Convert Lua data to a JSON string
const json = convert('playerName = "Thrall"');
// Parse into an object
const data = JSON.parse(convert(luaString));
// With options
const json = convert(luaString, {
emptyTable: "array",
arrayMode: "sparse",
arrayMaxGap: 10,
stringTransform: { maxLen: 1024, mode: "truncate" },
});
The package includes TypeScript type definitions. init() must be called once
before convert() — it loads the WASM module.
WebAssembly (browser)
For direct browser usage without a bundler, the WASM module can be loaded as an ES module:
import from "./luadata.js";
await ;
const json = ;
A ready-made web interface is also included:
# Opens at http://localhost:8080
Lua data format
The library parses Lua files containing top-level variable assignments. This is a common data persistence technique used by Lua systems (including World of Warcraft addon data). Each assignment is a variable name followed by = and a value:
playerName = "Thrall"
playerLevel = 60
guildRoster =
This is a valid Lua file, with a list of assignments to inline data values. These are parsed into a map-like structure, where the keys are the variable names, and the values are JSON equivalents of the Lua values.
Raw values
In addition to variable assignments, luadata can parse a single raw Lua value (a table, string, number, boolean, or nil). When a raw value is detected, the output contains a single key @root with the parsed value:
|
# {"@root":["a","b","c"]}
# {'@root': {'a': 1, 'b': 2}}
Binary strings
Lua strings are raw byte sequences with no inherent encoding. Game addons like Questie use this to store compact binary data (packed coordinates, pointer maps, serialized databases) directly inside Lua string values. When the game client writes these to disk, the bytes are written verbatim between quotes.
luadata handles this with a per-string heuristic: if a string's bytes are valid UTF-8, it decodes them as UTF-8 (so accented player names like "Fröst" render correctly). If the bytes contain any invalid UTF-8 sequences, each byte is mapped to its Latin-1 code point, preserving every byte losslessly.
A consumer can recover the original bytes from a binary string value:
=
const rawBytes = .;
rawBytes := []byte(jsonValue)
Options
All parse and convert functions accept options controlling three behaviors: string transform, array detection, and empty table rendering. The defaults are the same across all languages.
String transform
Limit string length during parsing. When a string exceeds the max length, the transform is applied — the parser treats the result as if the transformed value was the original.
| Mode | Behavior |
|---|---|
truncate |
Truncate to max length |
empty |
Replace with "" |
redact |
Replace with "[redacted]" |
replace |
Replace with a custom string |
Strings at or under the max length are not modified.
Rust:
config.string_transform = Some;
Go:
luadata.WithStringTransform(1024, "truncate")
luadata.WithStringTransform(2048, "replace", "[removed]")
Python:
CLI:
WASM:
Array detection
Lua tables with integer keys are conceptually arrays, but Lua has no distinct array type — arrays are just tables with sequential integer keys. This creates an ambiguity when converting to JSON, where arrays and objects are distinct types.
In Lua (and WoW SavedVariables in particular), array data can appear in two forms:
- Implicit index syntax:
{"apple", "banana", "cherry"}— elements have no explicit keys - Explicit integer key syntax:
{[1] = "apple", [2] = "banana", [3] = "cherry"}— each element has a[n] =prefix
WoW addons may switch between these forms over time. An array initially saved with implicit syntax may later be re-saved with explicit [n]= keys after entries are added or removed. Sparse arrays (with gaps) like {[1] = "apple", [3] = "cherry"} are also common when elements are deleted.
By default, both implicit index tables and tables with explicit integer keys render as JSON arrays, as long as the gap between consecutive keys does not exceed 20 (sparse mode with max gap 20). Missing indices are filled with null. This produces the most natural JSON for array-like data.
| Mode | {[1]="a",[2]="b"} |
{[1]="a",[3]="c"} |
{"a","b"} |
|---|---|---|---|
sparse (default, max gap 20) |
["a","b"] |
["a",null,"c"] |
["a","b"] |
sparse (max gap 0) |
["a","b"] |
{"1":"a","3":"c"} |
["a","b"] |
index-only |
{"1":"a","2":"b"} |
{"1":"a","3":"c"} |
["a","b"] |
none |
{"1":"a","2":"b"} |
{"1":"a","3":"c"} |
{"1":"a","2":"b"} |
Gaps are measured from index 0, so Lua's 1-based arrays (starting at [1]) have a gap of 0 from the start. A table starting at [2] has a leading gap of 1.
Rust:
config.array_mode = Some;
config.array_mode = Some;
config.array_mode = Some;
Go:
luadata.WithArrayMode("sparse", 0) // contiguous only
luadata.WithArrayMode("index-only")
luadata.WithArrayMode("none")
Python:
CLI:
WASM:
Empty tables
Lua has no distinction between an empty array and an empty object — both are simply {}. This creates an ambiguity when converting to JSON, where [] and {} have different meanings. By default, empty tables render as null, which avoids making an arbitrary choice between the two JSON types while still making the key visible in the output (unlike omitting it, which could look like a bug).
| Mode | foo={} |
|---|---|
null (default) |
{"foo":null} |
omit |
{} (key omitted) |
array |
{"foo":[]} |
object |
{"foo":{}} |
Both {} and whitespace-only tables (like {\n}) are treated the same way under all modes. The mode applies everywhere empty tables appear — top-level values, nested table values, and elements inside arrays.
Rust:
config.empty_table_mode = Array;
Go:
luadata.WithEmptyTableMode("array")
Python:
CLI:
WASM:
Development
Prerequisites
Optional (for Python development):
Setup
make setup
This installs gofumpt and the Rust clippy/rustfmt components.
Running checks locally
make check
Runs build, Rust tests, lint, format check, and testdata validation.
| Command | Description |
|---|---|
make test |
Run Rust + Go tests |
make test-rust |
Run Rust tests only |
make test-go |
Build clib and run Go tests |
make test-python |
Build Python module and run pytest |
make lint |
Run clippy |
make fmt |
Format Rust and Go code |
make fmt-check |
Check formatting without modifying |
make check |
Build + test + lint + fmt-check + validate |
Building
| Command | Description |
|---|---|
make build |
Build CLI binary to bin/cli/luadata |
make build-clib |
Build C shared library to bin/clib/ |
make build-clib-go |
Build clib and copy to Go embed location |
make build-wasm |
Build WebAssembly module to bin/web/ |
make build-site |
Build WASM + docs site |
make serve |
Build site and serve on :8080 |
make clean |
Remove bin/ and target/ directories |
Releasing
To push a new patch release (the most common case), run:
make release
This tags an RC on main, which triggers CI to cross-compile the Rust shared library for all platforms, run tests, create the release branch with embedded libraries, and publish the GitHub Release.
For other version bumps, set BUMP:
| Command | Description |
|---|---|
make release |
Patch bump (default) |
make release BUMP=minor |
Minor bump, resets patch to 0 |
make release BUMP=major |
Major bump, resets minor and patch |
make release BUMP=manual |
Prompt for an exact version string |
See ARCHITECTURE.md for details on the release workflow and Go consumer model.