A Go library for parsing, modifying, and serializing TOML documents while preserving all comments, formatting, and declaration order. It wraps the excellent creachadair/tomledit library to provide a stable, user-friendly API.
- ✅ Comment Preservation: All comments (block, inline, and trailing) are preserved during read-modify-write operations
- ✅ Format Preservation: Original formatting and whitespace are maintained
- ✅ Order Preservation: Declaration order of keys and sections is preserved
- ✅ Quote Style Preservation: String quote styles (single
', double", multiline"""or''') are preserved when modifying values - ✅ Nestedness Style Preservation: Dotted keys (
server.host = "...") vs section style ([server]+host = "...") are preserved - ✅ Simple API: Easy-to-use interface with
Get,Set,Delete, andHasmethods - ✅ Dotted Path Support: Access nested values using dotted paths (e.g.,
server.database.host) - ✅ Quoted Key Support: Full support for TOML quoted keys with special characters (e.g.,
aliases.".",section."key with spaces") - ✅ Path Validation: Validates paths according to TOML specification, rejecting invalid paths like
aliases.(trailing dot) - ✅ Full TOML v1.0.0 Support: Supports all TOML features including arrays, inline tables, and multiline strings
- ✅ Extensively Tested: Comprehensive test suite with 39 test cases covering all edge cases
go get github.com/neongreen/mono/lib/tomlpackage main
import (
"fmt"
"log"
"github.com/neongreen/mono/lib/toml"
)
func main() {
// Parse a TOML document
input := `# Server configuration
[server]
host = "localhost" # The host to bind to
port = 8080 # The port to listen on
[database]
url = "postgres://localhost/db"
`
doc, err := toml.ParseString(input)
if err != nil {
log.Fatal(err)
}
// Read values
host, _ := doc.Get("server.host")
fmt.Println("Host:", host) // Output: Host: localhost
// Modify values (comments are preserved!)
doc.Set("server.port", 9090)
doc.Set("server.debug", true)
// Delete values
doc.Delete("database.url")
// Check if a value exists
if doc.Has("server.host") {
fmt.Println("Host is configured")
}
// Write back to TOML (with all comments preserved)
fmt.Println(doc.String())
}Parses a TOML document from a byte slice.
data, _ := os.ReadFile("config.toml")
doc, err := toml.Parse(data)Parses a TOML document from a string.
doc, err := toml.ParseString(`
name = "myapp"
version = 1
`)Retrieves a value at the given dotted path. Returns nil if the path doesn't exist.
value, err := doc.Get("server.port")
if err != nil {
log.Fatal(err)
}
fmt.Printf("Port: %v\n", value)Supported return types:
string- for string valuesint64- for integer valuesfloat64- for floating-point valuesbool- for boolean values[]interface{}- for arraysmap[string]interface{}- for inline tables
Returns true if the given path exists in the document.
if doc.Has("server.port") {
fmt.Println("Port is configured")
}Sets a value at the given dotted path, creating intermediate sections if necessary. If the key already exists, its comments are preserved.
Supported value types:
string,int,int64,float64,bool[]interface{},[]string,[]intmap[string]interface{}
// Set a simple value
doc.Set("server.port", 8080)
// Set a nested value (creates [server] section if needed)
doc.Set("server.tls.enabled", true)
// Set an array
doc.Set("server.hosts", []string{"localhost", "example.com"})
// Set an inline table
doc.Set("person", map[string]interface{}{
"name": "Alice",
"age": 30,
})Removes a key at the given dotted path.
doc.Delete("server.debug")Serializes the document back to TOML format as a string, preserving all comments and formatting.
output := doc.String()
fmt.Println(output)Serializes the document back to TOML format as a byte slice.
data := doc.Bytes()
os.WriteFile("config.toml", data, 0644)package main
import (
"fmt"
"log"
"os"
"github.com/neongreen/mono/lib/toml"
)
func main() {
// Read existing config
data, err := os.ReadFile("app.toml")
if err != nil {
log.Fatal(err)
}
doc, err := toml.Parse(data)
if err != nil {
log.Fatal(err)
}
// Update configuration
doc.Set("app.version", "2.0.0")
doc.Set("app.debug", false)
doc.Set("server.max_connections", 1000)
// Save back to file (comments preserved!)
err = os.WriteFile("app.toml", doc.Bytes(), 0644)
if err != nil {
log.Fatal(err)
}
fmt.Println("Configuration updated successfully")
}package main
import (
"fmt"
"github.com/neongreen/mono/lib/toml"
)
func main() {
input := `# Application settings
app_name = "myapp" # The name of the application
version = 1 # Current version
license = 'MIT' # Single-quoted string
# Database configuration
[database]
# Connection settings
host = "localhost"
port = 5432
`
doc, _ := toml.ParseString(input)
// Modify values - comments AND quote styles are preserved!
doc.Set("version", 2) // Comment preserved
doc.Set("license", "Apache-2.0") // Single quotes preserved
doc.Set("database.port", 5433) // Comment preserved
fmt.Println(doc.String())
// Output:
// # Application settings
// app_name = "myapp" # The name of the application
// version = 2 # Current version
// license = 'Apache-2.0' # Single-quoted string (note: STILL single-quoted!)
//
// # Database configuration
// [database]
// # Connection settings
// host = "localhost"
// port = 5433
}package main
import (
"fmt"
"github.com/neongreen/mono/lib/toml"
)
func main() {
doc, _ := toml.ParseString("")
// Create a complex nested structure
doc.Set("servers.alpha.ip", "10.0.0.1")
doc.Set("servers.alpha.dc", "eqdc10")
doc.Set("servers.beta.ip", "10.0.0.2")
doc.Set("servers.beta.dc", "eqdc10")
// Set arrays
doc.Set("database.ports", []int{8001, 8002, 8003})
// Set inline tables
doc.Set("owner", map[string]interface{}{
"name": "Tom",
"email": "tom@example.com",
})
fmt.Println(doc.String())
}package main
import (
"fmt"
"github.com/neongreen/mono/lib/toml"
)
func main() {
// Example with dotted key style
input1 := `
server.host = "localhost"
server.port = 8080
`
doc1, _ := toml.ParseString(input1)
doc1.Set("server.host", "0.0.0.0") // Stays as dotted key!
fmt.Println(doc1.String())
// Output:
// server.host = "0.0.0.0"
// server.port = 8080
// Example with section style
input2 := `
[server]
host = "localhost"
port = 8080
`
doc2, _ := toml.ParseString(input2)
doc2.Set("server.host", "0.0.0.0") // Stays in [server] section!
fmt.Println(doc2.String())
// Output:
// [server]
// host = "0.0.0.0"
// port = 8080
}package main
import (
"fmt"
"github.com/neongreen/mono/lib/toml"
)
func main() {
input := `
[old_section]
setting = "value"
[server]
host = "localhost"
`
doc, _ := toml.ParseString(input)
// Migrate old setting to new location
if oldValue, _ := doc.Get("old_section.setting"); oldValue != nil {
doc.Set("server.setting", oldValue)
doc.Delete("old_section.setting")
}
fmt.Println(doc.String())
}TOML allows special characters in keys when they are quoted. This is useful for tools like jj (Jujutsu VCS) that use single-character aliases.
package main
import (
"fmt"
"github.com/neongreen/mono/lib/toml"
)
func main() {
// Example: jj-style aliases with quoted single-character keys
input := `
[aliases]
"." = "status"
".." = "show @-"
"..." = "show @--"
l = "log"
`
doc, _ := toml.ParseString(input)
// Get quoted keys - use backslash escaping or raw strings
status, _ := doc.Get(`aliases."."`)
fmt.Println("Alias '.':", status) // Output: Alias '.': status
showParent, _ := doc.Get(`aliases.".."`)
fmt.Println("Alias '..':", showParent) // Output: Alias '..': show @-
// Set new quoted keys
doc.Set(`aliases."!!!!"`, "diff")
// Regular keys work as usual
log, _ := doc.Get("aliases.l")
fmt.Println("Alias 'l':", log) // Output: Alias 'l': log
// Invalid paths are rejected
_, err := doc.Get("aliases.") // trailing dot
fmt.Println("Invalid path error:", err != nil) // Output: Invalid path error: true
fmt.Println(doc.String())
// Output:
// [aliases]
// "." = "status"
// ".." = "show @-"
// "..." = "show @--"
// l = "log"
// "!!!!" = "diff"
}Path Validation:
The library uses TOML-compliant path parsing that:
- ✅ Accepts:
aliases.".",section."key with spaces",config."key-with-dashes" - ❌ Rejects:
aliases.(trailing dot),.aliases(leading dot),aliases..key(double dot)
doc, _ := toml.ParseString(`
tags = ["go", "toml", "parser"]
`)
tags, _ := doc.Get("tags")
if arr, ok := tags.([]interface{}); ok {
for i, tag := range arr {
fmt.Printf("Tag %d: %v\n", i, tag)
}
}doc, _ := toml.ParseString(`
person = { name = "Alice", age = 30 }
`)
person, _ := doc.Get("person")
if table, ok := person.(map[string]interface{}); ok {
fmt.Println("Name:", table["name"])
fmt.Println("Age:", table["age"])
}When retrieving values, you'll need to assert the type:
// String
if str, ok := value.(string); ok {
fmt.Println(str)
}
// Integer (always returned as int64)
if num, ok := value.(int64); ok {
fmt.Println(num)
}
// Float
if f, ok := value.(float64); ok {
fmt.Println(f)
}
// Boolean
if b, ok := value.(bool); ok {
fmt.Println(b)
}The library includes a comprehensive test suite with 39 test cases covering:
- Parsing various TOML formats
- Getting and setting values
- Deleting values
- Comment preservation (block, inline, trailing)
- Quote style preservation (single, double, multiline)
- Key order preservation
- Nestedness style preservation (dotted keys vs sections)
- Round-trip parsing and serialization
- Arrays and inline tables
- Array of tables (
[[name]]syntax) - Array of inline tables with formatting preservation
- Quoted keys (
"key with spaces",[foo."bar:baz".qux]) - Malformed TOML detection and error handling
- Edge cases and special syntax
- Unicode support
- Large and complex documents
- Multiple modifications and deletions
- Concurrent access
Run tests with:
go test -v ./...See TEST_COVERAGE.md for detailed test coverage documentation.
| Feature | toml | go-toml/v2 | BurntSushi/toml |
|---|---|---|---|
| Comment preservation | ✅ | ❌ | ❌ |
| Format preservation | ✅ | ❌ | ❌ |
| Order preservation | ✅ | ❌ | ❌ |
| Quote style preservation | ✅ | ❌ | ❌ |
| Nestedness preservation | ✅ | ❌ | ❌ |
| Marshal/Unmarshal | ❌ | ✅ | ✅ |
| Struct tags | ❌ | ✅ | ✅ |
| Use case | Config editing | Data serialization | Data serialization |
When to use toml:
- You need to edit TOML files while preserving comments
- You're building a configuration management tool
- You need to maintain human-readable formatting (quote styles, key order, nestedness style)
- You want to programmatically update config files without losing documentation
- You need to respect the original author's formatting choices
When to use go-toml/v2 or BurntSushi/toml:
- You just need to deserialize TOML into Go structs
- Comments and formatting don't matter
- You need the fastest parsing performance
toml is built on top of creachadair/tomledit, which provides the low-level AST-based parsing and formatting capabilities. toml adds:
- A higher-level, more ergonomic API
- Automatic comment preservation when updating values
- Simplified path-based access to nested values
- Type conversion helpers
- The library works at the AST level, not the semantic level. It preserves the syntactic structure but doesn't validate semantic constraints (like duplicate keys).
- When creating new values programmatically, they're added without comments unless you modify the underlying AST.
- Marshal/unmarshal to Go structs is not supported (use go-toml/v2 or BurntSushi/toml for that).
This project is licensed under the MIT License.
Contributions are welcome! Please feel free to submit issues or pull requests.
- creachadair/tomledit - The underlying TOML parser and formatter
- TOML - The TOML specification