Skip to content

Read Java properties

Java .properties files become a config layer through config-properties, a sibling module: the core's own codec is YAML, and this one adds nothing to your dependency graph, because a properties file is read with the standard library. The layer is read-only.

go get gitlab.com/phpboyscout/go/config-properties
import (
    "gitlab.com/phpboyscout/go/config"
    configproperties "gitlab.com/phpboyscout/go/config-properties"
)

store, err := config.NewStore(ctx,
    config.WithFiles(config.OS(), "/etc/app.yaml"),               // real files outrank them
    config.WithBackend(configproperties.New(config.OS(), "/etc/app.properties")), // outranks it
)
if err != nil {
    return err
}

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

with /etc/app.properties containing:

server.host=localhost
server.port=8080

Map properties to keys

A properties key is dotted already, and each dot is a nesting separator, so a properties layer merges into a nested YAML or JSON one by the same paths. In Java a dotted key is a single flat string. Here it nests, which is what makes it a configuration layer rather than a map.

Values are strings, and the read path casts them. When two lines name the same key the later one wins, as in Java. A key that is a prefix of another is refused: Java allows db=x beside db.host=y, but it would make db a value and a parent at once, which this mapping cannot hold. The load fails with config.ErrBackendUnsafe naming the line, in either order. Rename one of the keys.

The format it honours

  • key=value, key:value and key value are all separators. Leading whitespace and whitespace around the separator is ignored.
  • A line starting with # or ! is a comment. A # after a value is part of the value.
  • A line ending in an odd number of backslashes continues onto the next.
  • The standard escapes are recognised in keys and values: \t \n \r \f \\, an escaped separator or space, and \uXXXX.
  • An empty key, or a truncated or invalid \u escape, fails the load with config.ErrBackendParse, naming the file and line.

Read-only, and where writes go

The backend is never a write target. A store.Apply to a key the properties layer provides is routed to the next writable layer and reported shadowed, because the properties value still outranks it. With no writable layer at all, the write fails with config.ErrNoWritableLayer.

Watch for changes

A properties file is watched like any file-backed source: native filesystem notification where it works, polling where it does not, at the interval you pass to Store.Watch with config.WithPollInterval (two seconds by default). See hot-reload.

Test with a string

Codec is exported, so decoding a string needs no store and no filesystem:

docs, err := configproperties.Codec{}.Decode("app.properties", []byte("server.port=8080\n"))
// docs[0]["server"].(map[string]any)["port"] == "8080"

To test through a store, point config.Dir(t.TempDir()) at a file you write first.

Errors you'll see

Error Cause Fix
config.ErrBackendParse An empty key, or a truncated or invalid \u escape. The error names the file and line. Fix the line.
config.ErrBackendUnsafe A key is a prefix of another, such as db beside db.host. The error names the file and line. Rename one of the keys.
config.ErrNoWritableLayer An Apply set a key and no layer in the store can be written. Add a writable layer, such as a YAML file.

When you don't need this

If the file is yours, YAML costs nothing extra.

Reach for .properties when you are sharing configuration with a JVM application that already reads one, so both sides consume the same file rather than one generating the other.

What it costs

Adds nothing beyond the config graph, and an allowlist test in the module fails if that changes

A .properties file is parsed with the standard library, so there is no parser module, and no filesystem library either because you supply the config.FS.