A small, fast, and correct TOML parser and serializer. smol-toml is fully(ish) spec-compliant with TOML v1.1.0.
Why yet another TOML parser? Well, the ecosystem of TOML parsers in JavaScript is quite underwhelming, most likely due to a lack of interest. With most parsers being outdated, unmaintained, non-compliant, or a combination of these, a new parser didn't feel too out of place.
[insert xkcd 927]
Nowadays, smol-toml is the most downloaded TOML parser on npm thanks to its quality. From frameworks to tooling, it has been battle-tested and is actively used in production systems.
smol-toml passes most of the tests from the toml-test suite; use the
run-toml-test.bash script to run the tests. Due to the nature of JavaScript and the limits of the language,
it doesn't pass certain tests, namely:
- Invalid UTF-8 strings (and comments) are not rejected
- Certain invalid dates are not rejected
- For instance,
2023-02-30would be accepted and parsed as2023-03-02. While additional checks could be performed to reject these, they've not been added for performance reasons.
- For instance,
Please also note that by default, the behavior regarding integers doesn't preserve type information, nor does it allow deserializing integers larger than 53 bits. See Integers.
You can see a list of all tests smol-toml fails (and the reason why it fails these) in the list of skipped tests in
run-toml-test.bash. Note that some failures are not specification violations per-se. For instance, the TOML spec
does not require 64-bit integer range support or sub-millisecond time precision, but are included in the toml-test
suite. See toml-lang/toml-test#154 and toml-lang/toml-test#155
[pnpm | yarn | npm] i smol-toml
import { parse, stringify } from 'smol-toml'
const doc = '...'
const parsed = parse(doc)
console.log(parsed)
const toml = stringify(parsed)
console.log(toml)Alternatively, if you prefer something similar to the JSON global, you can import the library as follows
import TOML from 'smol-toml'
TOML.stringify({ ... })A few notes on the stringify function:
undefinedandnullvalues on objects are ignored (does not produce a key/value).undefinedandnullvalues in arrays are rejected.- Functions, classes and symbols are rejected.
- By default, floats will be serialized as integers if they don't have a decimal part. See Integers
stringify(parse('a = 1.0')) === 'a = 1'
- JS
Datewill be serialized as Offset Date Time- Use the
TomlDateobject for representing other types.
- Use the
When parsing, both integers and floats are read as plain JavaScript numbers, which essentially are floats. This means loss of type information, and makes it impossible to safely represent integers beyond 53 bits.
When serializing, numbers without a decimal part are serialized as integers (except if they're outside of the safe
range; i.e. they cannot be represented as a signed 53-bit integer). This allows in most cases to preserve
whether a number is an integer or not, but fails to preserve type information for numbers like 1.0.
To parse integers beyond 53 bits, it's possible to tell the parser to return all integers as BigInt. This will therefore preserve the type information at the cost of using a slightly more expensive container.
import { parse } from 'smol-toml'
const doc = '...'
const parsed = parse(doc, { integersAsBigInt: true })If you want to keep numbers for integers that can safely be represented as a JavaScript number, you can pass
"asNeeded" instead.
To get end-to-end type preservation, you can tell the serializer to always treat numbers as floating point numbers. Then, only BigInts will be serialized as integers and numbers without a decimal part will still be serialized as float.
import { stringify } from 'smol-toml'
const obj = { ... }
const toml = stringify(obj, { numbersAsFloat: true })smol-toml uses an extended Date object to represent all types of TOML Dates. In the future, smol-toml will use
objects from the Temporal proposal, but for now we're stuck with the legacy Date object.
import { TomlDate } from 'smol-toml'
// Offset Date Time
const date = new TomlDate('1979-05-27T07:32:00.000-08:00')
console.log(date.isDateTime(), date.isDate(), date.isTime(), date.isLocal()) // ~> true, false, false, false
console.log(date.toISOString()) // ~> 1979-05-27T07:32:00.000-08:00
// Local Date Time
const date = new TomlDate('1979-05-27T07:32:00.000')
console.log(date.isDateTime(), date.isDate(), date.isTime(), date.isLocal()) // ~> true, false, false, true
console.log(date.toISOString()) // ~> 1979-05-27T07:32:00.000
// Local Date
const date = new TomlDate('1979-05-27')
console.log(date.isDateTime(), date.isDate(), date.isTime(), date.isLocal()) // ~> false, true, false, true
console.log(date.toISOString()) // ~> 1979-05-27
// Local Time
const date = new TomlDate('07:32:00')
console.log(date.isDateTime(), date.isDate(), date.isTime(), date.isLocal()) // ~> false, false, true, true
console.log(date.toISOString()) // ~> 07:32:00.000You can also wrap a native Date object and specify using different methods depending on the type of date you wish
to represent:
import { TomlDate } from 'smol-toml'
const jsDate = new Date()
const offsetDateTime = TomlDate.wrapAsOffsetDateTime(jsDate)
const localDateTime = TomlDate.wrapAsLocalDateTime(jsDate)
const localDate = TomlDate.wrapAsLocalDate(jsDate)
const localTime = TomlDate.wrapAsLocalTime(jsDate)The benchmark is ran using mitata.
Parsers and serializers are tested in 2 scenarios: using the example from TOML's homepage and specification, and using a ~5MB randomly generated1 file.
While fast-toml is included as it's a challenging candidate, it takes a lot of shortcuts that makes it very fast,
but at the expense of correctness (it has significant defects and does not pass the TOML test suite). smol-toml is
almost as fast, without sacrificing correctness. π
| Parse | smol-toml@1.7.0 | @iarna/toml@3.0.0 | @ltd/j-toml@1.38.0 | fast-toml@0.5.4 | @std/toml@1.0.11 | toml@4.1.1 | js-toml@1.1.2 | @decimalturn/toml-patch@2.0.0 |
|---|---|---|---|---|---|---|---|---|
| Spec example | 4.90 Β΅s/iter | 12.36 Β΅s/iter | 33.17 Β΅s/iter | 4.80 Β΅s/iter | 22.94 Β΅s/iter | 29.86 Β΅s/iter | 23.67 Β΅s/iter | 17.81 Β΅s/iter |
| ~5MB test file | 116.74 ms/iter | DNF | 198.57 ms/iter | 93.51 ms/iter | 429.91 ms/iter | 415.92 ms/iter | 361.89 ms/iter | 176.94 ms/iter |
| Stringify | smol-toml@1.7.0 | @iarna/toml@3.0.0 | @ltd/j-toml@1.38.0 | fast-toml@0.5.4 | @std/toml@1.0.11 | toml@4.1.1 | js-toml@1.1.2 | @decimalturn/toml-patch@2.0.0 |
|---|---|---|---|---|---|---|---|---|
| Spec example | 2.24 Β΅s/iter | 8.68 Β΅s/iter | 89.89 Β΅s/iter | N/A | 3.86 Β΅s/iter | N/A | 3.53 Β΅s/iter | N/A2 |
| ~5MB test file | 42.34 ms/iter | 132.74 ms/iter | 921.98 ms/iter | N/A | 68.37 ms/iter | N/A | 106.14 ms/iter | N/A2 |
Detailed benchmark data
node --expose-gc bench/parse.bench.ts
clk: ~5.47 GHz
cpu: AMD Ryzen 9 9950X3D 16-Core Processor
runtime: node 26.3.1 (x64-linux)
benchmark avg (min β¦ max) p75 / p99 (min β¦ top 1%)
------------------------------------------- -------------------------------
β’ spec document
------------------------------------------- -------------------------------
smol-toml 4.90 Β΅s/iter 4.85 Β΅s β
(4.72 Β΅s β¦ 96.03 Β΅s) 6.13 Β΅s ββ
( 2.09 kb β¦ 325.14 kb) 12.96 kb βββββββββββββββββββββ
4.01 ipc ( 99.63% cache) 21.59 branch misses
27.50k cycles 110.19k instructions 4.09k c-refs 15.27 c-misses
@iarna/toml 12.36 Β΅s/iter 12.19 Β΅s β
(11.86 Β΅s β¦ 131.24 Β΅s) 16.53 Β΅s β
(176.00 b β¦ 360.27 kb) 24.00 kb β
ββ
ββββββββββββββββββ
3.97 ipc ( 99.71% cache) 60.52 branch misses
68.38k cycles 271.17k instructions 12.01k c-refs 35.06 c-misses
@ltd/j-toml 33.17 Β΅s/iter 32.50 Β΅s ββ
(31.03 Β΅s β¦ 1.10 ms) 36.36 Β΅s ββββ
( 40.00 b β¦ 537.94 kb) 27.61 kb βββββββββββββββββββββ
3.15 ipc ( 99.50% cache) 305.47 branch misses
183.96k cycles 579.46k instructions 32.33k c-refs 160.93 c-misses
fast-toml 4.80 Β΅s/iter 4.77 Β΅s β
(4.61 Β΅s β¦ 96.44 Β΅s) 5.58 Β΅s ββ
(256.00 b β¦ 329.85 kb) 12.55 kb βββββ
ββββββββββββββββ
4.61 ipc ( 99.61% cache) 18.02 branch misses
26.90k cycles 123.90k instructions 3.73k c-refs 14.45 c-misses
deno's @std/toml 22.94 Β΅s/iter 22.84 Β΅s β
(21.43 Β΅s β¦ 183.90 Β΅s) 27.22 Β΅s ββ
(800.00 b β¦ 619.62 kb) 64.88 kb βββββββ
ββββββββββββββ
3.73 ipc ( 99.67% cache) 106.77 branch misses
126.43k cycles 472.03k instructions 23.79k c-refs 79.52 c-misses
node-toml 29.86 Β΅s/iter 29.51 Β΅s β
(28.17 Β΅s β¦ 200.95 Β΅s) 42.50 Β΅s ββ
(976.00 b β¦ 785.96 kb) 104.23 kb βββββββββββββββββββββ
4.01 ipc ( 99.36% cache) 146.79 branch misses
163.80k cycles 657.50k instructions 27.26k c-refs 174.37 c-misses
js-toml 23.67 Β΅s/iter 23.31 Β΅s ββ
(22.10 Β΅s β¦ 196.26 Β΅s) 34.04 Β΅s ββ
( 2.55 kb β¦ 678.23 kb) 96.06 kb βββββββββββββββββββββ
4.14 ipc ( 98.56% cache) 141.38 branch misses
129.53k cycles 536.67k instructions 18.52k c-refs 266.18 c-misses
@decimalturn/toml-patch 17.81 Β΅s/iter 17.68 Β΅s ββ
(16.82 Β΅s β¦ 159.63 Β΅s) 23.01 Β΅s βββ
(264.00 b β¦ 651.59 kb) 56.03 kb βββββββββββββββββββββ
3.60 ipc ( 99.55% cache) 73.51 branch misses
98.49k cycles 354.88k instructions 20.21k c-refs 90.93 c-misses
summary
fast-toml
1.02x faster than smol-toml
2.57x faster than @iarna/toml
3.71x faster than @decimalturn/toml-patch
4.77x faster than deno's @std/toml
4.93x faster than js-toml
6.22x faster than node-toml
6.9x faster than @ltd/j-toml (nice)
β’ 5MB document
------------------------------------------- -------------------------------
smol-toml 116.74 ms/iter 118.39 ms β β
(110.52 ms β¦ 127.54 ms) 123.91 ms β β
β
β β
β
β
β
β
( 32.98 mb β¦ 49.20 mb) 42.93 mb βββββββββββββββββββββ
2.48 ipc ( 98.45% cache) 3.17M branch misses
629.10M cycles 1.56G instructions 101.49M c-refs 1.58M c-misses
@iarna/toml error: Unexpected character in datetime, expected period (.), minus (-), plus (+) or Z at row 5, col 45, pos 569:
4: NfF6LuAerfn5mDPI7Cp 2qsrB4vGmJTyb5jNubOIBYYWrWlAsrw PX93S57gjb5GEhR8qOU5blDQmwfVJTA YvmJ9cE3cUZU NAJQgAbIdpZLhc4lOs4ZhMEWehhZqXCsVD1YP1vN2GEoM2WX''', 1986-05-27T18:36:13Z, -5460, "2ZAV3fYlb23hf7r7QoftVlicWE2iuwp", 4790.2253 ]
5> SzeC4Me8T = [ 5928.9340, 1991-04-28 19:24:24, 7807, -782.4516, 0b1000011111, "YvXRZKBGle9d51sqE90t8hP" ]
^
6: SLMvBirT.pmpX1D.9PivpfFBo = 2010-09-29
@ltd/j-toml 198.57 ms/iter 196.77 ms β
(181.11 ms β¦ 291.66 ms) 209.47 ms ββ
( 8.55 mb β¦ 55.95 mb) 35.95 mb βββββββββββββββββββββ
2.32 ipc ( 97.82% cache) 3.98M branch misses
1.08G cycles 2.50G instructions 166.94M c-refs 3.63M c-misses
fast-toml 93.51 ms/iter 95.06 ms β
(89.98 ms β¦ 96.83 ms) 95.74 ms β
β
β
ββ
β
β
β
β
β
( 13.30 mb β¦ 34.88 mb) 24.04 mb βββββββββββββββββββββ
2.83 ipc ( 98.14% cache) 2.36M branch misses
507.32M cycles 1.44G instructions 66.44M c-refs 1.23M c-misses
deno's @std/toml 429.91 ms/iter 436.58 ms β
(419.32 ms β¦ 441.05 ms) 437.63 ms β
β
β
β
β
β
β
β
β
β
(180.32 mb β¦ 204.05 mb) 191.51 mb βββββββββββββββββββββ
3.15 ipc ( 97.05% cache) 6.63M branch misses
2.36G cycles 7.45G instructions 255.14M c-refs 7.54M c-misses
node-toml 415.92 ms/iter 413.57 ms ββ
(404.84 ms β¦ 448.96 ms) 438.33 ms ββ β
( 45.80 mb β¦ 78.18 mb) 50.13 mb βββββββββββββββββββββ
4.26 ipc ( 97.29% cache) 6.53M branch misses
2.25G cycles 9.59G instructions 194.56M c-refs 5.27M c-misses
js-toml 361.89 ms/iter 369.39 ms β
(346.05 ms β¦ 406.77 ms) 394.74 ms β
β β
(249.62 mb β¦ 282.33 mb) 265.65 mb βββββββββββββββββββββ
2.55 ipc ( 94.91% cache) 5.64M branch misses
1.73G cycles 4.43G instructions 112.66M c-refs 5.74M c-misses
@decimalturn/toml-patch 176.94 ms/iter 174.86 ms β
(172.74 ms β¦ 197.94 ms) 182.08 ms β ββ
( 27.14 mb β¦ 38.55 mb) 28.81 mb βββββββββββββββββββββ
3.18 ipc ( 97.69% cache) 3.58M branch misses
947.48M cycles 3.02G instructions 130.37M c-refs 3.01M c-misses
summary
fast-toml
1.25x faster than smol-toml
1.89x faster than @decimalturn/toml-patch
2.12x faster than @ltd/j-toml
3.87x faster than js-toml
4.45x faster than node-toml
4.6x faster than deno's @std/toml
node --expose-gc bench/stringify.bench.ts
clk: ~5.48 GHz
cpu: AMD Ryzen 9 9950X3D 16-Core Processor
runtime: node 26.3.1 (x64-linux)
benchmark avg (min β¦ max) p75 / p99 (min β¦ top 1%)
------------------------------------------- -------------------------------
β’ spec document
------------------------------------------- -------------------------------
smol-toml 2.24 Β΅s/iter 2.24 Β΅s β β
(2.22 Β΅s β¦ 2.27 Β΅s) 2.27 Β΅s ββ β
β ββ
( 4.60 kb β¦ 4.71 kb) 4.61 kb β
βββββββββββββββ
βββββ
4.62 ipc ( 99.64% cache) 5.08 branch misses
12.21k cycles 56.44k instructions 1.12k c-refs 4.08 c-misses
@iarna/toml 8.68 Β΅s/iter 8.67 Β΅s β
(8.65 Β΅s β¦ 8.89 Β΅s) 8.73 Β΅s β
ββ
β
(654.88 b β¦ 1.53 kb) 720.60 b βββββββββββββββββββββ
4.00 ipc ( 99.49% cache) 23.53 branch misses
47.95k cycles 191.80k instructions 7.11k c-refs 36.09 c-misses
@ltd/j-toml 89.89 Β΅s/iter 90.21 Β΅s β
(69.82 Β΅s β¦ 209.14 Β΅s) 95.93 Β΅s β
( 4.04 kb β¦ 389.75 kb) 37.49 kb βββββββββββββββββββββ
4.20 ipc ( 99.76% cache) 544.09 branch misses
490.58k cycles 2.06M instructions 35.22k c-refs 82.84 c-misses
deno's @std/toml 3.86 Β΅s/iter 3.86 Β΅s β
(3.83 Β΅s β¦ 4.24 Β΅s) 3.88 Β΅s β
β
ββ
β
βββ
(836.05 b β¦ 4.28 kb) 0.98 kb βββββ
ββ
ββββββββββββββ
4.00 ipc ( 99.60% cache) 6.22 branch misses
21.36k cycles 85.36k instructions 3.32k c-refs 13.27 c-misses
js-toml 3.53 Β΅s/iter 3.54 Β΅s βββ
(3.52 Β΅s β¦ 3.58 Β΅s) 3.58 Β΅s ββββ
( 5.76 kb β¦ 7.29 kb) 6.98 kb ββββββββ
βββββββββββββ
4.37 ipc ( 99.11% cache) 10.24 branch misses
19.45k cycles 84.99k instructions 2.10k c-refs 18.67 c-misses
@decimalturn/toml-patch 81.54 Β΅s/iter 81.22 Β΅s β
(78.63 Β΅s β¦ 341.24 Β΅s) 103.14 Β΅s ββ
(872.00 b β¦ 831.95 kb) 113.00 kb ββββ
βββββββββββββββββ
4.56 ipc ( 99.28% cache) 567.51 branch misses
443.97k cycles 2.02M instructions 52.63k c-refs 381.28 c-misses
summary
smol-toml
1.58x faster than js-toml
1.73x faster than deno's @std/toml
3.88x faster than @iarna/toml
36.44x faster than @decimalturn/toml-patch
40.18x faster than @ltd/j-toml
β’ 5MB document
------------------------------------------- -------------------------------
smol-toml 42.34 ms/iter 43.06 ms β β
(40.50 ms β¦ 45.33 ms) 43.51 ms β β
β
β
β
β
β
β
β
β
( 9.46 mb β¦ 35.74 mb) 12.91 mb βββββββββββββββββββββ
2.12 ipc ( 95.19% cache) 1.59M branch misses
217.80M cycles 462.56M instructions 14.99M c-refs 720.67k c-misses
@iarna/toml 132.74 ms/iter 134.02 ms β
(125.91 ms β¦ 138.18 ms) 137.02 ms β β
( 27.35 mb β¦ 54.08 mb) 41.53 mb βββββββββββββββββββββ
2.56 ipc ( 97.08% cache) 4.44M branch misses
704.85M cycles 1.80G instructions 85.83M c-refs 2.50M c-misses
@ltd/j-toml 921.98 ms/iter 927.58 ms β ββ β
(889.05 ms β¦ 937.02 ms) 929.09 ms β
β β
βββ
β
( 4.20 mb β¦ 33.46 mb) 18.59 mb βββββββββββββββββββββ
4.06 ipc ( 99.42% cache) 6.75M branch misses
5.04G cycles 20.45G instructions 243.67M c-refs 1.41M c-misses
deno's @std/toml 68.37 ms/iter 69.68 ms β β
(61.79 ms β¦ 78.19 ms) 77.23 ms β
β
ββ
β
β
β β
β
( 29.04 mb β¦ 62.29 mb) 33.86 mb βββββββββββββββββββββ
2.21 ipc ( 95.21% cache) 1.74M branch misses
332.33M cycles 732.79M instructions 30.06M c-refs 1.44M c-misses
js-toml 106.14 ms/iter 107.44 ms β
(102.35 ms β¦ 117.91 ms) 108.69 ms β
β β
β
β
β
β
β
β
β
( 34.03 mb β¦ 62.21 mb) 44.13 mb βββββββββββββββββββββ
2.39 ipc ( 96.23% cache) 3.05M branch misses
570.22M cycles 1.36G instructions 50.24M c-refs 1.90M c-misses
@decimalturn/toml-patch 1.07 s/iter 1.08 s β β β
(1.05 s β¦ 1.14 s) 1.09 s ββ
β
β
β β β
β
(116.61 mb β¦ 147.62 mb) 121.20 mb βββββββββββββββββββββ
3.67 ipc ( 93.69% cache) 12.18M branch misses
5.77G cycles 21.17G instructions 780.26M c-refs 49.26M c-misses
summary
smol-toml
1.61x faster than deno's @std/toml
2.51x faster than js-toml
3.13x faster than @iarna/toml
21.77x faster than @ltd/j-toml
25.29x faster than @decimalturn/toml-patch