Choosing an implementation
In most cases the answer is “the one for the language you are writing in” — that is the point of maintaining four. They share a specification, a test corpus and a three-stage pipeline (lexer, parser, resolver), and behaviour is compared across all four before anything is released.
| Language | Package | Requires | Foreign formats |
|---|---|---|---|
| TypeScript | @o3co/ts.hocon | Node.js 22+ | Subpath exports with optional peer dependencies |
| Go | github.com/o3co/go.hocon | Go 1.23+ | A nested module, so the core keeps its own go.mod clean |
| Rust | hocon-parser | Rust 1.82+ | Cargo features (`adapters-*`), off by default |
| Python | hocon-parser | Python 3.11+ | Submodules with extras (`hocon-parser[yaml]`) |
The Requires column is read from each repository’s package.json, go.mod, Cargo.toml
and pyproject.toml when this page is built, so it cannot drift from the manifests.
What they share
Section titled “What they share”One specification. Every clause of Lightbend’s HOCON.md is a numbered item, and every
item has a recorded status per implementation. Behaviour outside the specification — the
places where the spec is silent and four independent parsers could reasonably disagree — is
pinned separately as a numbered convention, so “undefined” never quietly means “different”.
One corpus. Fixtures and expected values live in xx.hocon and are generated from Lightbend’s own implementation, not hand-written. All four run against it.
One pipeline. Lexer, parser, resolver, in that order, with substitution resolution as a distinct phase after the document is fully parsed. A fix in one port is a diff the other three can be read against.
Where they differ, and why
Section titled “Where they differ, and why”Dependencies. Python is pure standard library. Go and TypeScript carry no runtime
dependencies in the core. Rust carries one (indexmap, for insertion-ordered maps). Every
foreign-format adapter is opt-in in all four, so nothing reaches the core.
Language-forced divergence. A few differences are not choices. JavaScript strings are UTF-16 and can hold an unpaired surrogate, so the TypeScript port accepts one where Rust refuses; Go strings are arbitrary bytes where JavaScript strings arrive already decoded. These are recorded as per-implementation out-of-scope items with the reasoning attached, rather than smoothed over.
Coverage. The four are not at identical points on the checklist at any given moment.
| Implementation | Spec-total | In-scope | ✅ | ⚠️ | ❌ | 🤷 | ➖ |
|---|---|---|---|---|---|---|---|
| ts.hocon | 90.0% | 100.0% | 189 | 0 | 0 | 0 | 21 |
| rs.hocon | 92.9% | 100.0% | 195 | 0 | 0 | 0 | 15 |
| go.hocon | 90.5% | 100.0% | 190 | 0 | 0 | 0 | 20 |
| py.hocon | 88.8% | 96.6% | 180 | 13 | 0 | 0 | 17 |
Two denominators are reported because one alone misleads. Spec-total divides by every item in the specification, including ones an implementation deliberately does not support, and answers “how much of HOCON does this handle?”. In-scope drops that implementation’s out-of-scope items and answers “of what it undertakes, how much is covered?”. Neither is the flattering number by construction.
Beyond parsing
Section titled “Beyond parsing”Every implementation can also read JSON with comments, TOML, YAML and Java .properties into
the same configuration object, so a single API covers a mixed repository —
see format adapters.
If the consumer of your configuration cannot be changed at all, hocon2 converts HOCON to those formats and back from the command line.