A Rust library and CLI tool for TJSON
TJSON is a hyper-readable, round trip safe and data preserving substitute for JSON that feels like text and represents the same data while looking quite different and allowing different generator rules to optimize readability. It is not a superset or a subset of JSON, but it does represent the same underlying data in a different format. It's position based to emphasize locality of meaning, and adds bare strings, pipe tables, comments, multiline string literals, and line folding to make the contained data easier to read while remaining fully convertible to and from standard JSON while retaining exactly the same data. TJSON is optimized for reading and deterministic data, not human editing.
Usage as a binary, library (including WASM too), through serde Serialize, and from other languages via a C API are all fully supported.
The below JSON is hand-formatted and hand-aligned. It is used here as a theoretical upper bound on how good JSON can look for comparison. The output of JSON.stringify(json, null, 2) and most platform pretty printers look far worse than this. The TJSON output is completely automatic with zero hand tuning or options, and still looks better.
Hand-Formatted JSON Input
{
"name": "Alice",
"age": 30,
"active": true,
"bio": "She is a developer.\nShe loves Rust.",
"scores": [90, 85, 92],
"tags": ["rust", "wasm", "json", "serialization"],
"team": [
{"name": "Alice", "age": 30, "role": "admin"},
{"name": "Bob", "age": 25, "role": "user"},
{"name": "Carol", "age": 35, "role": "user"}
]
}
TJSON output
name: Alice age:30 active:true
bio: ``
| She is a developer.
| She loves Rust.
``
scores: 90, 85, 92
tags: rust wasm json serialization
team:
|name |age |role |
| Alice |30 | admin |
| Bob |25 | user |
| Carol |35 | user |
Add to your Cargo.toml:
[dependencies]
tjson = { package = "tjson-rs", version = "0.10" }Install the CLI:
cargo install tjson-rsuse tjson::Value;
// Parse a TJSON object (keys indented 2 spaces at the top level)
let value: Value = " name: Alice\n age:30".parse()?;
// Parse a bare string
let value: Value = " hello world".parse()?;use tjson::{Value, RenderOptions};
// From a serde_json value
let value = Value::from(serde_json::json!({"name": "Alice", "age": 30}));
// Default options
let tjson = value.to_tjson_with(RenderOptions::default())?;
// Canonical (one key per line, no packing, see docs for details)
let canonical = value.to_tjson_with(RenderOptions::canonical())?;
// Back out to JSON. `to_json` is MINIMAL JSON -- no whitespace outside
// strings -- which is also valid TJSON, so it can be fed straight back in.
// `to_json_pretty` is the same data laid out for a person to read.
let json = value.to_json();
let shown = value.to_json_pretty();use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
struct Person {
name: String,
age: u32,
}
// Deserialize from TJSON
let person: Person = tjson::from_str(" name: Alice\n age:30")?;
// Serialize to TJSON
let tjson = tjson::to_string(&person)?;When the document doesn't match your types, the error points at the actual spot in the file — the field, the line, and the offending text:
line 2, column 8: age: invalid type: string "banana", expected u32
age: banana
^
If you already hold a parsed Value or Document, tjson::from_value(&value)
and tjson::from_document(&doc) deserialize them directly — the latter lets one
parse yield both the comments and the typed data. Their errors name the failing
field but carry no line numbers, since there is no source text behind them. And for documents that are only
partly structured, a struct field can be typed as tjson::Value to accept any
shape there.
TJSON files can contain comments. Parsing to a Value drops them — parse a
Document instead when you want comments and formatting choices to survive
reading and re-writing a file:
let doc: tjson::Document = "// header\n a:1".parse()?;
let out = doc.to_tjson_with(RenderOptions::default());
assert!(out.starts_with("// header"));The renderer still lays everything out fresh — indentation, alignment, and line
wrapping are normalized — but what you wrote stays what you wrote: quoted strings
stay quoted, tables stay tables, and comments come back in the right places.
Four options control this (honor_string_forms, honor_key_forms,
honor_tables, render_comments), all on by default; canonical() keeps
comments and normalizes everything else. To build commented TJSON output from
code, construct the tree with tjson::document::{Node, Entry, Comment}.
RenderOptions is a builder. Start from RenderOptions::default(), or from
RenderOptions::canonical() — a preset (not a flag) that renders one key per
line with no packing, no tables, no multiline blocks, and no folding. All
builder methods take self and return Self:
let opts = RenderOptions::default()
.wrap_width(Some(60))
.tables(false)
.multiline_strings(false);
let tjson = tjson::to_string_with(&value, opts)?;Option enums live in tjson::options (e.g. use tjson::options::MultilineStyle;).
Key options:
| Option | Default | Description |
|---|---|---|
wrap_width(Option<usize>) |
Some(80) |
Column wrap limit, clamped to >= 20; None for unlimited |
tables(bool) |
true |
Render arrays-of-objects as pipe tables |
multiline_strings(bool) |
true |
Use `` blocks for strings containing newlines |
inline_objects(bool) |
true |
Pack multiple key-value pairs onto one line |
inline_arrays(bool) |
true |
Pack multiple array items onto one line |
string_array_style(StringArrayStyle) |
PreferSpaces |
What strings may share a line with: Comma, PreferComma, PreferSpaces, Spaces, None (least to most restrictive) |
Advanced options:
| Option | Default | Description |
|---|---|---|
bare_strings(StringStyle) |
Bare |
How a string value announces itself: Quoted always quotes; Bare uses the unquoted form where the spec permits, its opening quote being the space in front of it; Marked writes that space as _ so it can be seen |
bare_keys(BareStyle) |
Prefer |
Use bare (unquoted) object keys when spec permits: Prefer, None |
force_markers(bool) |
false |
Force explicit [ / { indent markers on single-step indents |
multiline_style(MultilineStyle) |
Bold |
Multiline block style: Bold, Floating, BoldFloating, BoldLight, Light, Transparent, FoldingQuotes |
multiline_min_lines(usize) |
1 |
Min newlines in a string before using a multiline block |
indent_glyph_style(IndentGlyphStyle) |
Auto |
When to wrap deeply nested content in /< /> glyphs: Auto, Fixed, None |
indent_glyph_marker_style(IndentGlyphMarkerStyle) |
Compact |
Where to place the opening /< glyph: Compact, Separate |
table_unindent_style(TableUnindentStyle) |
Auto |
How to reposition wide tables toward the left margin: Left, Auto, Floating, None |
table_min_rows(usize) |
3 |
Min rows required to render a table |
table_min_columns(usize) |
3 |
Min columns required to render a table |
table_min_similarity(f32) |
0.8 |
Min fraction of rows sharing a column |
table_column_max_width(Option<usize>) |
Some(40) |
Bail on table if any column exceeds this width |
fold(FoldStyle) |
— | Set all four fold styles at once (Auto, Fixed, None); individual options override |
number_fold_style(FoldStyle) |
Auto |
How to fold long numbers across lines |
string_bare_fold_style(FoldStyle) |
Auto |
How to fold long bare strings |
string_quoted_fold_style(FoldStyle) |
Auto |
How to fold long quoted strings |
string_multiline_fold_style(FoldStyle) |
None |
How to fold multiline block continuation lines |
eol(Eol) |
Lf |
Line ending between output lines: Lf, CrLf |
honor_string_forms(bool) |
true |
Document rendering: honor recorded bare/quoted/multiline string forms |
honor_key_forms(bool) |
true |
Document rendering: honor recorded bare/quoted key forms |
honor_tables(bool) |
true |
Document rendering: honor recorded was-a-table facts |
render_comments(bool) |
true |
Document rendering: emit carried comments |
Experimental options (may change or be removed in a future version):
| Option | Default | Description |
|---|---|---|
kv_pack_multiple(usize) |
2 |
Spacing multiplier between packed key-value pairs (1–4; spaces = value × 2) |
multiline_max_lines(usize) |
10 |
Max lines in a Floating block before falling back to Bold |
table_fold(bool) |
false |
Fold long table rows across continuation lines |
# JSON to TJSON
echo '{"name":"Alice","scores":[1,2,3]}' | tjson
# TJSON to JSON
echo ' name: Alice' | tjson --json
# From/to files
tjson -i data.json -o data.tjson
tjson --json -i data.tjson -o data.json
# Canonical output
tjson --canonical -i data.jsonThis crate also compiles to WebAssembly. The npm package @rfanth/tjson wraps it with a JavaScript/TypeScript API (camelCase options, full TypeScript types). See the npm README for usage and options.
The library can be built as a shared library exporting a small C API, callable
from any language with a C FFI — C, C++, Delphi, C#, Python via ctypes, Go,
Lua, and more:
cargo build --release --features capi#include "tjson.h"
TjsonError err = { 0, 0, 0, NULL };
char *json = tjson_to_json(" name: Alice city: London", &err);
/* ... use json ... */
tjson_free_string(json);
/* Same data, indented, for output a person reads. */
char *shown = tjson_to_json_pretty(" name: Alice city: London", &err);
tjson_free_string(shown);The header is include/tjson.h; the full reference —
memory ownership, error codes, and the options object — is in
docs/c-api.md.
- Website and live demo: textjson.com
- Test suite: tjson-tests
- npm package: @rfanth/tjson — JavaScript/TypeScript bindings
- MariaDB/MySQL UDF: tjson-udf — same options in SQL
- Editor support: tjson-highlight — syntax highlighting and inline parse errors for VS Code and VSCodium, plus a GNU nano syntax file
- Specification: tjson-specification.md — The spec is versioned independently from this implementation: each release is written against the spec as published at release time (the two are typically released together when the spec behavior changes).
BSD-3-Clause. See LICENSE.