Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

214 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

go.hocon — HOCON Parser for Go

Go Reference Go Report Card CI Lint codecov License

A Lightbend HOCON parser for Go. See Spec Compliance for the current conformance rate.

Implemented by Claude (Anthropic) — designed and built end-to-end with Claude Code. Reviewed by GitHub Copilot and OpenAI Codex.

日本語

Library stance -- go.hocon is a HOCON config loader, not a low-level parser API. Its purpose is reading .hocon files and providing typed access via the Config API (GetString, GetInt, GetFloat64, GetBool, GetDuration, GetBytes, Unmarshal). Internal types such as ScalarVal may change between minor versions.

Cross-language conformance -- This implementation is tested against shared expected-JSON fixtures from o3co/xx.hocon alongside ts.hocon, rs.hocon, and py.hocon to ensure all four implementations meet the same Lightbend HOCON specification.


Quick Start

1. Install

go get github.com/o3co/go.hocon

Requires Go 1.23+.

2. Use

import "github.com/o3co/go.hocon"

cfg, err := hocon.ParseString(`
  server {
    host = "localhost"
    port = 8080
  }
`)
if err != nil {
    log.Fatal(err)
}

host := cfg.GetString("server.host")  // "localhost"
port := cfg.GetInt("server.port")     // 8080

Why HOCON?

.env JSON YAML HOCON
Comments No No Yes Yes
Nesting No Yes Yes Yes
References / Substitution No No No Yes (${var})
File inclusion No No No Yes (include)
Object merging No No Anchors (fragile) Yes (deep merge)
Optional values No No No Yes (${?var})
Trailing commas N/A No N/A Yes
Unquoted strings Yes No Yes Yes

HOCON isn't just a serialization format — it's a config-injection language. JSON, YAML, and TOML describe data structures and leave file layering, environment variables, and reference resolution to your code (Pydantic, Serde, Zod, etc.). HOCON bakes those into the spec itself: by the time your program reads the config, fallback files are merged and ${VAR} references resolved into a single composed object. Conditional branching from "is this value present in this layer?" disappears at the format boundary.

On top of that, HOCON combines the readability of YAML with the structure of JSON — making it a strong fit for anything beyond flat key-value config.

Features

  • Full HOCON parsing: objects, arrays, scalars, substitutions (${path}, ${?path})
  • Self-referential substitutions (path = ${path} ["/extra"])
  • Deep-merge for duplicate keys (last definition wins)
  • += append operator
  • include "file.conf" and include file("file.conf") directives
  • Triple-quoted strings ("""...""")
  • Duration parsing (10ms, 2s, 1h, 1d)
  • Byte size parsing (1KB, 1KiB, 1MB, …)
  • Generic Option[T] for safe optional access
  • Struct unmarshalling with hocon struct tags
  • Deferred resolution: separate Parse / WithFallback / Resolve lifecycle for runtime-supplied substitution values (v1.4.0+)
  • FromMap / Empty value factories for in-memory fallback layers (v1.4.0+)
  • Format adapters for config owned by other programs — Properties, env, JSONC, TOML, YAML (see below)
  • No external runtime dependencies — gopkg.in/yaml.v3 used only in test code (fixture scenario runner)

API

Parsing

hocon.ParseString(input string) (*Config, error)
hocon.ParseFile(path string)    (*Config, error)

// Deferred parse (v1.4.0+): returns an unresolved Config when the input
// contains `${...}` and you want to layer fallback values via WithFallback
// before resolving.
hocon.ParseStringWithOptions(input string, opts hocon.ParseOptions) (*Config, error)
hocon.ParseFileWithOptions(path string,    opts hocon.ParseOptions) (*Config, error)
hocon.FromMap(values map[string]any, originDescription string)     (*Config, error)
hocon.Empty(originDescription string)                              *Config

Deferred resolution example (v1.4.0+)

Use when a substitution references a value only available at runtime (e.g. CI build number) — see #99.

cfg, _ := hocon.ParseStringWithOptions(
    `version = ${shortversion}-${CI_RUN_NUMBER}
     variables { shortversion = "1.2.3" }`,
    hocon.DefaultParseOptions().WithResolveSubstitutions(false),
)
runtime, _ := hocon.FromMap(map[string]any{"CI_RUN_NUMBER": "42"}, "")
vars := cfg.GetConfig("variables")
resolved, _ := cfg.
    WithFallback(runtime).
    WithFallback(vars).
    Resolve(hocon.DefaultResolveOptions())
fmt.Println(resolved.GetString("version")) // "1.2.3-42"

Use ResolveOptions.WithUseSystemEnvironment(false) for hermetic resolution (no os.Environ fallback) or .WithAllowUnresolved(true) for partial resolve (unresolved paths remain placeholders; getter on them returns hocon.ErrNotResolved).

Scalar Getters

Method Returns Panics if
GetString(path) string missing, null, wrong type
GetInt(path) int missing, null, wrong type
GetInt64(path) int64 missing, null, wrong type
GetFloat64(path) float64 missing, null, wrong type
GetFloat32(path) float32 missing, null, wrong type
GetBool(path) bool missing, null, wrong type
GetDuration(path) time.Duration missing, null, invalid format
GetBytes(path) int64 missing, null, invalid format

Each has a corresponding GetXxxOption(path) Option[T] variant that returns None instead of panicking.

Slice Getters

cfg.GetStringSlice(path)   []string
cfg.GetInt64Slice(path)    []int64
cfg.GetIntSlice(path)      []int
cfg.GetConfigSlice(path)   []*Config

Each has a GetXxxSliceOption variant.

Object Access

sub := cfg.GetConfig("server")          // *Config scoped to "server"
opt := cfg.GetConfigOption("server")    // Option[*Config]

Inspection

cfg.Has("server.host")  // true even for null values
cfg.Keys()              // direct child keys, in declaration order

Fallback Merge

merged := overrides.WithFallback(defaults)
// overrides win; defaults fill in missing keys

Option[T]

opt := cfg.GetStringOption("key")
if opt.IsSome() {
    v, _ := opt.Get()
}
v := opt.OrElse("default")

Unmarshal

type ServerConfig struct {
    Host    string        `hocon:"host"`
    Port    int           `hocon:"port"`
    Timeout time.Duration `hocon:"timeout,omitempty"`
    Tags    []string      `hocon:"tags"`
}

var s ServerConfig
err := cfg.Unmarshal(&s)

// map[string]any also supported
m := make(map[string]any)
err = cfg.Unmarshal(&m)

Fields without a hocon tag use the lowercased field name. omitempty preserves the pre-populated value when the key is missing.

Error Types

var pe *hocon.ParseError   // lexing/parsing failure — has Line, Col, FilePath
var re *hocon.ResolveError // substitution/include failure — has Path
var ce *hocon.ConfigError  // GetXxx panic payload — has Path

HOCON Examples

# Comments with # or //
database {
  host = "db.example.com"
  port = 5432
  url  = "jdbc:"${database.host}":"${database.port}  // substitution + concat
}

# Duplicate keys deep-merge (last wins for scalars)
server { host = localhost }
server { port = 8080 }      // result: { host: localhost, port: 8080 }

# Self-referential append
path = ["/usr/bin"]
path = ${path} ["/usr/local/bin"]  // ["/usr/bin", "/usr/local/bin"]

# += shorthand (appends the value as a single element: a += b ≡ a = ${?a} [b])
items = [1]
items += 2        // [1, 2]
items += [3, 4]   // [1, 2, [3, 4]]  (an array RHS is appended as one nested element)

# Include
include "defaults.conf"
include file("overrides.conf")

# Duration and byte sizes
timeout   = "30s"
cache-ttl = "5m"
max-size  = "512MiB"

Performance

Measured on Apple M4 Pro with go test -bench (built-in Go benchmark framework). Each iteration includes parsing and a GetString lookup. Run go test -bench=. -benchmem ./... to reproduce.

Scenario ops/sec Time per op
Small config (10 keys) ~173,000 ~5.8 µs
Medium config (100 keys) ~27,000 ~38 µs
Large config (1,000 keys) ~2,000 ~422 µs
10 substitutions ~70,000 ~14 µs
50 substitutions ~16,000 ~61 µs
100 substitutions ~8,000 ~121 µs
Depth 5 nesting ~168,000 ~6.0 µs
Depth 10 nesting ~108,000 ~9.3 µs
Depth 20 nesting ~64,000 ~15.6 µs

For typical application configs (loaded once at startup), the parsing cost is negligible — even a 1,000-key config parses in under 1 ms.

Comparison

✅ Full support / ⚠️ Partial / ❌ Not supported

HOCON Implementation

Verified against gurkankaymak/hocon v1.3.0 on 2026-07-26.

Feature go.hocon gurkankaymak/hocon v1.3.0
Substitutions (${path})
Optional substitutions (${?path})
Include
include required(...) / file(...)
Object/Array concatenation
Type coercion
Duration parsing (30s, 5m)
Byte size parsing (512MB) ❌ (parses as a string)
+= append
Struct unmarshal
Non-panicking access ✅ (GetXxxE + Option[T]) ✅ (GetXxxE)
Env variable fallback
Deferred resolution (parse / withFallback / resolve) ✅ (v1.4.0) ✅ (v1.3.0)
FromMap / Empty value factories ✅ (v1.4.0)

Config Framework

Compared against viper v1.21.0 on 2026-07-26.

go.hocon viper v1.21.0
Formats
HOCON
JSON
YAML ✅ (adapters/yaml)
TOML ✅ (adapters/toml)
Env vars ✅ (fallback + adapters/env)
.properties ✅ (via include + adapters/properties)
Features
Substitutions
File includes
Type coercion
Struct unmarshal
Watch/reload
Remote config

Spec Compliance

Conformance against the Lightbend HOCON specification is tracked at item granularity in docs/spec-compliance.md, which is the source these rates are computed from — TestDocs_ReadmeComplianceRates recomputes them and fails the build if this table drifts. See xx.hocon/docs/compliance-matrix.md for the cross-implementation roll-up.

Metric Status
Spec total (incl. out-of-scope) 89.0%
In-scope only 98.4%
Lightbend equiv01equiv05 + test01test13 passing (TestLightbendEquiv / TestLightbendSuite; three *-expected.json comparisons carry documented environment-dependent exclusions)
hocon2 conformance (JSON/YAML/TOML/Properties output) passing

Format adapters

Config files that belong to other programs can be mounted as HOCON, so a ${...} in your document can reach into them.

adapters/ is a separate Go module — that is how the parser keeps zero dependencies — so it has an install of its own:

go get github.com/o3co/go.hocon/adapters
import (
    "github.com/o3co/go.hocon"
    "github.com/o3co/go.hocon/adapters/env"
)

// APP_DB__HOST=db.internal  ->  db.host
base, _ := env.Load(env.Options{Prefix: "APP_"})

// Substitutions must stay unresolved until the fallback is attached.
cfg, _ := hocon.ParseFileWithOptions("app.conf",
    hocon.DefaultParseOptions().WithResolveSubstitutions(false))

merged, _ := cfg.WithFallback(base).Resolve(hocon.ResolveOptions{})
# app.conf — ${db.host} resolves against the mounted environment
url = "postgres://"${db.host}":"${db.port}

Importing the parser still pulls in nothing; only the adapter you import brings a dependency.

Package Notes
adapters/properties java.util.Properties, sharing this parser's include syntax layer
adapters/env Bulk-mounts a prefixed namespace; also reads .env files
adapters/jsonc JSON with comments and trailing commas; no dependency
adapters/toml via pelletier/go-toml/v2
adapters/yaml via goccy/go-yaml

Versioning

The adapters module will carry its own tags, adapters/vX.Y.Z, pairing with the core version they are built against — the first will be adapters/v1.10.x. None is pushed yet, so go get currently resolves a pseudo-version from the default branch. Either way its go.mod names the core version whose API it uses, and go get brings that core with it.

Plain JSON needs no adapter — HOCON is a JSON superset, so hocon.ParseFile accepts a .json file as it stands. Reading a single environment variable needs none either; that is what ${?VAR} is for.

Foreign data stays data: a ${a.b} in a mounted value is literal text, never a reference, because the file belongs to a program that never agreed to HOCON's syntax.

Where the adapters are strict

Ingestion refuses input whose meaning would otherwise depend on something the caller cannot see — the order a Go map happens to iterate in, or text a decoder happens to stop before:

  • JSONC holds exactly one value. Whitespace and comments may follow it; anything else, including a stray closer such as {"a":1} }, is an error rather than silently ignored text.
  • JSONC comments separate tokens. A comment becomes whitespace, not nothing, so 1/*x*/2 is a syntax error rather than the number 12.
  • YAML keys that stringify alike collide. A non-string scalar key takes its string form, so 1.0: and "1":, or ~: and "null":, are an error naming both spellings and their lines rather than one value quietly winning. A <<: merge key is exempt — it legitimately supplies a key the mapping overrides. The same applies to a tree handed to yaml.FromValue.

For YAML, scalar resolution belongs to the library rather than to this module — whether 010 is 8 or 10 is goccy's answer, not a guarantee here. yaml.FromValue takes an already-decoded tree, so a caller who needs a different library or schema decodes it themselves and hands the result over. See adapters/README.md.

Related Projects

Project Language Registry Description
ts.hocon TypeScript npm HOCON parser for TypeScript/Node.js
rs.hocon Rust crates.io HOCON parser for Rust
py.hocon Python PyPI HOCON parser for Python
hocon2 Go pkg.go.dev HOCON → JSON/YAML/TOML/Properties CLI

The four parser implementations (ts.hocon, rs.hocon, go.hocon, py.hocon) are all tracked against the same Lightbend HOCON spec — see the cross-impl roll-up for per-impl conformance rates.

Best Practices

Config Structure

  • Split by domain: Separate configuration into logical units (database.conf, server.conf, logging.conf)
  • Use include for composition: Compose a full config from domain-specific files
  • Avoid logic in config: HOCON is for declarative data, not conditionals or computation

Environment Variables

  • Minimize ${ENV} usage: Prefer ${?ENV} (optional) with sensible defaults defined in the config itself
  • Never require env vars for local development: Defaults should work out of the box
  • Document required env vars: List them in your project's README or a .env.example

Dev / Prod Separation

config/
├── application.conf    # shared defaults
├── dev.conf            # include "application.conf" + dev overrides
└── prod.conf           # include "application.conf" + prod overrides

Validation

  • Always validate config at application startup, not at point-of-use
  • Use schema validation (Zod for TypeScript, struct unmarshalling for Go, Serde for Rust) to catch errors early
conf, err := hocon.ParseString(`
server {
  host = "localhost"
  port = 8080
}
debug = true
`)
if err != nil {
    log.Fatal(err)
}

var app struct {
    Server struct { Host string; Port int } `hocon:"server"`
    Debug  bool                             `hocon:"debug"`
}
if err := conf.Unmarshal(&app); err != nil {
    log.Fatal(err) // fails fast on startup
}

Known Limitations

  • include url(...) is not supported. Fetching remote configuration is outside the scope of this parser. Use your application's HTTP client to fetch the content, then pass it to ParseString().
  • include classpath(...) is not supported. This is a JVM-specific include form with no equivalent outside Java runtimes.
  • No watch/reload — the library parses config at load time. For live-reloading, re-call ParseString() or ParseFile() on change.
  • No streaming parser — the entire input is loaded into memory.

For full API documentation, see pkg.go.dev.

Security Considerations

When parsing untrusted HOCON input, be aware of:

  • Path traversal in includes: include "../../../etc/passwd.conf" will resolve relative to BaseDir. Validate include paths if parsing untrusted input.
  • Input size: The parser has no built-in input size limit. For untrusted input, validate size before calling ParseString().

License

Apache License 2.0 — see LICENSE.

Copyright 2026 1o1 Co. Ltd.

About

Full Lightbend HOCON spec-compliant parser and config library for Go

Topics

Resources

Contributing

Security policy

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages