yq — Gotchas

Dotted Keys

This is the most common yq trap. yq interprets dots as path separators by default.

Dotted key — WRONG vs CORRECT
cat <<'YAML' > /tmp/hosts.yml
hosts:
  gitlab.com:
    token: ghp_xxxx
    protocol: https
YAML
# WRONG — yq traverses hosts → gitlab → com
yq '.hosts.gitlab.com.token' /tmp/hosts.yml
# Output: null (wrong path)

# CORRECT — bracket notation preserves the literal key
yq '.hosts["gitlab.com"].token' /tmp/hosts.yml
# Output: ghp_xxxx

Rule: any key containing ., /, -, or special characters needs bracket notation. Hyphens in antora.yml attributes: ["ise-ip"] not .ise-ip.

Testing bracket notation on a safe field first
yq '.asciidoc.attributes["attribute-missing"]' docs/antora.yml
# Output: warn@

# Confirm it works before running on credential files

!!null Tag

YAML’s !!null tag explicitly sets a value to null. It is NOT the same as a missing key.

Detect null values
cat <<'YAML' > /tmp/test.yml
active: true
deprecated: !!null
name: test
version: ~
YAML
yq -r 'to_entries[] | select(.value == null) | .key' /tmp/test.yml
# Output:
# deprecated
# version

Both !!null and ~ produce null. has("deprecated") returns true — the key exists, it just holds null.

The !!null gotcha in glab/gh config
cat <<'YAML' > /tmp/config.yml
hosts:
  github.com:
    oauth_token: !!null
    git_protocol: ssh
YAML
yq '.hosts["github.com"].oauth_token' /tmp/config.yml
# Output: null

# Check if it is explicitly null vs missing
yq '.hosts["github.com"] | has("oauth_token")' /tmp/config.yml
# Output: true (key exists but is null — this is intentional)

If oauth_token shows !!null, it was deliberately cleared. Remove the key entirely if it is truly unwanted: yq -i 'del(.hosts["github.com"].oauth_token)' config.yml.

String vs Number

YAML autodetects types. Unquoted numbers become integers; quoted ones stay strings.

Type coercion demonstration
cat <<'YAML' > /tmp/types.yml
port_number: 443
port_string: "443"
version: 1.0
flag: true
YAML
yq '.port_number | type' /tmp/types.yml
# Output: !!int

yq '.port_string | type' /tmp/types.yml
# Output: !!str

yq '.version | type' /tmp/types.yml
# Output: !!float

yq '.flag | type' /tmp/types.yml
# Output: !!bool
Why antora.yml quotes port values
yq '.asciidoc.attributes["port-https"] | type' docs/antora.yml
# Output: !!str

# Antora attributes are ALWAYS strings — quoting ports prevents
# YAML from parsing "443" as integer 443, which Antora would reject.
The Norway problem — unquoted "no" becomes boolean false
cat <<'YAML' > /tmp/norway.yml
countries:
  - name: Norway
    code: NO
  - name: Sweden
    code: SE
YAML
yq '.countries[0].code' /tmp/norway.yml
# Output: false (YAML 1.1 interprets NO as boolean false!)

# Fix: quote it
cat <<'YAML' > /tmp/norway-fixed.yml
countries:
  - name: Norway
    code: "NO"
  - name: Sweden
    code: "SE"
YAML
yq '.countries[0].code' /tmp/norway-fixed.yml
# Output: "NO"

YAML 1.1 treats yes, no, on, off, y, n as booleans. Always quote values that might collide.

yq vs jq Differences

yq reads YAML and JSON; jq reads only JSON
# yq handles both
yq '.name' docs/antora.yml           # YAML — works
echo '{"name":"test"}' | yq '.name'  # JSON — works

# jq handles only JSON
echo '{"name":"test"}' | jq '.name'  # JSON — works
jq '.name' docs/antora.yml           # YAML — parse error
Bridge: yq to jq pipeline
yq -o json '.' docs/antora.yml | jq '.asciidoc.attributes | keys | length'
# Use yq to convert YAML→JSON, then jq for complex JSON processing
Syntax differences
# yq uses the same expression language as jq, but:
# 1. yq preserves YAML comments; jq does not handle comments
# 2. yq supports YAML tags (!!str, !!int); jq has no equivalent
# 3. yq's -i flag edits YAML in place; jq has no -i flag (use sponge or temp file)
# 4. yq can output YAML, JSON, XML, CSV, TSV, props; jq outputs only JSON

Credential Safety

NEVER dump full credential files
# WRONG — exposes all tokens
yq '.' ~/.config/gh/hosts.yml

# CORRECT — inspect keys only
yq 'keys' ~/.config/gh/hosts.yml 2>/dev/null

# CORRECT — test path syntax on safe fields
yq -r '.hosts["github.com"].git_protocol' ~/.config/gh/hosts.yml 2>/dev/null
Safe inspection pattern for credential files
# 1. List top-level structure
yq 'keys' file.yml

# 2. Check specific non-secret fields
yq '.field_name | type' file.yml

# 3. Never pipe credential files to other commands
# 4. Never redirect credential file output to disk

YAML Anchors and Aliases

Anchors (&) define reusable values; aliases (*) reference them
cat <<'YAML' > /tmp/anchors.yml
defaults: &defaults
  adapter: postgres
  host: localhost
  port: 5432

development:
  <<: *defaults
  database: dev_db

production:
  <<: *defaults
  database: prod_db
  host: db.prod.internal
YAML
yq '.production' /tmp/anchors.yml
# Output:
# adapter: postgres
# host: db.prod.internal
# port: 5432
# database: prod_db

yq resolves anchors on read. The <<: *defaults merge key injects the anchor’s content. Production overrides host — the last definition wins.

Detect anchors in a file
cat <<'YAML' > /tmp/anchors.yml
defaults: &defaults
  timeout: 30
service_a:
  <<: *defaults
  name: alpha
service_b:
  <<: *defaults
  name: beta
YAML
yq '.. | select(anchor != "")' /tmp/anchors.yml 2>/dev/null
# Inspects anchor metadata — useful for auditing shared config

Multiline Strings

Literal block scalar (|) preserves newlines
cat <<'YAML' > /tmp/multi.yml
script: |
  #!/bin/bash
  echo "line 1"
  echo "line 2"
YAML
yq -r '.script' /tmp/multi.yml
# Output:
# #!/bin/bash
# echo "line 1"
# echo "line 2"
Folded block scalar (>) joins lines
cat <<'YAML' > /tmp/multi.yml
description: >
  This is a long
  description that
  gets folded into
  one line.
YAML
yq -r '.description' /tmp/multi.yml
# Output: This is a long description that gets folded into one line.

The | and > indicators control how yq interprets whitespace. Be aware of trailing newlines: | preserves them, |- strips the trailing newline.