In my day job building software, I have to interact with complex configuration files continually. They are (as of 2025) typically in one of three formats: JSON-with-comments, YAML, or TOML.
Although they enjoy widespread usage, each of these formats is painful to use in practice:
- JSON-with-comments — it’s really hard to comment out a line in a JSON file, because you end up with an extra trailing
,
on the previous line which breaks the syntax. - TOML — why do
[]
and[[]]
do different things but look the same? And how abouta.b = true
vsa = {"b" = true}
. The M in TOML is more like “muddled” than “minimal”. (see also). - YAML — 63 different multiline string formats? tags??, the norway problem (solved if your YAML parser no longer supports YAML v1.1…).
I’ve also seen a few new formats trying to add in types, for loops, and other “just a bit of programming”; but I think that’s completely backwards. If you need to generate a configuration file, use your favorite turing complete programming language.
So, as it was foretold, I created CONL.
The goal was to create a minimal format that felt like the “markdown” of configuration files. It should be:
- Easy to read and edit
- Representing a JSON-like data model
- Easy to implement
And I’m pretty happy with it!
; a conl document defines key value pairs ; values are usually scalars port =8080 ; but can also be lists of values watch_directories =~/go =~/rust ; or maps of keys to values env REGION =us-east1 QUEUE_NAME =example-queue ; multiline scalars work like markdown ; with an optional syntax hint init_script = """bash #!/usr/bin/env bash echo "hello world!"
The format of CONL can now be considered “stable”, so I’d encourage you to start using it!
I have working implementations for Rust, and Go; a langauge server and a Zed extention.
The aggressively small feature set of CONL makes it trivial for you to build your own (you can probably even one-shot-vibe-code the whole thing if you’re into that kind of stuff). If you want to port it to your favorite language, there is a more formal spec, and a re-usable test suite of example CONL documents with their equivalent JSON.
Design decisions (for the curious..)
Given that I want to make something easy to read and easy to parse, it’s clear that it needed a minimal unambiguous syntax.
I was strongly inspired by the INI critique of TOML to avoid the pitfall of syntactic typing. At a casual glance "false"
and false
are the same value (despite the first one being true in Javascript), and so the syntax should not distinguish the two.
On the flip side, INI really falls down (as does TOML to some extent) when you want structure in your document. In practice software configuration is frequently a nested tree, which JSON and YAML handle very nicely; so I wanted to keep that ability.
That led to a data-model where each value is one of scalar|list|map
(Compared to JSON’s null|bool|number|string|object|array
, this felt good). I toyed around with allowing rust-style enums too, and providing syntax to select the enum variant; but although it’s a commonly useful feature in configuration files, it’s not well-supported by programming languages, so it got axed.
The primary limitation of the simplified model is round-tripping isn’t supported. You can convert any JSON document to CONL, or any CONL document to JSON; but JSON -> CONL -> JSON loses type information.
I was also particularly irritated at the time by JSON-with-comments. The most common operation in a config file is “commenting out the stuff I just added to see if that fixes things”. The problem with JSON-with-comments is that it also has commas; and you need to make sure that they exist at the right points. This led me fairly quickly to the requirement that each key would be defined on its own line. Newlines make excellent delimiters. No commas to manage anymore!
Structure
Given that each key should have its own line, my initial approach for nested maps/lists was to use the JSON delimeters. This worked but felt a bit contrived (and also made me really want to add commas to get line-literals like in TOML).
a = {
b = 2
c = [
1
2
I knew from using TOML that I didn’t want two different formats for the same thing (particularly if one version uses newlines as delimeters, and the other uses commas). I also knew that the table syntax (although initially cool) leaves you noticing the lack of structure if you need to do anything more than the top level.
I also noticed, it’s actually unambiguous if you look at the first argument. If the =
is at the start, you have a list, otherwise you have a map. This does complicate things for implementors, but avoids the possibility you’ll mix up [
and {
and get a confused error message.
I was/am worried about indentation. That said, if it’s good enough for Python, I think it’s good enough for me…
The final edge-case is keys with no value. I wanted to make these an error:
a ; error?!
b = c
But, then I realized that if I commented out the contents of a map, that would become an error. So now, if you want to represent a key with no value (or a JSON null
) you can.
Syntax
The first version of CONL used #
as a comment token, but I quickly ran into issues. URLs contain #
, so my next version required the # to be preceded by a space. Then I noticed colors, which start with a #
(and I wanted to fix the parse ickiness of a =#
not being a comment). The other common comment is //
, but that is used in URLs, so I’d have to keep my spacing rule. The answer ultimately came from INI. ;
is rarely used in values found in configuration files; so it became the comment character.
I knew from very early on that I wanted markdown’s multiline strings. So that meant that """
had to be a reserved token. That said, I was initially very against quoted scalars. I wanted to avoid having multiple ways to represent the same thing. So instead, I decided to use "
as the escape character (kind of like how Powershell uses backtick):
"= = "; ; {"=":";"}
After sitting with this for a while, the problem is that (in the already somewhat rare case that you need escapes) you frequently need more than one escape. Embedding a Regex was extremely painful, as you’re not usually on the look-out for = and ; as the “things you need to escape”.
So, to bring CONL more into the mainstream, I switched to double-quoted literals with backslash escapes. To avoid JSON’s UCS-2 problems I didn’t want to support \u0000
(and the C \U0000000
seems unnecessarily verbose given that unicode maces out at 10FFFF). Hence the variable length \{0000}
for codepoints.
; keys and values can contain anything ; except ; (and = for keys). welcome message =Whatcha "%n"! ; types are deferred until parse time; ; the app knows what it wants. enabled =yes country_code =no ; units are encouraged for numeric values timeout =500ms ; if you need an empty string or ; escape sequences, use quotes. empty_string ="" indent =" \t "; the following escape sequences ; work inside quoted literals escape_sequences =" \\ "; '\' =" \" "; '"' =" \t "; tab =" \n "; newline =" \r "; carriage return =" \{1F321} "; 🐱 (or any codepoint)
So long, and thanks for reading!