mrkeyoor.com_
Thu 06 Aug 05:59 UTC
npmUtilsupdated 06 Aug 2026

smol-toml

smol-toml is a TOML parser and serializer for JavaScript with a deliberately small surface: parse(string) gives you a plain object, stringify(object) gives you a TOML document, and that is nearly the whole API. It has no dependencies, ships both ESM and CommonJS builds with bundled types, and targets the TOML 1.1.0 specification. It exists because the previous generation of JavaScript TOML parsers had drifted into being unmaintained, non-compliant, or both, and it now sits underneath a lot of build tooling, which is how a project with a few hundred stars ends up with tens of millions of weekly downloads. Two extra exports round it out: TomlDate, which models the four distinct TOML date and time types that a plain JS Date cannot express, and TomlError, which reports the line, column and a rendered code excerpt for a syntax failure.

Verdict

The right default for reading and writing TOML in JavaScript today: small, dependency-free, quick, and correct on nearly everything the official test suite throws at it. Decide how you want integers represented before you write any code against it, and do not expect it to preserve comments.

API stability5/5The whole public API is parse, stringify, TomlDate and TomlError, and it has stayed on 1.x throughout. The one deprecation so far is the TomlPrimitive type alias, which is still exported and just points at TomlValue.
Docs3/5The README is honest and unusually specific about limitations, integer behaviour, date types and benchmark methodology, but it is the only documentation. There is no reference site, no per-option API page, and the maxDepth option is not documented at all outside the source.
Maintenance4/5Pushed within the last few days with 1.7.1 current and only 8 open issues. The deduction is bus factor: it is essentially one maintainer carrying a package that a lot of the JavaScript build ecosystem now depends on.
Ecosystem4/5Roughly 27.8M weekly downloads makes it the most installed TOML parser on npm, and frameworks and tooling pull it in as a dependency. There is no plugin surface to speak of, which is by design for a parser this small.

Use it if

  • You read TOML config files in Node and want a parser that passes most of the official toml-test suite rather than a hand-rolled regex approximation
  • You also write TOML back out; stringify is the fastest serializer in the project's own benchmark, several times ahead of @iarna/toml on both small and large documents
  • You want zero dependencies, bundled TypeScript types, and both import and require entry points from a package that is about 11 kB minified
  • You need error messages a user can act on: TomlError carries line, column and a formatted code block pointing at the offending character
  • You care about TOML date semantics, where an offset date-time, a local date-time, a local date and a local time are four different things that TomlDate keeps distinguishable
Skip it if

Setup reality

npm install smol-toml and there is nothing else to configure: no dependencies, no build step, types included, and both an ESM and a CJS entry so it works either way on Node 18 or newer. The decisions that actually cost you time come after the install. First, integer handling has to be settled at the boundary, because switching integersAsBigInt on later changes the runtime type of values all through your code, and 'asNeeded' gives you a union of number and bigint that TypeScript will make you narrow at every use. Second, the stringify contract is asymmetric in a way that will surprise you at runtime: undefined and null on an object property are silently dropped and produce no key, but the same values inside an array throw, and functions, classes and symbols throw as well. So the same config object serializes fine or blows up depending on where the empty value happens to sit. Third, parse defaults to maxDepth 1000 and throws a TomlError once nesting exceeds it, which is the knob you want to turn down before feeding it anything user-supplied.

Patterns

Parse a TOML stringparse-toml

import { parse } from 'smol-toml'

const config = parse(`
title = "My App"

[server]
port = 8080
hosts = ["a.example.com", "b.example.com"]
`)

console.log(config.title, config.server.port)

Tables come back as plain objects and arrays as plain arrays, with no prototype tricks and no wrapper classes, so the result is safe to hand to JSON.stringify or a schema validator.

Load a config file from diskread-config-file

import { readFile } from 'node:fs/promises'
import { parse } from 'smol-toml'

const raw = await readFile('config.toml', 'utf8')
const config = parse(raw)

Read it as utf8 and pass a string; the parser does not accept a Buffer. A file with a UTF-8 BOM will fail to parse, so strip it if your config might come from a Windows editor.

Serialize an object back to TOMLstringify-toml

import { stringify } from 'smol-toml'

const toml = stringify({
  title: 'My App',
  server: { port: 8080, hosts: ['a.example.com'] },
})

Comments and the original key ordering of a parsed document are not preserved, so this is for generating files, not for editing one a human maintains.

Use it like the JSON globaljson-style-namespace

import * as TOML from 'smol-toml'

const obj = TOML.parse(text)
const out = TOML.stringify(obj)

// CommonJS
const TOML = require('smol-toml')

There is also a default export carrying the same four members, so both import TOML from and import * as TOML work. Pick one style per codebase to avoid confusing bundler interop.

Keep integers exact past 53 bitsbigint-integers

import { parse } from 'smol-toml'

parse('id = 9007199254740993', { integersAsBigInt: true })
// { id: 9007199254740993n }  every integer is a bigint

parse('a = 1\nb = 9007199254740993', { integersAsBigInt: 'asNeeded' })
// { a: 1, b: 9007199254740993n }  bigint only when required

"asNeeded" keeps small values as numbers, which is friendlier at runtime but gives TypeScript a number | bigint union you have to narrow everywhere. Also remember JSON.stringify throws on BigInt, so this choice leaks into anything that serializes the config.

Stop floats collapsing into integerspreserve-float-types

import { parse, stringify } from 'smol-toml'

stringify(parse('a = 1.0'))                        // 'a = 1'
stringify({ a: 1.0 }, { numbersAsFloat: true })     // 'a = 1.0'

With numbersAsFloat every plain number serializes as a float and only BigInt values serialize as integers, which is the only way to get end-to-end type preservation when combined with integersAsBigInt on parse.

Report a syntax error usefullyhandle-parse-errors

import { parse, TomlError } from 'smol-toml'

try {
  parse(raw)
} catch (err) {
  if (err instanceof TomlError) {
    console.error(`config.toml:${err.line}:${err.column}`)
    console.error(err.codeblock)
  }
  throw err
}

codeblock is a pre-rendered excerpt with the surrounding lines and a caret under the offending character. err.message already embeds it, so print one or the other, not both.

Tell the four TOML date types aparttoml-date-types

import { parse, TomlDate } from 'smol-toml'

const { a, b, c, d } = parse(`
a = 1979-05-27T07:32:00-08:00
b = 1979-05-27T07:32:00
c = 1979-05-27
d = 07:32:00
`)

console.log(a.isDateTime(), a.isLocal())   // true, false  (offset date-time)
console.log(b.isLocal())                   // true         (local date-time)
console.log(c.isDate(), d.isTime())        // true, true

A plain JS Date cannot express "a date with no time zone", so smol-toml returns TomlDate, a Date subclass with the type flags attached. toISOString() on it prints only the parts the original value actually had.

Serialize a JS Date as a specific TOML date typewrite-date-values

import { stringify, TomlDate } from 'smol-toml'

const now = new Date()

stringify({ at: now })                                 // offset date-time
stringify({ on: TomlDate.wrapAsLocalDate(now) })       // 1979-05-27
stringify({ at: TomlDate.wrapAsLocalTime(now) })       // 07:32:00
stringify({ at: TomlDate.wrapAsLocalDateTime(now) })

A bare Date always serializes as an offset date-time. If you want a local date or time in the output you have to wrap it explicitly, because the information is not recoverable from the Date itself.

Bound the parser on untrusted inputlimit-nesting-depth

import { parse, TomlError } from 'smol-toml'

try {
  parse(userSubmitted, { maxDepth: 20 })
} catch (err) {
  if (err instanceof TomlError) return reject('config too deeply nested')
  throw err
}

The default is 1000, which is generous for a config file and plenty of room for a hostile document to burn CPU. Exceeding the limit throws a TomlError rather than overflowing the stack. maxDepth is not in the README; it is only visible in the source.

Type the parsed value in TypeScripttype-the-result

import { parse, type TomlTable, type TomlTableWithoutBigInt } from 'smol-toml'

const plain: TomlTableWithoutBigInt = parse(raw)
const withBig: TomlTable = parse(raw, { integersAsBigInt: true })

interface Config { server: { port: number } }
const config = parse(raw) as unknown as Config

The overloads pick the return type from the options object, so the bigint union only appears when you asked for it. Neither type validates your schema; run the result through zod or valibot if the shape matters.

Know what stringify refuses to writeserialization-pitfalls

stringify({ a: 1, b: undefined, c: null })   // 'a = 1'  b and c vanish

stringify({ list: [1, null] })               // throws
stringify({ fn: () => 1 })                   // throws
stringify({ id: Symbol('x') })               // throws

TOML has no null, so an empty object property is dropped without warning while the same value in an array is an error. Strip undefined and null before serializing if you want the behaviour to be predictable.

Alternatives

PackageRegistryPick it when
@ltd/j-tomlnpmYou need strict spec conformance with explicit control over which TOML version you accept
@iarna/tomlnpmAn older project already depends on it and you only need it to keep parsing
js-tomlnpmYou want a TypeScript-first parser built on a formal grammar rather than a hand-written scanner