Standard library · Encoding and data

encoding/toml

Reading and writing TOML configuration files.

import encoding.toml

TOML is the configuration format of [section] tables and key = value lines used by Cargo.toml, pyproject.toml and many server configs. This package reads TOML text into a dynamic Value and writes a Value back as TOML. It has five functions: DecodeStrict, Decode and Valid for reading, EncodeStrict and Encode for writing.

Pick TOML for files people edit by hand. For data that programs exchange use encoding/json; for YAML files such as CI or Kubernetes configs use encoding/yaml; for KEY=value secrets files use encoding/envfile. If you only want settings loaded from a file with defaults and environment overrides, config may already do the job.

How TOML maps to a Value

TOMLValue
[table], [a.b], inline { k = v }, dotted keys a.b = 1object (nested)
[[name]]array of objects
"basic" with escapes, 'literal'string
42, -7, 1_000, 0x1F, 0o17, 0b101int (64-bit)
3.5, 1e6, inf, nanfloat
true, falsebool
dates and times such as 1979-05-27T07:32:00Zstring, unchanged

What trips people up

  • Use DecodeStrict, not Decode, on files people edit. Decode never reports an error: on broken input it returns whatever it read before the mistake, so a typo silently drops the rest of the file.
  • The parser is lenient. It checks syntax, not every rule of the TOML specification. A key or [table] defined twice is accepted (the tables merge, the later key wins), and an unquoted word such as mode = fast is read as the string "fast" instead of being rejected.
  • Some syntax is missing. Multi-line strings in triple quotes (""" or ''') make the document invalid, and the 8-digit \U escape is not decoded. Dates stay strings; parse them with time if you need them.
  • Writing is not formatting. Encode writes keys in no fixed order, drops keys whose value is null, and writes an array of objects as an inline array [{...}, {...}] rather than [[table]] blocks. The result is valid TOML that reads back to the same data, but comments and layout of an original file are lost.

Guide: read a project manifest

Project files such as Cargo.toml or pyproject.toml mix plain tables, inline tables and arrays of tables. DecodeStrict reads the whole file into a Value and tells you through ok whether it was valid; from there the value functions walk it.

encoding-toml-guide-manifest.salam
import encoding.toml
import encoding.value
import sort

func main:
    text := `[package]
name = "shop"
version = "1.4.0"
authors = ["Sara", "Reza"]

[dependencies]
http = "2.1"
json = { version = "1.0", optional = true }
log = "0.9"

[[bin]]
name = "shop"
path = "src/main.salam"

[[bin]]
name = "shop-admin"
path = "src/admin.salam"
`
    mut ok := false
    doc := toml.DecodeStrict(text, ok)
    if not ok:
        println "not valid TOML"
        ret 1
    end
    pkg := value.ObjectGet(doc, "package")
    println value.AsString(value.ObjectGet(pkg, "name")), value.AsString(value.ObjectGet(pkg, "version"))
    println "authors:", value.ArrayLen(value.ObjectGet(pkg, "authors"))

    deps := value.ObjectGet(doc, "dependencies")
    names := value.ObjectKeys(deps)
    sort.QuickSort(names)
    each n in names:
        d := value.ObjectGet(deps, n)
        if value.IsString(d):
            println "dep", n, value.AsString(d)
        else:
            println "dep", n, value.AsString(value.ObjectGet(d, "version")), "(optional)"
        end
    end

    bins := value.ObjectGet(doc, "bin")
    repeat value.ArrayLen(bins) in i:
        b := value.ArrayGet(bins, i)
        println "binary", value.AsString(value.ObjectGet(b, "name")), "from", value.AsString(value.ObjectGet(b, "path"))
    end
end
Outputshop 1.4.0 authors: 2 dep http 2.1 dep json 1.0 (optional) dep log 0.9 binary shop from src/main.salam binary shop-admin from src/admin.salam

A dependency can be a plain version string or an inline table with more detail, so the program asks IsString before reading it. Objects do not keep the order of their keys; sorting ObjectKeys with sort.QuickSort gives a stable listing.

Guide: TOML into a struct

For settings you use all over a program, load them into a struct once. Encode the decoded value as JSON with json.EncodeDefault and let json.UnmarshalLenient fill the struct: keys the file leaves out keep the struct's defaults, and a [server] table fills a nested struct.

encoding-toml-guide-struct.salam
import encoding.toml
import encoding.json
import encoding.encerr

struct Server:
    pub host: str = "127.0.0.1"
    pub port: int = 8000
end

struct Settings:
    pub title: str = ""
    pub debug: bool = false
    pub server: Server = Server {}
    pub allowed: Vector<str> = Vector {}
end

func main:
    text := `title = "Shop admin"
allowed = ["10.0.0.0/8", "192.168.1.0/24"]

[server]
port = 9090
`
    mut ok := false
    doc := toml.DecodeStrict(text, ok)
    mut s := Settings {}
    mut e := encerr.NoError()
    if not ok or not json.UnmarshalLenient(json.EncodeDefault(doc), s, e):
        println "bad settings"
        ret 1
    end
    println s.title, "debug:", s.debug
    println "listen on", s.server.host, s.server.port
    println "allowed networks:", s.allowed.len(), s.allowed.get(0)
end
OutputShop admin debug: false listen on 127.0.0.1 9090 allowed networks: 2 10.0.0.0/8

Guide: write a TOML file

To write TOML, build a Value object and pass it to Encode or EncodeStrict. A nested object becomes a [table]. The top level must be an object, since a TOML document is always a table; EncodeStrict reports false for anything else.

encoding-toml-guide-write.salam
import encoding.toml
import encoding.value

func replica(host: str, weight: int): value.Value:
    mut r := value.NewValueObject()
    value.ObjectSet(r, "host", value.ValueString(host))
    value.ObjectSet(r, "weight", value.ValueInt(weight))
    ret r
end

func main:
    mut db := value.NewValueObject()
    value.ObjectSet(db, "url", value.ValueString("postgres://localhost/shop"))
    value.ObjectSet(db, "pool", value.ValueInt(8))
    value.ObjectSet(db, "timeout", value.ValueFloat(30.0))
    value.ObjectSet(db, "password", value.ValueNull())

    mut replicas := value.NewValueArray()
    value.ArrayPush(replicas, replica("10.0.0.2", 2))
    value.ArrayPush(replicas, replica("10.0.0.3", 1))
    value.ObjectSet(db, "replicas", replicas)

    mut root := value.NewValueObject()
    value.ObjectSet(root, "database", db)

    mut ok := false
    text := toml.EncodeStrict(root, ok)
    println "encoded:", ok
    print text

    back := value.ObjectGet(toml.Decode(text), "database")
    println "timeout is still a float:", value.IsFloat(value.ObjectGet(back, "timeout"))
    println "password written:", value.ObjectHas(back, "password")
    println "replicas read back:", value.ArrayLen(value.ObjectGet(back, "replicas"))
end
Outputencoded: true [database] url = "postgres://localhost/shop" pool = 8 timeout = 30.0 replicas = [{host = "10.0.0.2", weight = 2}, {host = "10.0.0.3", weight = 1}] timeout is still a float: true password written: false replicas read back: 2

Three things show in the output. The null password is left out, because TOML has no null. The array of replica objects is written inline on one line. The float 30.0 keeps its .0, so it reads back as a float and not as the integer 30. Keys come out in no particular order, so never compare the text with a fixed string; compare the data, for example with value.Equal after decoding the text again.

Guide: catching mistakes

Use DecodeStrict or Valid on any file a person edits. They catch broken syntax: a key without a value, an unclosed string, array or table header.

encoding-toml-guide-errors.salam
import encoding.toml

func check(label: str, text: str):
    mut ok := false
    _ := toml.DecodeStrict(text, ok)
    println label, ok
end

func main:
    check("good:            ", "a = 1\nb = 'two'\n")
    check("no value:        ", "a = \n")
    check("unclosed string: ", "a = \"open\n")
    check("unclosed array:  ", "a = [1, 2\n")
    check("unclosed table:  ", "[server\nport = 1\n")
    check("multi-line text: ", "a = \"\"\"x\"\"\"\n")
    check("duplicate key:   ", "a = 1\na = 2\n")
    check("unquoted string: ", "mode = fast\n")
    println "Valid agrees:", toml.Valid("a = 1"), toml.Valid("a = ")
end
Outputgood: true no value: false unclosed string: false unclosed array: false unclosed table: false multi-line text: false duplicate key: true unquoted string: true Valid agrees: true false

The last two lines are the leniency described above: a duplicate key is accepted and the later value wins, and mode = fast passes as the string "fast", although real TOML requires quotes. If either would hide a real mistake in your files, check the keys you care about after decoding, for example with value.IsString and a list of allowed values.

Functions

DecodeStrict

func DecodeStrict(text: str, ok &: bool): value.Value
Reads TOML, setting ok false when the text is not valid.
encoding-toml-Decode.salam
import encoding.toml
import encoding.value

func main:
    text := "title = \"salam\"\n\n[server]\nhost = \"localhost\"\nport = 8080\ntags = [\"a\", \"b\"]\n"
    println toml.Valid(text), toml.Valid("= no key")
    v := toml.Decode(text)
    server := value.ObjectGet(v, "server")
    println value.AsString(value.ObjectGet(v, "title")), value.AsInt(value.ObjectGet(server, "port"))
    println toml.Encode(v)
    mut read := false
    _ := toml.DecodeStrict("a = \n", read)
    println read
    mut wrote := false
    println toml.EncodeStrict(value.ValueInt(1), wrote), wrote
end
Outputtrue false salam 8080 title = "salam" [server] host = "localhost" port = 8080 tags = ["a", "b"] false false

Decode

func Decode(text: str): value.Value
Turns TOML into a Value without reporting errors: on invalid text it returns whatever was read before the error, so prefer DecodeStrict for files people edit.

The example for this item is shown with the ones above: encoding-toml-Decode.salam

Valid

func Valid(text: str): bool
True when the text is valid TOML.

The example for this item is shown with the ones above: encoding-toml-Decode.salam

EncodeStrict

func EncodeStrict(v: value.Value, ok &: bool): str
Turns a value into TOML. ok turns false when the top level is not an object or an array holds a null; null keys in a table are skipped.

The example for this item is shown with the ones above: encoding-toml-Decode.salam

Encode

func Encode(v: value.Value): str
Turns a value into TOML, ignoring problems; a top level that is not an object gives an empty string.

The example for this item is shown with the ones above: encoding-toml-Decode.salam