Blog

Luis Majano

August 04, 2026

Spread the word


Share your thoughts

TOML has quietly become the configuration format of the modern toolchain. Rust ships Cargo.toml, Python ships pyproject.toml, and a growing pile of CLIs, deployment platforms, and infrastructure tools expect it. If your BoxLang application needs to read one of those files, or generate one, you now have first class support for it.

We are happy to announce bx-toml 1.0.0, a new BoxLang module that brings full TOML parsing and serialization to the runtime natively.

Install It

box install bx-toml

Or from the BoxLang CLI:

install-bx-module bx-toml

That's it. Four new BIFs are immediately available anywhere in your application: tomlDeserialize(), tomlDeserializeFile(), tomlSerialize(), and tomlSerializeFile().

Quick Start

data = tomlDeserialize( '
	title = "bx-toml"

	[owner]
	name = "Ortus Solutions"
' )

println( data.title )       // bx-toml
println( data.owner.name )  // Ortus Solutions

toml = tomlSerialize( data )

A TOML document goes in, a BoxLang struct comes out. A BoxLang struct goes in, a TOML document comes out. The same mental model you already have for jsonSerialize() and jsonDeserialize().

Reading TOML

TOML is built around simple key = value assignments, with square brackets marking nested tables and double square brackets marking arrays of tables:

name = "bx-toml-demo"
enabled = true
ports = [ 8080, 8443 ]

[database]
host = "localhost"
port = 5432
ssl = true

Use tomlDeserialize() when the content is already a string. Nested tables become nested structs, arrays stay arrays, and TOML dates arrive as BoxLang DateTime values:

config = tomlDeserialize( toml )

println( config.name )           // bx-toml-demo
println( config.database.host )  // localhost
println( config.database.port )  // 5432
println( config.ports[ 1 ] )     // 8080

For files on disk, reach for tomlDeserializeFile(). The optional charset argument covers the cases where the file is not encoded with the system default:

config       = tomlDeserializeFile( "config/app.toml" )
legacyConfig = tomlDeserializeFile( "config/legacy.toml", "ISO-8859-1" )

By default, tables come back as ordered structs, so the key order in your BoxLang code matches the key order in the source document. That matters more than you'd think when you're reading a file, tweaking a value, and writing it back out.

Writing TOML

tomlSerialize() takes a struct and hands you TOML text. The optional options struct lets you control key sorting, indentation, and date formatting for that single call:

config = {
	name     : "bx-toml-demo",
	enabled  : true,
	database : {
		host : "localhost",
		port : 5432
	}
}

toml = tomlSerialize( config, options = { sortKeys : true, indent : 4 } )
println( toml )

To write straight to disk, either pass a filepath to tomlSerialize() or use the explicit tomlSerializeFile() form. Both file-writing forms return null once the write completes:

tomlSerialize( config, "config/generated.toml" )
tomlSerializeFile( config, "config/generated-explicit.toml", "UTF-8" )

The explicit form exists purely for symmetry. If tomlDeserializeFile() reads a paired name, tomlSerializeFile() writes it, and your code reads the same way in both directions.

Type Mapping

TOML is a typed format, which is most of the reason to prefer it over a flat properties file. Here's how those types land in BoxLang:

TOML typeBoxLang type
StringString
IntegerLong
FloatDouble
BooleanBoolean
ArrayArray
TableStruct (ordered, case-sensitive)
Array of tablesArray of Struct
Inline tableStruct
Offset date-time, local date-time, local date, local timeDateTime

Two things worth calling out.

Integers just work. TOML integers are 64-bit signed. BoxLang auto-promotes numbers across Integer, Long, BigDecimal, and BigInteger as needed, so there is no "int64 mode" toggle to reason about on the read side. On the write side, a value outside TOML's 64-bit integer range raises a TomlSerializationException rather than silently truncating.

Keys are case-sensitive. The TOML spec says "Key" and "key" are distinct keys, so tables always deserialize into case-sensitive structs. This is independent of the ordering setting and is not configurable, because making it configurable would mean producing documents that no longer round-trip through other TOML tooling.

Module Settings

Set your defaults once in boxlang.json and TOML away!

{
	"modules" : {
		"bxtoml" : {
			"settings" : {
				"specVersion"   : "1.0",
				"ordered"       : true,
				"sortKeys"      : false,
				"indent"        : 2,
				"dateTimeStyle" : "auto"
			}
		}
	}
}
SettingDefaultDescription
specVersion"1.0"Target TOML spec version
orderedtrueReturn tables as ordered structs that preserve declaration order
sortKeysfalseAlphabetize keys on serialize instead of preserving struct iteration order
indent2Cosmetic indentation width in spaces
dateTimeStyle"auto"How a BoxLang DateTime is re-emitted: auto, offset-datetime, local-datetime, local-date, or local-time

Every one of these can be overridden per call through the options argument on any of the four BIFs:

toml = tomlSerialize( data, options = { sortKeys : true } )

Global defaults for the common case, local overrides for the exception. No wrapper class required.

Conformance Is Not an Afterthought

Configuration parsing is one of those areas where "mostly correct" is worse than useless, because the failures show up in production at 3am rather than in your tests.

So bx-toml is validated against the official toml-test conformance suite on every single build. That's 210 valid-document fixtures that must parse correctly and 497 invalid-document fixtures that must be rejected. The suite is vendored into the repository and pinned to a known commit, so a green build means a spec-compliant build.

If you're evaluating whether to trust this module with your application configuration, that's the number I'd look at.

What It Does Not Do

I'd rather tell you the limits up front than have you discover them:

Comments are not preserved. Parse a document and re-serialize it, and the comments and original formatting are gone. The data model is value-only, exactly the way jsonDeserialize() and jsonSerialize() behave. If you need to edit a human-maintained TOML file in place while preserving its comments, this is not the tool for that job.

DateTime round-trips are best-effort. TOML has four distinct temporal kinds. BoxLang has one DateTime type. All four collapse into one on parse. On serialize, the auto heuristic picks a sensible form back out based on offset and time-of-day, but a bare local date and a midnight UTC offset-datetime are genuinely indistinguishable once parsed. If you need deterministic output, set dateTimeStyle explicitly.

Line endings are normalized. Both LF and CRLF input parse fine. Output is LF on every operating system.

TOML 1.1 is a placeholder. The specVersion : "1.1" setting exists, but the 1.1 spec is not ratified upstream and currently accepts the same grammar as 1.0.0. Treat it as forward-looking rather than functional today. It's on the fast-follow list.

Go Build Something

box install bx-toml

The module is on ForgeBox and the source is on GitHub. Full docs live at https://boxlang.ortusbooks.com/boxlang-framework/modularity/toml.

If you hit a rough edge, open an issue or come find us at community.ortussolutions.com. This module exists because the ecosystem moved to TOML and BoxLang applications should not have to hand-roll a parser to keep up.

Happy coding!

Add Your Comment

Recent Entries

BoxLang 1.16.0 Released!

BoxLang 1.16.0 Released!

BoxLang 1.16.0 is here, closing 50 issues across new features, improvements, and bug fixes. The theme running through this release is control: control over how HTTP clients are created and reused, control over what happens when a request fails, control over how much data a response is allowed to buffer, control over when Java classpaths reload, and tighter alignment with CFML behavior in the edge cases that only show up in production.

Luis Majano
Luis Majano
August 04, 2026
Ortus & BoxLang July Recap 2026

Ortus & BoxLang July Recap 2026

July continued to showcase the rapid evolution of the Ortus ecosystem with major BoxLang releases, innovative developer tools, and expanded AI capabilities. From new runtime features and cloud-native integrations to practical learning resources and technical deep dives, this month's highlights reflect Ortus' commitment to building modern, high-performance solutions for the JVM.

The month also featured community initiatives, conference resources, and international events, including CFCa...

Victor Campos
Victor Campos
July 31, 2026