Project

tomlrb

1.33
A long-lived project that still receives updates
Parses TOML 1.1 and 1.0 documents into Ruby Hashes. Pure Ruby, no runtime dependencies, checked against the official toml-test suite.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies
 Project Readme

Tomlrb

CI Gem Version

A fast, pure-Ruby TOML parser with no runtime dependencies, for TOML 1.1 and 1.0. It passes every case of the official toml-test suite for both versions.

Requires Ruby 3.3 or later. Runs on CRuby (with or without YJIT), JRuby and TruffleRuby.

Installation

bundle add tomlrb

Usage

require "tomlrb"

Tomlrb.parse(%(title = "TOML"\n[owner]\nname = "Tom"))
# => {"title" => "TOML", "owner" => {"name" => "Tom"}}

Tomlrb.load_file("config.toml", symbolize_keys: true)
# => {title: "TOML", owner: {name: "Tom"}}

Tomlrb.parse also accepts an IO. Invalid documents raise Tomlrb::ParseError with the line and column; impossible dates and times raise ArgumentError.

Documents are read as TOML 1.1, which accepts every valid TOML 1.0 document unchanged. Pass version: "1.0" to also reject what 1.1 added (\e and \xHH escapes, times without seconds, inline tables spanning lines or ending with a comma), for example when a file must stay readable by a 1.0-only parser:

Tomlrb.parse("t = 07:32")                 # => {"t" => #<Tomlrb::LocalTime: 07:32:00>}
Tomlrb.parse("t = 07:32", version: "1.0") # raises Tomlrb::ParseError

TOML values map to Ruby as follows:

TOML Ruby
string String
integer Integer
float Float (inf, nan included)
boolean true, false
offset date-time Time
local date-time Tomlrb::LocalDateTime
local date Tomlrb::LocalDate
local time Tomlrb::LocalTime
array Array
table, inline table Hash

Fractional seconds are exact (Rational), and the local types convert with #to_time.

Type signatures for the public API are in sig/.

Benchmark

ruby benchmarks/bench.rb [file...] compares against toml-rb and perfect_toml when they are installed. Each iteration parses every given file (benchmarks/example.toml by default, a 4.6 KB TOML 1.0 document using every value type and table form, which all three parsers accept).

ruby [--yjit] benchmarks/matrix.rb runs one parser over generated workloads, one per input shape (plain, escaped and 1 MB strings, plain and quote-dense multi-line strings, signed and unsigned numbers, floats, inline tables, date-times, many small documents), and reports time, Ruby allocations, peak RSS growth and a digest of the results per workload. --lib DIR loads tomlrb from another checkout, so equal digests show that two versions parse alike; --parser perfect_toml measures perfect_toml instead. Inputs keep the known encoding code range a string from File.read has; --unknown-coderange also times the UTF-8 validation of a string whose code range Ruby has not computed.

CPU instructions per parse (fewer is better), measured with perf stat on CRuby 3.3 without YJIT. With YJIT tomlrb's lead grows: 2.6-2.9x fewer instructions than perfect_toml and 14-16x fewer than 2.0.4. The crates.io corpus is the first 300 Cargo.toml files of a local Cargo registry (~/.cargo/registry/src/*/*/Cargo.toml, sorted by path) that all three parsers accept.

Input tomlrb perfect_toml 0.9.1 tomlrb 2.0.4
benchmarks/example.toml (4.6 KB) 3.0 M 6.6 M 34.1 M
300 crates.io Cargo.toml files (622 KB) 243 M 559 M 3,100 M
rust-lang/rust Cargo.lock at 56df459 (158 KB) 48 M 121 M 687 M

ruby benchmarks/profile.rb [file] records a Vernier profile.

Development

bin/setup            # fetch the toml-test submodule and install gems
bundle exec rake     # tests, Standard (lint), RBS validation
bundle exec rake toml_test   # the official toml-test runner (needs Go)
bin/console          # IRB with tomlrb loaded

Releases are published from GitHub Actions with trusted publishing: bump lib/tomlrb/version.rb, add the CHANGELOG entry, then run the release workflow.

Thanks

Thanks to @jpbougie for the crash course on the Chomsky hierarchy and general tips.