Skip to content
datahacks.dev
YAML

Naming Keys Consistently in YAML Config Files Everywhere

Pick one key style, enforce it with a JSON Schema pattern and fix old files with a yq command that renames every key. Real output for each step.

6 min read
Summarize in AI
Flat illustration of a tree of nested objects beside the document it came from, with the YAML logo.

Configuration keys are an interface. People type them from memory, scripts look them up, code reads them by name, and a typo is silent: retries and Retries and max-retries are three different keys, and the program that wanted one finds none and uses a default. A consistent style removes a class of mistakes and makes a file predictable to read. This post chooses a style, enforces it with a schema, and migrates an inconsistent file with one yq command, all with real output.

Choosing a style

The common styles are snake_case (max_retries), kebab-case (max-retries), camelCase (maxRetries) and PascalCase. Pick by what your readers and consumers use.

  • snake_case maps cleanly to Python, Ruby and SQL identifiers, works as an environment variable name in upper case (MAX_RETRIES) and has no characters that need quoting.

  • kebab-case is common in tools' own files (CI, Kubernetes labels). It is not a valid identifier in most languages, so a loader must translate it.

  • camelCase is natural for JavaScript and Java consumers, and for Kubernetes API objects, whose field names are camelCase.

For application settings read by your own code, I prefer snake_case, and I write that choice in the file header or the contributing notes. The more important decision is to have one. Also decide: singular or plural for collections (hosts for a list), how to name booleans (tls or tls_enabled, and never negatives like disable_x), how to include units (timeout_seconds or a comment), and the order of keys (identifying key first).

Avoid keys that YAML 1.1 parsers read as booleans or nulls: no, on, yes, off, y, n and null/~ as bare keys. A key named on becomes True in PyYAML (the Norway problem and YAML boolean surprises has the collision example), so name it enabled or trigger. Avoid keys that start with a digit or contain spaces and colons, which force quoting.

Enforcing it

A linter for formatting does not check names, and yamllint has no rule for key style. A JSON Schema can, with propertyNames:

naming.jsonJSON
{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "type": "object",
  "propertyNames": {"pattern": "^[a-z][a-z0-9]*(_[a-z0-9]+)*$"},
  "additionalProperties": {
    "if": {"type": "object"},
    "then": {"propertyNames": {"pattern": "^[a-z][a-z0-9]*(_[a-z0-9]+)*$"}}
  }
}

propertyNames constrains the key names of an object; the additionalProperties branch applies the same check to nested objects (this simple schema covers two levels; a recursive $ref extends it to any depth). The pattern means lowercase words joined by single underscores. Running it on a file that mixes styles:

YAML
serverHost: a
max-retries: 3
log_level: info
DB:
  Port: 1
  user_name: x
Text
names_bad.yaml::$: 'serverHost' does not match '^[a-z][a-z0-9]*(_[a-z0-9]+)*$'
names_bad.yaml::$: 'max-retries' does not match '^[a-z][a-z0-9]*(_[a-z0-9]+)*$'
names_bad.yaml::$: 'DB' does not match '^[a-z][a-z0-9]*(_[a-z0-9]+)*$'
names_bad.yaml::$.DB: 'Port' does not match '^[a-z][a-z0-9]*(_[a-z0-9]+)*$'

Four offenders, each with the object it belongs to. The correct file (server_host, max_retries, log_level, db with port and user_name) printed ok -- validation done. In practice the naming check is usually combined with the application schema, where each known property is listed by name and additionalProperties: false rejects any other spelling. That is stricter: only the keys that exist are allowed, which catches maxRetries by being absent from the list. Use propertyNames for files where keys are open-ended (a mapping of service names) and the explicit property list where they are known.

Fixing an old file

To migrate, rename keys in the files and in the code that reads them. For the files, yq can rewrite every key in every mapping, recursively, including inside lists:

Shell
yq '(.. | select(tag == "!!map")) |= with_entries(.key |= (sub("([a-z0-9])([A-Z])", "${1}_${2}") | downcase))' camel.yaml

On this input:

YAML
ServerHost: a
Db:
  MaxConn: 5
  Name: x
List:
  - {Id: 1}

the output:

Text
server_host: a
db:
  max_conn: 5
  name: x
list:
  - {id: 1}

The expression finds every mapping (.. | select(tag == "!!map")), and for each rewrites its keys: the sub inserts an underscore between a lowercase letter or digit and a capital (ServerHost to Server_Host) and downcase lowercases the result. MaxConn became max_conn, and the key inside the list item became id. A simpler downcase alone gave serverhost, which loses the word boundary; the underscore insertion matters. Run it on a copy and diff, and check keys that contain acronyms (DBHost becomes dbhost, since there is no lowercase-then-capital boundary before H), digits and hyphens, which need extra rules (kebab-case to snake_case is sub("-", "_")). For a handful of files, edit by hand with the schema's error list in front of you.

Migrating without breaking readers

A key rename is a contract change. The program that reads serverHost will not find server_host. Migrate in steps:

  1. Make the reader accept both names for a transition, preferring the new one and logging a deprecation warning for the old. A small translation table in the loader does it.

  2. Rewrite the files with the yq command, and run the application's tests against them.

  3. Switch the schema to allow only the new names.

  4. Remove the old-name support after every environment has moved.

For files owned by other teams or customers, give a date and keep the compatibility layer until they have moved. A one-line notice in the log when the old name is used tells you who has not.

Where the convention stops

Your convention covers your files. Names that belong to other tools are theirs: Kubernetes fields are camelCase, Docker Compose keys are snake_case and kebab-case in different places, GitHub Actions keys are kebab-case and snake_case mixed (runs-on, timeout-minutes, if). Do not rename them, and do not run the rename expression over those files. Say in the convention where it applies: "application settings under config/, not CI or Kubernetes manifests". Where your file embeds an external structure, as in a field that holds a Kubernetes snippet, keep the external names inside it.

Document the rule in the contributing notes with an example of a good and a bad key, and let the schema do the nagging.

The cluster hub is structuring YAML configuration files that last. Schema checks are covered in validating config YAML with JSON Schema in CI pipelines, and the YAML edit mechanics in using yq to update many YAML files at once. The propertyNames keyword is described in the JSON Schema object reference, and Kubernetes' camelCase field names in its API conventions. Reformat a renamed file in the YAML formatter.

When a key naming convention cannot be enforced

The keys belong to another tool. Follow the tool.

Keys are data. A mapping from customer names to settings has arbitrary keys. Constrain the shape of the values, not the names.

Old readers exist that you do not control. Keep both spellings until they move, or version the file format.

The team has not agreed. A convention imposed by a script and resented by the people editing the files lasts until the first deadline. Agree first, then automate.

Share

Keep reading

Get new articles by email

We send one email when a new article is published. No spam.

Unsubscribe in one click. No spam, ever.