This guide shows zutoml’s main tasks, one section each. For a tour
built around real configuration files, see
vignette("zutoml").
Read a document
toml_parse() reads TOML text; toml_read()
reads a file, a URL or a connection. Both return a named list, with keys
in the order the document gives them.
doc <- '
title = "Example"
[server]
host = "localhost"
port = 8080
tags = ["web", "api"]
[[users]]
name = "ana"
admin = true
[[users]]
name = "bo"
'
x <- toml_parse(doc)
str(x)
#> List of 3
#> $ title : chr "Example"
#> $ server:List of 3
#> ..$ host: chr "localhost"
#> ..$ port: int 8080
#> ..$ tags: chr [1:2] "web" "api"
#> $ users :List of 2
#> ..$ :List of 2
#> .. ..$ name : chr "ana"
#> .. ..$ admin: logi TRUE
#> ..$ :List of 1
#> .. ..$ name: chr "bo"
path <- tempfile(fileext = ".toml")
writeLines(doc, path)
toml_read(path)$server$port
#> [1] 8080Read only part of a document
select returns the value at a path, written as the TOML
key syntax: dots between keys, quotes around keys that need them, and
[i] for array elements, counting from 1.
toml_parse(doc, select = "server")
#> $host
#> [1] "localhost"
#>
#> $port
#> [1] 8080
#>
#> $tags
#> [1] "web" "api"
toml_parse(doc, select = "users[2].name")
#> [1] "bo"
toml_read(path, select = c("server", "tags"))
#> [1] "web" "api"The whole document is still checked, since TOML lets a table be added
to anywhere in it, but only the selected part becomes R values. A path
the document does not have is an error, never NULL:
try(toml_parse(doc, select = "server.timeout"))
#> Error : TOML document has no `server.timeout`: `server` has no `timeout`Types
Every TOML type maps to one R type, chosen by the document’s syntax, never by what a string looks like:
| TOML | R |
|---|---|
| string | character |
| integer |
integer; double past R’s integer range;
toml_bigint past 2^53 |
| float |
double (inf, nan as
Inf, NaN) |
| boolean | logical |
| offset date-time |
POSIXct in UTC |
| local date-time |
POSIXct in the session’s time zone |
| local date | Date |
| local time |
character, or difftime
|
| table | named list
|
| array | a vector when its elements share a type, else a
list
|
str(toml_parse('
when = 1979-05-27T07:32:00-08:00
day = 1979-05-27
alarm = 07:30:00
text = "1979-05-27"
big = 9223372036854775807
'))
#> List of 5
#> $ when : POSIXct[1:1], format: "1979-05-27 15:32:00"
#> $ day : Date[1:1], format: "1979-05-27"
#> $ alarm: chr "07:30:00"
#> $ text : chr "1979-05-27"
#> $ big : 'toml_bigint' chr "9223372036854775807"datetimes = "keep" returns date-times as RFC 3339 text,
and local_time = "difftime" returns times of day as seconds
since midnight. big_integers decides what happens past
2^53: a toml_bigint (exact text, the default), the nearest
double, or an error.
Arrays
An array whose elements share a type becomes a vector; integers and
floats together become a double vector. Booleans never join
the numbers. Anything else is a list.
str(toml_parse("a = [1, 2]\nb = [1, 2.5]\nc = [true, 1]\nd = []\ne = [3]"))
#> List of 5
#> $ a: int [1:2] 1 2
#> $ b: num [1:2] 1 2.5
#> $ c:List of 2
#> ..$ : logi TRUE
#> ..$ : int 1
#> $ d: logi(0)
#> $ e: 'AsIs' int 3A one-element array is marked with I() so that writing
it back gives an array again, as e shows.
simplify = "none" makes every array a list.
Data frames
data_frame = TRUE turns an array of tables into a data
frame: one row per table, columns in first-seen key order,
NA where a table lacks a key.
toml_parse(doc, data_frame = TRUE)$users
#> name admin
#> 1 ana TRUE
#> 2 bo NAWrite a document
toml_emit() writes a named list as TOML text;
toml_write() writes it to a file or connection.
cfg <- list(
title = "Example",
port = 8080,
ratio = 0.75,
started = as.Date("2026-10-08"),
server = list(host = "localhost", tags = c("web", "api")),
users = list(list(name = "ana"), list(name = "bo"))
)
cat(toml_emit(cfg))
#> title = "Example"
#> port = 8080
#> ratio = 0.75
#> started = 2026-10-08
#>
#> [server]
#> host = "localhost"
#> tags = ["web", "api"]
#>
#> [[users]]
#> name = "ana"
#>
#> [[users]]
#> name = "bo"- A table’s own values come first, then its sub-tables, as TOML requires.
-
port = 8080is a double in R but is written as the integer a TOML reader expects; non-whole doubles are written in the shortest digits that read back exactly. - A list of named lists, or a data frame, becomes an array of tables.
- The output is the same on every platform, and reads back:
identical(toml_parse(toml_emit(cfg))$users, cfg$users)
#> [1] TRUENA and NULL have no TOML form, so they are
refused unless na = "omit". Arrays past width
characters are written one element per line, and inline = n
writes small flat tables inline.
Infrastructure configuration
Many infrastructure tools are configured in TOML: Traefik,
containerd, GitLab Runner, Telegraf, Netlify, Cloudflare’s
wrangler.toml, Fly.io’s fly.toml. Reading one
is the same as reading any document, here Traefik’s sample static
configuration, pinned to a release:
traefik <- "https://raw.githubusercontent.com/traefik/traefik/v3.7.14/traefik.sample.toml"
static <- toml_read(traefik)
names(static)
#> [1] "global" "entryPoints" "log" "api" "ping"
#> [6] "providers"
static$entryPoints$websecure
#> $address
#> [1] ":443"Writing is where TOML from R pays off: a configuration generated from data. This is a Traefik dynamic configuration that routes a host to a set of backends:
backends <- c("http://10.0.0.1:8080", "http://10.0.0.2:8080")
dynamic <- list(http = list(
routers = list(app = list(
rule = "Host(`app.example.com`)",
service = "app",
entryPoints = I("websecure"),
tls = list(certResolver = "letsencrypt")
)),
services = list(app = list(
loadBalancer = list(servers = lapply(backends, function(u) list(url = u)))
))
))
cat(toml_emit(dynamic))
#> [http.routers.app]
#> rule = "Host(`app.example.com`)"
#> service = "app"
#> entryPoints = ["websecure"]
#>
#> [http.routers.app.tls]
#> certResolver = "letsencrypt"
#>
#> [[http.services.app.loadBalancer.servers]]
#> url = "http://10.0.0.1:8080"
#>
#> [[http.services.app.loadBalancer.servers]]
#> url = "http://10.0.0.2:8080"I("websecure") keeps a one-element array an array, which
Traefik expects; the list of servers becomes an array of tables. Write
it where Traefik watches for files with
toml_write(dynamic, "dynamic.toml").
Choose how one value is written
Markers change how a value is written, never what it reads back as:
cat(toml_emit(list(
dependencies = list(
serde = toml_inline(list(version = "1.0", features = "derive")),
rand = "0.8"
),
pattern = toml_literal("^\\d+\\.\\d+$"),
motd = toml_multiline("Welcome!")
)))
#> pattern = '^\d+\.\d+$'
#> motd = """
#> Welcome!"""
#>
#> [dependencies]
#> serde = { version = "1.0", features = "derive" }
#> rand = "0.8"toml_inline() writes a table inline,
toml_literal() writes literal strings that need no escapes,
and toml_multiline() writes multi-line strings.
Point at a value in the document
positions = TRUE records where every key and array
element is defined, so a check on a configuration can say where the
problem is:
cfg <- toml_parse("[server]\nhost = 'localhost'\nport = 99999\n", positions = TRUE)
pos <- attr(cfg, "toml_positions")
pos
#> path type line column offset
#> 1 server table 1 2 1
#> 2 server.host string 2 1 9
#> 3 server.port integer 3 1 28
at <- pos[pos$path == "server.port", ]
sprintf("server.port (line %d, column %d) must be at most 65535", at$line, at$column)
#> [1] "server.port (line 3, column 1) must be at most 65535"The path column uses the same syntax as
select.
Check a document
toml_validate() runs the whole check without building
any R value. It returns TRUE or FALSE, or with
error = TRUE raises the reason:
toml_validate("a = 1\nb = 2")
#> [1] TRUE
toml_validate("a = 1\na = 2")
#> [1] FALSE
try(toml_validate("a = 1\na = 2", error = TRUE))
#> Error : TOML parse error at line 2, column 1: key defined twiceHandle errors
Every error has a class, so it can be caught by kind. Parse errors
carry the line, column, byte
offset and kind of the fault:
err <- tryCatch(toml_parse("[t]\nx = 1\n[t]\n"), zutoml_parse_error = function(e) e)
c(err$kind, err$line, err$column)
#> [1] "table_redefined" "3" "2"| Class | When |
|---|---|
zutoml_parse_error |
the document is not TOML |
zutoml_limit_error |
a limit was reached (zutoml_size_limit,
zutoml_depth_limit, zutoml_item_limit,
zutoml_string_limit) |
zutoml_unrepresentable |
valid TOML that R cannot hold, such as a string with U+0000 |
zutoml_missing_key |
select names a path the document does not have |
zutoml_unsupported_type,
zutoml_na_error
|
an R value with no TOML form, or
NA/NULL
|
zutoml_invalid_argument,
zutoml_io_error
|
a bad argument; a file that cannot be read or written |
All of them inherit from zutoml_error. See
?"zutoml-conditions".
Untrusted input
Four limits bound what a document can make zutoml do, and are checked before any R value is built:
try(toml_parse(paste0("a = ", strrep("[", 200)), max_depth = 64))
#> Error : TOML limit reached at line 1, column 69: the document nests deeper than max_depth = 64| Argument | Default | Bounds |
|---|---|---|
max_size |
64 MiB | the document’s bytes; toml_read() never reads past
it |
max_depth |
128 | nested tables and arrays (at most 1023) |
max_items |
1e6 | keys plus array elements |
max_string |
2^31 - 1 | one string’s or key’s bytes |
TOML versions
zutoml reads TOML 1.1.0 by default, which adds \e and
\xHH escapes, times without seconds, and newlines and
trailing commas in inline tables. version = "1.0.0" reads
strictly. The emitter always writes text that TOML 1.0.0 readers
accept.
toml_parse("t = 07:30")$t
#> [1] "07:30:00"
try(toml_parse("t = 07:30", version = "1.0.0"))
#> Error : TOML parse error at line 1, column 5: not a valid TOML date or time