README
ΒΆ
go-toon
go-toon is a Go implementation of Token-Oriented Object Notation (TOON) v4.4. It provides an ordered-node library, format normalization helpers, ordered JSON output, and a toon CLI.
The TOON specification this implementation follows lives at toon-format/spec/SPEC.md.
The library exposes toon.SpecVersion = "4.4".
toon-spec: 4.4
See CHANGELOG.md for release notes.
Demo
Try conversions in the browser:
Features
- π TOON v4.4 compliant β encoder, decoder, and validator follow the TOON spec and run against the vendored spec fixtures.
- π§© v4 syntax β keyed tabular arrays, nested field groups,
#comment lines, and the v4 list-item layout. - π¦ Ordered document model β
toon.Nodepreserves field order end-to-end; Gomapis never the primary representation. - π Format normalization β convert JSON, YAML, CSV, and XML into ordered
*toon.Nodevalues, plus ordered JSON output that bypasses Go maps. - π‘οΈ Security-first defaults β strict decode on, JSON duplicate keys rejected, XML DTDs and external entities blocked, YAML alias cycle detection, configurable resource limits.
- π’ Lossless numbers β raw decimal token preservation by default, including out-of-range values, with explicit opt-ins for
float64and safe-string fallbacks for unsafe values. - π§° Production CLI β
toon encode,decode, andvalidatewith a flag surface mirroring library options. - β‘ Battle-tested β race tests, fuzz targets, golden conformance tests, and cross-platform builds for
linux/amd64,linux/arm64,darwin/amd64,darwin/arm64, andwindows/amd64.
Comparison
| Feature | go-toon | toon-format/toon-go |
|---|---|---|
| Primary API style | π§± Ordered *toon.Node core plus toon/reflect |
πͺΆ Top-level Marshal / Unmarshal |
| Decode target | π§± Ordered *toon.Node |
πΊοΈ any, map[string]any, []any |
| Preserves decoded object order | β Yes | β No |
| Lossless number tokens on decode | β Yes | β οΈ No, decodes numbers as float64 |
| Struct marshal/unmarshal | β
Yes, via toon/reflect |
β Yes, top-level |
omitempty struct tags |
β Yes | β Yes |
Custom time.Time formatting |
β Yes | β Yes |
| JSON/YAML/CSV/XML to TOON | β Yes | β No |
| Ordered TOON to JSON | β Yes | β No |
| CLI | β Yes | β No |
| Validation-only API | β Yes | β No dedicated API |
| Typed error codes | β Yes | β No public error-code API |
| Resource limits | β Yes | β No public resource limits |
| Reusable encoder/decoder types | β No public core types | β Yes |
| String convenience APIs | β No core EncodeString / DecodeString |
β Yes |
| External dependencies | π¦ gopkg.in/yaml.v3 |
β None found |
Library API
Core package
import "github.com/shepard-labs/go-toon/toon"
Types
toon.Nodeβ the ordered document model. A node can represent any TOON value.toon.Fieldβ a key-value pair within anObjectKindnode.toon.Numberβ stores the raw string representation of a number for lossless round-trips.toon.Kindβ the type of node (NullKind,BoolKind,NumberKind,StringKind,ArrayKind,ObjectKind).
Encoding
n := &toon.Node{
Kind: toon.ObjectKind,
Object: []toon.Field{
{Key: "id", Value: &toon.Node{Kind: toon.NumberKind, Number: toon.Number{Raw: "1"}}},
{Key: "user", Value: &toon.Node{
Kind: toon.ObjectKind,
Object: []toon.Field{
{Key: "name", Value: &toon.Node{Kind: toon.StringKind, String: "Ada"}},
{Key: "active", Value: &toon.Node{Kind: toon.BoolKind, Bool: true}},
},
}},
},
}
// Encode to bytes
data, err := toon.Encode(n)
// Encode to a writer
var buf bytes.Buffer
err = toon.EncodeToWriter(&buf, n)
// With custom indentation
data, err = toon.Encode(n, func(o *toon.EncodeOptions) {
o.IndentSize = 4
})
// With a tab or pipe document delimiter (declared in every array header)
data, err = toon.Encode(n, func(o *toon.EncodeOptions) {
o.Delimiter = toon.Pipe
})
Encoding returns an invalid_utf8 error when a string value or object key contains ill-formed UTF-8 (invalid bytes or unpaired surrogates).
Decoding
// Decode from bytes
n, err := toon.Decode([]byte("id: 1\nuser:\n name: Ada\n active: true"))
// Decode from a reader
n, err = toon.DecodeReader(strings.NewReader("id: 1"))
// Validate without decoding
err = toon.Validate([]byte("id: 1"))
// Dotted keys are always literal: "a.b: 1" decodes to the single key "a.b"
n, err = toon.Decode([]byte("a.b: 1"))
// With strict mode disabled (applies only the spec's non-strict recoveries)
n, err = toon.Decode([]byte("id: 1"), func(o *toon.DecodeOptions) {
o.Strict = false
})
// With resource limits
n, err = toon.Decode([]byte("id: 1"), func(o *toon.DecodeOptions) {
o.Limits = toon.ResourceLimits{
MaxDepth: 10,
MaxNodes: 1000,
MaxBytes: 1024,
MaxStringBytes: 1024,
MaxArrayLength: 100,
}
})
Error handling
err := toon.Validate([]byte("invalid"))
if err != nil {
// Extract the error code for branching without string matching
code := toon.CodeOf(err)
if code == toon.ErrDuplicateKey {
// handle duplicate key
}
// Or use errors.As
var toonErr *toon.Error
if errors.As(err, &toonErr) {
fmt.Println(toonErr.Line, toonErr.Column, toonErr.Code)
}
}
Tabular arrays
// Encoding tabular arrays
n := &toon.Node{
Kind: toon.ObjectKind,
Object: []toon.Field{
{Key: "items", Value: &toon.Node{
Kind: toon.ArrayKind,
Array: []*toon.Node{
{Kind: toon.ObjectKind, Object: []toon.Field{
{Key: "sku", Value: &toon.Node{Kind: toon.StringKind, String: "A1"}},
{Key: "qty", Value: &toon.Node{Kind: toon.NumberKind, Number: toon.Number{Raw: "2"}}},
{Key: "price", Value: &toon.Node{Kind: toon.NumberKind, Number: toon.Number{Raw: "9.9900"}}},
}},
{Kind: toon.ObjectKind, Object: []toon.Field{
{Key: "sku", Value: &toon.Node{Kind: toon.StringKind, String: "B2"}},
{Key: "qty", Value: &toon.Node{Kind: toon.NumberKind, Number: toon.Number{Raw: "1"}}},
{Key: "price", Value: &toon.Node{Kind: toon.NumberKind, Number: toon.Number{Raw: "14.5"}}},
}},
},
}},
},
}
data, _ := toon.Encode(n)
// Output: items[2]{sku,qty,price}:\n A1,2,9.99\n B2,1,14.5
Keyed tabular arrays
An object with two or more entries whose values are all non-empty objects with the same keys encodes as a keyed tabular array. Each entry row starts with the entry key:
users[2:]{age,city}:
alice: 30,Oslo
bob: 25,Rome
At the document root the header has no key:
[2:]{age,city}:
alice: 30,Oslo
bob: 25,Rome
Both forms decode back to ordered objects ({"alice": {...}, "bob": {...}}).
Nested field groups
Columns that hold uniform nested objects are written as a field group inside the header, and each row stays flat. Cells are listed depth-first:
users[2]{id,name,geo{lat,lng}}:
1,Ada,1.5,2.5
2,Linus,3,4
Groups nest further, for example rows[2]{id,a{b{c,d}}}:.
Comments
A line whose first non-space character is # is a comment. Decoders drop it before any other processing. The encoder never writes comments, and quotes strings that start with #.
# users
name: Ada
# trailing note
age: 36
List items
The first field of an object list item always sits on the hyphen line, and an empty object is a bare -:
items[4]:
- id: 1
tags[2]: a,b
- id: 2
- [2]: 1,2
- [0]:
Number modes
// Lossless (default) β preserves decimal tokens
n, _ := toon.Decode([]byte("pi: 3.141592653589793238462643383279"))
// n.Object[0].Value.Number.Raw == "3.141592653589793238462643383279"
// Out-of-range values decode as well: the decoder keeps the token text in Raw
n, _ = toon.Decode([]byte("big: 1e500"))
// n.Object[0].Value.Number.Raw == "1e500"
// Lossless encode (default) writes the canonical decimal token
data, _ := toon.Encode(&toon.Node{Kind: toon.NumberKind, Number: toon.Number{Raw: "1e500"}})
// Output: 1e+500
// Float64 β rounds through float64; values outside the float64 range return an error
data, err := toon.Encode(&toon.Node{Kind: toon.NumberKind, Number: toon.Number{Raw: "3.141592653589793238462643383279"}},
func(o *toon.EncodeOptions) { o.NumberMode = toon.NumberFloat64 })
// Output: 3.141592653589793
// String for unsafe values on encode
data, _ = toon.Encode(&toon.Node{Kind: toon.NumberKind, Number: toon.Number{Raw: "1e500"}},
func(o *toon.EncodeOptions) { o.NumberMode = toon.NumberStringForUnsafe })
// Output: "1e+500" (quoted string)
Utility functions
// Check if a string is a valid unquoted TOON key
toon.IsValidUnquotedKey("myField") // true
toon.IsValidUnquotedKey("123abc") // false
// Escape a string for TOON output
toon.EscapeString("hello\nworld") // "hello\\nworld"
// Check if a string needs quoting
toon.NeedsQuotes("hello", toon.Comma) // false
toon.NeedsQuotes("hello,world", toon.Comma) // true
// Parse a primitive token
n, _ := toon.ParsePrimitiveToken("42") // NumberKind with Raw="42"
n, _ = toon.ParsePrimitiveToken("true") // BoolKind with Bool=true
n, _ = toon.ParsePrimitiveToken("null") // NullKind
// Canonicalize a number token
toon.CanonicalizeNumberToken("1e3") // "1000"
toon.CanonicalizeNumberToken("1.200") // "1.2"
Error codes
const (
ErrInvalidIndent ErrorCode = "invalid_indent"
ErrInvalidUTF8 ErrorCode = "invalid_utf8"
ErrTabIndent ErrorCode = "tab_indent"
ErrInvalidEscape ErrorCode = "invalid_escape"
ErrUnterminatedString ErrorCode = "unterminated_string"
ErrArrayCountMismatch ErrorCode = "array_count_mismatch"
ErrTabularWidthMismatch ErrorCode = "tabular_width_mismatch"
ErrDuplicateKey ErrorCode = "duplicate_key"
ErrMalformedHeader ErrorCode = "malformed_header"
ErrHeaderDelimiterMismatch ErrorCode = "header_delimiter_mismatch"
ErrMissingColon ErrorCode = "missing_colon"
ErrPathExpansionConflict ErrorCode = "path_expansion_conflict" // no longer produced
ErrResourceLimit ErrorCode = "resource_limit"
ErrInvalidInputFormat ErrorCode = "invalid_input_format"
ErrUnsupportedFeature ErrorCode = "unsupported_feature"
ErrUnsupportedKind ErrorCode = "unsupported_kind"
ErrCyclicValue ErrorCode = "cyclic_value"
ErrUnmarshalType ErrorCode = "unmarshal_type"
ErrNonPointerTarget ErrorCode = "non_pointer_target"
)
Reflection package
import "github.com/shepard-labs/go-toon/toon/reflect"
Marshaling Go values to TOON
type User struct {
ID int `toon:"id"`
Name string `toon:"name"`
Tags []string `toon:"tags"`
Score float64 `toon:"score"`
}
// Marshal to TOON bytes via a writer
var buf bytes.Buffer
err := reflect.Marshal(&buf, User{ID: 42, Name: "Ada", Tags: []string{"a", "b"}, Score: 9.5})
// buf: "id: 42\nname: Ada\ntags[2]: a,b\nscore: 9.5"
// Slices of structs with nested structs encode as nested field groups
type Geo struct{ Lat, Lng float64 }
type Place struct {
ID int `toon:"id"`
Geo Geo `toon:"geo"`
}
buf.Reset()
err = reflect.Marshal(&buf, map[string]any{"places": []Place{{1, Geo{1, 2}}, {2, Geo{3, 4}}}})
// places[2]{id,geo{Lat,Lng}}:
// 1,1,2
// 2,3,4
// Marshal to a *toon.Node
node, err := reflect.NodeFromValue(User{ID: 42, Name: "Ada"})
// node.Kind == toon.ObjectKind
// With custom number mode for non-finite floats
node, err = reflect.NodeFromValue(math.NaN(), func(o *reflect.ValueOptions) {
o.NumberMode = toon.NumberStringForUnsafe
})
// node.Kind == toon.StringKind, node.String == "NaN"
// With resource limits
_, err = reflect.NodeFromValue([]int{1, 2, 3, 4, 5}, func(o *reflect.ValueOptions) {
o.Limits = toon.ResourceLimits{MaxArrayLength: 2}
})
// err.Code == toon.ErrResourceLimit
// Struct with skipped and unexported fields
type Record struct {
Name string `toon:"name"`
Optional string `toon:"optional,omitempty"` // skipped when empty
Skip string `toon:"-"` // skipped
hidden string // unexported β skipped
}
node, _ = reflect.NodeFromValue(Record{Name: "x", Skip: "y", hidden: "z"})
// node.Object = [{Key: "name", Value: String("x")}]
Custom time formatting
type Event struct {
At time.Time `toon:"at"`
}
var buf bytes.Buffer
err := reflect.Marshal(&buf, Event{At: time.Date(2025, 6, 4, 12, 30, 0, 0, time.UTC)}, func(o *reflect.Options) {
o.Value.TimeFormatter = func(t time.Time) string {
return t.UTC().Format("2006-01-02")
}
})
// buf: "at: 2025-06-04"
Unmarshaling TOON to Go values
// Unmarshal TOON bytes to a Go value
var u User
err := reflect.Unmarshal([]byte("id: 42\nname: Ada\ntags[2]: a,b\nscore: 9.5"), &u)
// u.ID == 42, u.Name == "Ada", u.Tags == []string{"a", "b"}, u.Score == 9.5
// Unmarshal from a reader
var u2 User
err = reflect.UnmarshalReader(strings.NewReader("id: 1\nname: Bob"), &u2)
// Unmarshal from an already-decoded node
n, _ := toon.Decode([]byte("name: Bob\nage: 30"))
type Person struct {
Name string `toon:"name"`
Age int `toon:"age"`
}
var p Person
err = reflect.UnmarshalNode(n, &p)
// p.Name == "Bob", p.Age == 30
// With strict mode disabled
err = reflect.Unmarshal([]byte("id: 1"), &u, func(o *reflect.UnmarshalOptions) {
o.Decode.Strict = false
})
// []byte is encoded as base64
var b []byte
n := &toon.Node{Kind: toon.StringKind, String: "SGVsbG8="}
err = reflect.NodeToValue(n, &b)
// b == []byte("Hello")
TextMarshaler / TextUnmarshaler
type Timestamp struct {
Time time.Time
}
func (t Timestamp) MarshalText() ([]byte, error) {
return []byte(t.Time.Format(time.RFC3339)), nil
}
func (t *Timestamp) UnmarshalText(b []byte) error {
var err error
t.Time, err = time.Parse(time.RFC3339, string(b))
return err
}
// MarshalText is honored on encode
node, _ := reflect.NodeFromValue(Timestamp{Time: time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)})
// node.Kind == toon.StringKind, node.String == "2025-01-01T00:00:00Z"
// UnmarshalText is honored on decode
var ts Timestamp
n := &toon.Node{Kind: toon.StringKind, String: "2025-06-04T12:00:00Z"}
reflect.NodeToValue(n, &ts)
// ts.Time == time.Date(2025, 6, 4, 12, 0, 0, 0, time.UTC)
Cycle detection
type ListNode struct {
Value int
Next *ListNode
}
a := &ListNode{Value: 1}
b := &ListNode{Value: 2, Next: a}
a.Next = b // creates a cycle
_, err := reflect.NodeFromValue(a)
// err.Code == toon.ErrCyclicValue
Formats package
import "github.com/shepard-labs/go-toon/formats"
Parsing JSON
// Parse JSON into a TOON node
n, err := formats.FromJSON(strings.NewReader(`{"name":"Ada","age":36,"active":true}`))
// n.Kind == toon.ObjectKind
// Allow duplicate keys (last value wins)
n, err = formats.FromJSON(strings.NewReader(`{"a":1,"a":2}`), func(o *formats.JSONOptions) {
o.AllowDuplicateKeys = true
})
// With resource limits
n, err = formats.FromJSON(strings.NewReader(`{"a":1}`), func(o *formats.JSONOptions) {
o.Limits = toon.ResourceLimits{MaxBytes: 1024}
})
Parsing YAML
// Parse YAML into a TOON node
n, err := formats.FromYAML(strings.NewReader(`name: Ada
age: 36
tags:
- a
- b`))
// Multiple documents β array
n, err = formats.FromYAML(strings.NewReader(`a: 1
---
b: 2`), func(o *formats.YAMLOptions) {
o.Documents = formats.YAMLDocumentsArray
})
// n.Kind == toon.ArrayKind, n.Array has 2 elements
// Force all scalars to strings
n, err = formats.FromYAML(strings.NewReader(`a: 1`), func(o *formats.YAMLOptions) {
o.Scalars = formats.YAMLScalarsString
})
// n.Object[0].Value.Kind == toon.StringKind (not NumberKind)
Parsing CSV
// With headers and type inference
n, err := formats.FromCSV(strings.NewReader(`id,name,zip
1,Ada,05
2,Bob,10`), func(o *formats.CSVOptions) {
o.HeaderMode = formats.CSVHeaderPresent
o.InferTypes = true
})
// n.Kind == toon.ArrayKind, n.Array[0] == {id: 1, name: "Ada", zip: "05"}
// Without headers, custom delimiter
n, err = formats.FromCSV(strings.NewReader(`1|true
2|false`), func(o *formats.CSVOptions) {
o.HeaderMode = formats.CSVHeaderAbsent
o.Delimiter = '|'
o.InferTypes = false
})
// n.Array[0] == {field1: "1", field2: "true"}
Parsing XML
// Parse XML into a TOON node
n, err := formats.FromXML(strings.NewReader(`<user id="1">Ada</user>`))
// n.Kind == toon.ObjectKind, n.Object = [{Key: "user", Value: { @id: 1, #text: "Ada" }}]
// Preserve mixed content
n, err = formats.FromXML(strings.NewReader(`<p>Hello <b>world</b>!</p>`), func(o *formats.XMLOptions) {
o.MixedContent = formats.XMLMixedContentPreserve
})
// n.Object = [{Key: "p", Value: [{#text: "Hello "}, {b: "world"}, {#text: "!"}]}]
// Namespace-qualified names
n, err = formats.FromXML(strings.NewReader(`<ns:root xmlns:ns="urn:x"><ns:a>1</ns:a></ns:root>`), func(o *formats.XMLOptions) {
o.Namespaces = formats.XMLNamespacesQualified
})
// n.Object = [{Key: "ns:root", Value: {ns:a: 1}}]
Serializing TOON to JSON
// Write a TOON node as ordered JSON
n, _ := formats.FromJSON(strings.NewReader(`{"z":1,"a":{"b":2}}`))
var out bytes.Buffer
formats.ToJSON(&out, n)
// out: {"z":1,"a":{"b":2}}
// With indentation
formats.ToJSON(&out, n, func(o *formats.JSONOutputOptions) {
o.Indent = " "
})
// out:
// {
// "z": 1,
// "a": {
// "b": 2
// }
// }
High-level format conversions
// JSON β TOON
var toonOut bytes.Buffer
formats.JSONToTOON(strings.NewReader(`{"a":1,"b":[2,3]}`), &toonOut, formats.JSONToTOONOptions{})
// toonOut: "a: 1\nb[2]: 2,3"
// TOON β JSON
var jsonOut bytes.Buffer
formats.TOONToJSON(strings.NewReader("a: 1"), &jsonOut,
formats.TOONToJSONOptions{JSON: formats.JSONOutputOptions{Indent: " "}})
// jsonOut: "{\n \"a\": 1\n}"
// YAML β TOON
var out bytes.Buffer
formats.YAMLToTOON(strings.NewReader("a: 1\nb: 2"), &out, formats.YAMLToTOONOptions{})
// out: "a: 1\nb: 2"
// XML β TOON
formats.XMLToTOON(strings.NewReader("<a>1</a>"), &out, formats.XMLToTOONOptions{})
// out: "a: 1"
// CSV β TOON
formats.CSVToTOON(strings.NewReader("a\n1\n"), &out,
formats.CSVToTOONOptions{CSV: formats.CSVOptions{HeaderMode: formats.CSVHeaderPresent, InferTypes: true}})
// out: "[1]{a}:\n 1"
CLI
Build:
go build ./cmd/toon
Encode external formats to TOON:
toon encode input.json
toon encode --input input.yaml --output output.toon
toon encode --format json < input.json
toon encode data.csv --csv-header present --csv-infer-types=true
toon encode data.tsv
Decode TOON to ordered JSON:
toon decode input.toon
toon decode input.toon --indent 4
toon decode input.toon --strict=false
Validate TOON:
toon validate input.toon
Important encode flags:
--format json|yaml|xml|csv|auto--delimiter comma|tab|pipe--indent N--stats--csv-header auto|present|absent--csv-delimiter CHAR--csv-infer-types true|false--csv-root-key NAME--yaml-docs error|array--yaml-scalars core|string--xml-attr-prefix @--xml-text-key '#text'--xml-infer-types true|false--xml-mixed-content compact|preserve--xml-namespaces local|qualified|uri
Defaults And Security
- TOON decode strict mode is enabled by default, including when
formats.TOONToJSONOptions.Decodesets other fields such asLimits; setformats.TOONToJSONOptions.NonStrictto turn it off. - Strict and non-strict decode both reject ill-formed UTF-8 (
invalid_utf8); non-strict mode applies only the five recoveries listed in SPEC Β§14.4. - Dotted keys are always literal; there is no key folding or path expansion.
- Number handling defaults to lossless raw tokens.
- Out-of-range numbers (for example
1e500) decode with their token text kept inNumber.Raw; the encoder's number mode decides how they are written (see Number Modes). - Encoding a string value or object key that contains ill-formed UTF-8 returns an
invalid_utf8error. #comment lines are dropped by the decoder; the encoder never emits comments.- JSON input containing a lone surrogate escape is rejected instead of being replaced with U+FFFD.
- JSON duplicate keys are rejected by default.
- CSV library callers must choose header-present or header-absent mode explicitly; the CLI treats
autoas header-present for normal CSV workflows. - XML DTDs are rejected and external entity processing is not enabled.
- YAML aliases are resolved through the YAML AST with cycle rejection.
- Resource limits are configurable for input bytes, string bytes, node count, depth, and array length.
- CLI safe defaults set
MaxDepthto512andMaxStringBytesto64 MiB. toon/reflectdecode requires a non-nil pointer destination (toon.ErrNonPointerTargetotherwise).toon/reflectcycles on encode are rejected withtoon.ErrCyclicValue; type mismatches on decode surface astoon.ErrUnmarshalType.toon/reflect[]byte(and named[]bytetypes) marshal as base64 strings and unmarshal from base64 strings.toon/reflectTextMarshaleris honored on encode;TextUnmarshaleris honored on decode as an escape hatch.toon/reflectdecode propagatestoon.Decodeerrors (e.g.,ErrInvalidIndent,ErrDuplicateKey) with their original code viatoon.CodeOf.
Deprecated Options
These fields remain on the option structs and are ignored by the encoder and decoder: EncodeOptions.IncludeLengthMarkers, EncodeOptions.KeyFolding, EncodeOptions.FlattenDepth, and DecodeOptions.ExpandPaths. The CLI has no flags for them, and neither do the MCP tools.
Number Modes
Decoding never converts numbers: every numeric token keeps its text in toon.Number.Raw, including values outside the float64 range such as 1e500 or 1e-400, in strict and non-strict mode.
Encoding follows EncodeOptions.NumberMode:
| Mode | Behavior |
|---|---|
toon.NumberLossless (default) |
Writes the canonical decimal token without float64 conversion (1.200 β 1.2, 1e500 β 1e+500). |
toon.NumberFloat64 |
Rounds through float64 (12345678901234567890 β 12345678901234567000). Values outside the float64 range return an error. |
toon.NumberStringForUnsafe |
Writes numbers outside the float64 range as quoted strings ("1e+500", "1e-400"). |
Numbers outside [1e-6, 1e21) are written in exponent form (1e-7, 1e+21); all others are plain decimals.
Patch releases must not change canonical output except to fix spec-compliance bugs.
Format Normalization
JSON:
- Uses
json.Decoder.Token()withUseNumber(). - Preserves object order.
- Rejects duplicate keys unless explicitly allowed, in which case deterministic last-write-wins applies.
- Rejects lone surrogate escapes.
YAML:
- Uses
gopkg.in/yaml.v3AST to preserve mapping order. - Multiple documents error by default or become an array when requested.
- Core scalars become typed primitives; timestamps remain strings.
- Merge keys are resolved deterministically.
- Unknown tags and non-string keys are stringified.
CSV:
- Header-present rows become arrays of objects keyed by header names.
- Header-absent rows use generated
field1,field2, ... keys. - Empty cells remain empty strings.
- Type inference is configurable.
- Numeric-looking cells with forbidden leading zeros remain strings.
XML:
- Elements become object fields keyed by element name.
- Attributes use
@by default. - Text with attributes or children uses
#textby default. - Repeated child elements become arrays.
- Comments and processing instructions are ignored.
- Mixed content defaults to compact mode; preserve mode keeps exact text/child order as an array.
- Namespace handling supports local, qualified, and URI-expanded modes.
Compatibility And Versioning
This module follows semantic versioning.
- Patch releases fix bugs and spec-compliance issues without intentional API breaks.
- Minor releases may add options and non-breaking APIs.
- Major releases may track breaking TOON spec changes or public API changes.
The ordered toon.Node model is the stable core API. Go maps are not used as the primary representation for ordered documents.
Release Verification
Recommended release checks:
go test ./...
go test -race ./...
go test ./toon -run=Fuzz -fuzz=FuzzDecode -fuzztime=10s
go test ./toon/reflect -run=Fuzz -fuzz=FuzzMarshalValue -fuzztime=10s
go test ./toon/reflect -run=Fuzz -fuzz=FuzzUnmarshalValue -fuzztime=10s
go test ./... -bench=. -run='^$'
go test ./toon also runs the vendored TOON v4.4 spec fixtures in toon/testdata/spec/. Re-vendor them with scripts/sync-spec-fixtures.sh [tag] (pinned to v4.4.0).
Supported build-check targets for cmd/toon:
linux/amd64linux/arm64darwin/amd64darwin/arm64windows/amd64
Benchmarks
Latest local benchmark snapshot, run with Go 1.26 on Darwin ARM64, Apple M4 Max:
go test ./toon ./formats -bench=. -run='^$' -benchmem -count=5
| Benchmark | Time/op | Bytes/op | Allocs/op |
|---|---|---|---|
BenchmarkEncodeLargeOrderedObject |
48-49 us | 33 KB | 110 |
BenchmarkEncodeLargeTabularArray |
91-94 us | 66 KB | 113 |
BenchmarkDecodeRepresentativeTOON |
192-204 us | 565 KB | 6031 |
BenchmarkLargeJSONInputNormalization |
197-201 us | 772 KB | 7033 |
BenchmarkLargeCSVInputNormalization |
294-297 us | 982 KB | 12026 |
BenchmarkJSONOutputOrderedNode |
88-92 us | 188 KB | 122 |
BenchmarkWideJSONObjectNormalization |
89-91 us | 329 KB | 1050 |
BenchmarkWideCSVHeaderNormalization |
147 us | 557 KB | 2085 |
BenchmarkMarshalSmallStruct (toon/reflect) |
~3.0 us | 4.5 KB | 62 |
BenchmarkUnmarshalSmallStruct (toon/reflect) |
~3.3 us | 5.3 KB | 50 |
BenchmarkMarshalLargeMap (toon/reflect, 1k keys) |
~333 us | 305 KB | 6122 |
BenchmarkUnmarshalLargeArray (toon/reflect, 256 ints) |
~21 us | 42 KB | 280 |
BenchmarkNodeFromValue (toon/reflect) |
~2.0 us | 3.5 KB | 48 |
BenchmarkNodeToValue (toon/reflect) |
~1.5 us | 0.8 KB | 9 |
JSON normalization uses a purpose-built ordered scanner to preserve object order, detect duplicate keys, and retain lossless number tokens without routing through Go maps.
Directories
ΒΆ
| Path | Synopsis |
|---|---|
|
cmd
|
|
|
toon
command
|
|
|
mcp
|
|
|
go-toon-mcp
command
|
|
|
reflect
Package reflect provides a reflection-based convenience layer above the toon.Node ordered document model.
|
Package reflect provides a reflection-based convenience layer above the toon.Node ordered document model. |