A pure-Scala, cross-platform implementation of HOCON
(Human-Optimized Config Object Notation) — the config format popularized by Lightbend's
com.typesafe:config.
Unlike com.typesafe:config, which is JVM-only, hocon is written in pure Scala 3 with no
java.* dependencies in its core, so the same parser runs on the JVM, in the browser/Node.js
via Scala.js, and as a native binary via Scala Native.
Status: Phases 1–4 complete — the lexer, parser, untyped
ConfigAPI, object merging, substitutions, value concatenation, and durations/sizes are in and tested on all three platforms.includedirectives and a typed (case-class) decoder follow. See the roadmap below.
import io.github.edadma.hocon.*
val config = Hocon.parse("""
en {
greeting = "Hello, world"
nav { home = "Home", about = "About" }
cart.items = "{count} items" # path-expression key
}
""")
config.getString("en.greeting") // "Hello, world"
config.getString("en.nav.home") // "Home"
config.getConfig("en").getInt("...")
config.hasPath("en.missing") // false
// i18n placeholder helper
val msg = Messages(config.getConfig("en"))
msg("cart.items", "count" -> 3) // "3 items"A partial config falls back to a base — perfect for a base locale with per-locale overrides:
val base = Hocon.parse("""greeting = "Hello", farewell = "Goodbye"""")
val fr = Hocon.parse("""greeting = "Bonjour"""")
val messages = Messages(fr.withFallback(base))
messages("greeting") // "Bonjour" (translated)
messages("farewell") // "Goodbye" (from base)
// Merge several layers; later arguments win:
val effective = Hocon.load(base, fr, userOverrides)Objects merge recursively; arrays and scalars replace. A null in the override shadows (unsets)
the fallback value at that key.
${path} references another value in the same document; ${?path} is optional and disappears when
nothing is found. Substitutions resolve against the merged root, so they are order-independent.
val config = Hocon.parse("""
host = localhost
url = ${host} # → "localhost"
service = ${defaults} # copies the whole object
defaults { timeout = 30 }
""")
// A missing required substitution throws; an optional one is simply absent:
Hocon.parse("x = ${?missing}").hasPath("x") // false
// Fall back to the environment for anything not in the config:
Hocon.parse("home = ${HOME}", EnvSource.fromMap(sys.env))Circular references throw CircularReferenceException.
Pieces written side by side with only whitespace between them concatenate. Strings, numbers, and substitutions join into one string (interior whitespace preserved); arrays concatenate element-wise; objects deep-merge left to right.
val config = Hocon.parse("""
host = example.com
port = 8080
url = "http://"${host}":"${port} // → "http://example.com:8080"
xs = [1, 2] [3, 4] // → [1, 2, 3, 4]
""")Read unit-suffixed values with getDuration (a cross-platform FiniteDuration) and getBytes
(a Long):
val config = Hocon.parse("timeout = 10s, cache = 512K")
config.getDuration("timeout") // 10.seconds
config.getBytes("cache") // 524288Duration units are ns/us/ms/s/m/h/d (bare number = milliseconds); size units
distinguish powers of 1024 (K, Ki, KiB) from powers of 1000 (kB, MB).
A note on quoting: HOCON allows unquoted strings, but forbids the characters
$ " { } [ ] : = , + # ^ ? ! @ * &and` inside them. Most UI strings (Hello, world,
Are you sure?, {count} items) hit one of these, so quote your translation strings. This is
spec-correct HOCON, not a limitation of this library.
HOCON is a comfortable format for configuration and, in particular, for internationalization (i18n) message files — nested, commented, human-friendly:
# messages.en.conf
en {
greeting = "Hello, world"
nav { home = "Home", about = "About" }
cart.items = "{count} items" # path-expression key
}Until now there has been no good way to read these on Scala.js or Scala Native. That's the gap hocon fills.
| Phase | Scope | Status |
|---|---|---|
| 1 | Lexer + parser → untyped Config (comments, quoted/unquoted strings, nested objects, path-expression keys, arrays). i18n-usable. |
✅ |
| 2 | Object merging + withFallback (base locale + overrides). |
✅ |
| 3 | Substitutions: ${path}, ${?path}, env fallback, cycle detection. |
✅ |
| 4 | Value concatenation + durations (10s) and sizes (512K, 10MB). |
✅ |
| 5 | include directives behind a pluggable, per-platform IO source. |
|
| 6 | Typed decoder with case-class derivation (config.as[A]). |
|
| 7 | Conformance against the Typesafe spec's test corpus. |
sbt hoconJVM/test
sbt hoconJS/test
sbt hoconNative/testISC — see LICENSE.