A TOML library for Go that preserves comments, formatting, and whitespace on round-trip.
Most TOML libraries parse into a map or struct and discard everything else.
Tommy keeps a concrete syntax tree (CST) so that reading a config, modifying a
value, and writing it back produces minimal diffs --- comments stay where they
were, blank lines don't move, and whitespace around = signs is untouched.
- Round-trip fidelity --- parse and serialize without losing comments or formatting
- Code generator --- annotate a struct with
//go:generate tommy generateand get type-safeDecode/Encodemethods with undecoded-key detection - Reflection-based marshal ---
UnmarshalDocument/MarshalDocumentfor quick use without codegen - Document API --- read and write values by dotted key path while preserving the surrounding document
- Formatter ---
tommy fmtnormalizes whitespace, comment spacing, and blank lines
go install github.com/amarbel-llc/tommy/cmd/tommy@latestOr with Nix:
nix build github:amarbel-llc/tommyTommy parses TOML into a concrete syntax tree where comments, whitespace, and blank lines are first-class nodes --- not discarded during parsing. When you modify a value through any of Tommy's APIs, only the value node in the tree is updated. Everything else --- comments above, inline comments after values, blank line separators between sections --- stays exactly where it was.
input.toml after Encode()
───────────── ──────────────
# Server configuration # Server configuration
[server] [server]
port = 8080 # default port port = 9090 # default port
host = "localhost" host = "localhost"
This is automatic. There is no flag to enable or annotation to add --- every path through Tommy (codegen, reflection marshal, document API) preserves the full document structure. The only thing that changes in the output is the value you changed.
The recommended way to use Tommy is through its code generator. Add a
//go:generate tommy generate directive above your struct and you get type-safe
Decode/Encode methods that handle all of this transparently --- you just
read and write normal Go struct fields. No CST manipulation, no special APIs, no
awareness of the preservation machinery needed.
The code generator is not yet fully type-exhaustive --- support for additional Go type patterns is being added as needed. See the open issues for what's planned and in progress.
This matters for config files that humans maintain: version-controlled TOML with explanatory comments, hand-tuned formatting, or sections separated by blank lines. A programmatic update to one field should not rewrite the entire file.
Annotate your struct and run go generate:
//go:generate tommy generate
type Config struct {
Title string `toml:"title"`
Port int `toml:"port"`
Debug bool `toml:"debug,omitempty"`
}This produces a config_tommy.go file with:
func DecodeConfig(input []byte) (*ConfigDocument, error)
func (d *ConfigDocument) Data() *Config
func (d *ConfigDocument) Encode() ([]byte, error)
func (d *ConfigDocument) Undecoded() []stringUsage:
doc, err := DecodeConfig(input)
cfg := doc.Data()
cfg.Port = 9090
output, err := doc.Encode()
// output preserves all comments and formatting from inputFor simpler cases without code generation:
import "github.com/amarbel-llc/tommy/pkg/marshal"
var cfg Config
handle, err := marshal.UnmarshalDocument(input, &cfg)
cfg.Port = 9090
output, err := marshal.MarshalDocument(handle, &cfg) For direct key-value manipulation:
import "github.com/amarbel-llc/tommy/pkg/document"
doc, err := document.Parse(input)
port, err := document.Get[int](doc, "server.port")
err = doc.Set("server.port", 9090)
output := doc.Bytes()The code generator handles:
Type TOML Representation
string, int, int64, float64, bool Scalar values
*string, *int, *bool, etc. Optional scalars
Nested structs [table] sections
*Struct [table] or flat dotted keys
Cross-package structs [table] via delegation
[]int, []string Arrays
[]Struct [[array-of-tables]]
map[string]string [table] with string values
map[string]Struct Sub-tables ([parent.key])
TOMLMarshaler/TOMLUnmarshaler Custom marshal via any
TextMarshaler/TextUnmarshaler Custom marshal via string
When a struct field references a type from another package, the generated code
delegates to that package's DecodeInto/EncodeFrom functions instead of
inlining field-by-field decoding. This means the external package must also use
//go:generate tommy generate on its structs.
// In package "options":
//go:generate tommy generate
type PrintOptions struct {
Abbreviations *abbreviations `toml:"abbreviations"`
PrintColors *bool `toml:"print-colors"`
}
// In your package:
//go:generate tommy generate
type Config struct {
Name string `toml:"name"`
PrintOptions options.PrintOptions `toml:"cli-output"`
}This delegation enables cross-package structs that contain unexported types --- the external package handles its own internals, and the consumer just delegates.
If your struct implements Validate() error, the generated Decode and
Encode methods call it automatically. Decode validates after all fields are
set; Encode validates before writing to the CST.
//go:generate tommy generate
type Config struct {
Port int `toml:"port"`
Name string `toml:"name"`
}
func (c Config) Validate() error {
if c.Port < 1 || c.Port > 65535 {
return fmt.Errorf("port must be 1-65535, got %d", c.Port)
}
return nil
}No interface import is required --- just add the method and re-run
go generate. This also works with newtypes to validate individual values while
preserving their native TOML types (no string coercion).
`toml:"key"` // required — maps field to TOML key
`toml:"key,omitempty"` // omit zero-value fields on encode
`toml:"key,multiline"` // use """ multiline string syntax
`toml:"-"` // skip this field# Format TOML files in-place
tommy fmt config.toml settings.toml
# Check formatting without modifying (exits non-zero if unformatted)
tommy fmt --check config.toml
# Format from stdin
cat config.toml | tommy fmt -
# Generate code (typically via go generate, not called directly)
tommy generateMIT