interpres
A TOML 1.0 parser and encoder for Go, written with the standard library alone.
interpres (Latin for interpreter) gives zero-dependency programs an
encoding/json-style API for reading and writing TOML, and passes the entire
official toml-test suite: 185 valid
and 371 invalid cases, zero failures.
Features
- Full TOML 1.0: bare, quoted and dotted keys; tables and arrays of tables;
basic and literal strings including multiline; integers in the four radixes
with
_separators; floats with exponents,infandnan; booleans; the four date-time kinds; arrays and inline tables. - Decoding and encoding:
Parsefor an untyped tree,UnmarshalandMarshalfor structs and maps, mirroringencoding/json. - Strict decoding:
NewDecoder().DisallowUnknownFields()rejects keys that match no destination field, at every struct depth. - Custom types:
MarshalerandUnmarshalerlet a type control its own TOML representation in both directions. - Cancellation: every entry point has a
*Contextsibling that honours acontext.Context. - Configurable emission:
Encoderoptions for declaration-order output, omitting empty arrays, and literal multiline strings.
Install
As a library:
go get sourcedock.dev/petrbalvin/interpres
Requires Go 1.27.0 or newer. The module imports only the standard library.
Quick start
git clone https://sourcedock.dev/petrbalvin/interpres.git
cd interpres
just example
just example runs the tour in examples/basic: it decodes an embedded
document into a struct, prints it, and re-encodes it under both Encoder
layouts.
Usage
Decode into a struct
type Config struct {
Title string `toml:"title"`
Server struct {
Host string `toml:"host"`
Port int `toml:"port"`
} `toml:"server"`
}
var cfg Config
err := interpres.Unmarshal(data, &cfg)
Fields match by the toml:"name" tag, or by the lower-cased field name when no
tag is present; toml:"-" skips a field. Parse returns the untyped
map[string]any tree instead, and UnmarshalContext accepts a context.
Encode from a struct
out, err := interpres.Marshal(cfg)
writes:
title = "example"
[server]
host = "127.0.0.1"
port = 9090
Tables are laid out scalars first, then sub-tables, then arrays of tables, which is the layout that re-parses to the same tree.
Strict decoding
err := interpres.NewDecoder().
DisallowUnknownFields().
Decode(data, &cfg)
A key with no matching field becomes an error instead of a silent drop.
Custom types
type Port int
func (p Port) MarshalTOML() (any, error) {
return int64(p), nil
}
type IP struct{ net.IP }
func (ip *IP) UnmarshalTOML(data any) error {
s, ok := data.(string)
if !ok {
return fmt.Errorf("ip: not a string")
}
ip.IP = net.ParseIP(s)
return nil
}
The value MarshalTOML returns is encoded in place of the receiver;
UnmarshalTOML receives the parsed value verbatim.
Encoder options
out, err := interpres.NewEncoder().
GroupByKind(false). // preserve declaration order
OmitEmptyArrays(). // skip empty scalar arrays
UseLiteralMultiline(80). // long multi-line strings as literal blocks
Marshal(cfg)
Cancellation
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
out, err := interpres.MarshalContext(ctx, cfg)
ParseContext, UnmarshalContext, (*Decoder).DecodeContext and
(*Encoder).MarshalContext follow the same pattern.
The full rules for field matching, numeric conversion and emission live in docs/API.md.
Development
just build # compile bin/interpres-decode
just test # the suite, with the 80 percent coverage floor
just toml-test # the official compliance suite, needs toml-test on PATH
just fmt # gofmt in place
See docs/DEVELOPMENT.md for the full workflow, and CONTRIBUTING.md for how to contribute.
Documentation
- docs/ARCHITECTURE.md: components and data flow
- docs/API.md: the API reference, decoding and encoding rules
- docs/CLI.md: the interpres-decode toml-test adapter
Licence
MIT. See LICENSE.
Copyright © 2026 Petr Balvín