Blogchevron_rightDevOps
DevOps

Common YAML Pitfalls and How to Avoid Them

YAML promised to be the readable alternative to XML and JSON. It mostly delivered — except for the implicit type coercion, the indent-sensitive grammar, the six multiline string modes, and the country code that turns into a boolean. Here are the bugs and the fixes.

December 10, 2026·8 min read·Validate YAML →

The Norway Problem

The most famous YAML bug starts with a list of country codes:

countries:
  - GB
  - IE
  - FR
  - DE
  - NO   # Norway

Loaded with a YAML 1.1 parser, the value of NO becomes the boolean false. Norway disappears from your dataset. The same parser will turn YES, Y, ON, OFF, TRUE, and FALSE into booleans regardless of casing. This is the origin of the "Norway problem" meme and a real bug that has shipped in production at multiple major companies.

YAML 1.2 (released 2009) restricted the boolean tokens to true and false only. The trouble is that YAML 1.1 is still the default in many parsers — including PyYAML before version 6, which means a huge fraction of the Python ecosystem inherited the bug.

The fix: quote anything that might look like a boolean. - "NO" stays a string. Or upgrade to a parser that defaults to YAML 1.2 (ruamel.yaml with explicit version, go-yaml/v3, JS yaml package).

Indentation: Two Spaces, Never Tabs

YAML uses indentation to denote nesting, exactly like Python. Unlike Python, the YAML spec explicitly forbids tabs as indentation characters. A single tab anywhere in your file is a parse error in conformant YAML 1.2 parsers.

The other indent rule is consistency within a block. Every key at the same nesting level must start at the same column. Mixing two-space and four-space indentation inside the same map produces a parse error or, worse, a subtly wrong tree:

# Wrong — image:tag is indented under "containers" but should be under "- name"
spec:
  containers:
    - name: app
      image: nginx
    image: redis    # this is now a sibling of "containers", not a second container

# Right
spec:
  containers:
    - name: app
      image: nginx
    - name: cache
      image: redis

The defensive habit is to configure your editor to insert spaces (never tabs) for .yml and .yaml files, and to run yamllint on save or in pre-commit. Two-space indentation is the de-facto standard; pick it once and never change.

Six Ways to Write a Multiline String

YAML supports six different multiline string syntaxes, each combining one of two folding modes (| literal, > folded) with one of three trailing-newline modes (default keep one, - strip all, + keep all). The combinations:

SyntaxNameNewline behaviorTrailingUse case
|LiteralNewlines preserved exactlySingle trailing newlineShell scripts, code snippets
>FoldedNewlines folded to spacesSingle trailing newlineLong prose that should re-wrap
|-Literal, stripNewlines preserved exactlyNo trailing newlineExact-byte string content
|+Literal, keepNewlines preserved exactlyAll trailing newlines keptWhen trailing whitespace matters
>-Folded, stripNewlines folded to spacesNo trailing newlineSingle-line value across multiple YAML lines
>+Folded, keepNewlines folded to spacesAll trailing newlines keptRare; usually a mistake
# | preserves newlines, keeps one trailing
script: |
  echo hello
  echo world
# Result: "echo hello\necho world\n"

# > folds newlines into spaces
description: >
  This is a long
  paragraph that will
  re-wrap.
# Result: "This is a long paragraph that will re-wrap.\n"

# |- strips trailing newline
exact: |-
  no newline at the end
# Result: "no newline at the end" (no trailing \n)

In Kubernetes manifests the most common need is | for shell commands and |- for inline secret material. > is rarer in DevOps and shows up mostly in human-edited config like Hugo front matter.

Anchors and Aliases

YAML lets you define a value once with an anchor (&name) and reference it elsewhere with an alias (*name). Combined with the merge key (<<:), this gives you a primitive include/extend mechanism without external files:

defaults: &defaults
  region: us-east-1
  retries: 3
  timeout: 30

production:
  <<: *defaults
  retries: 5     # override

staging:
  <<: *defaults
  region: us-west-2

Two warnings. First, the merge key (<<:) was a YAML 1.1 extension and is not in YAML 1.2. Some modern parsers (go-yaml/v3 in strict mode) reject it. Test your target parser before relying on merge.

Second, anchors and aliases create shared mutable state if your loader produces references rather than deep copies. PyYAML loads aliases as the same Python object — mutating one mutates the other. json.dump-ing the result then crashes on the cycle. The defensive pattern is to deep-copy after load if you intend to mutate.

Implicit Type Coercion

YAML tries to be clever about types. An unquoted token that looks like a number becomes a number. One that looks like a boolean becomes a boolean. One that looks like a date becomes a date. The rules for "looks like" are sometimes surprising:

version: 1.10        # number -> 1.1 (trailing zero LOST)
api_key: 0123456789   # YAML 1.1: octal -> 342391; YAML 1.2: string
country: NO           # YAML 1.1: false; YAML 1.2: "NO"
released: 2026-12-10  # date -> Date object, not the string "2026-12-10"
size: 1G              # YAML: string "1G"; sometimes confused with K8s resource quantities
phone: +1-555-0100    # YAML: string; some parsers stumble on the leading +

The most painful of these in practice is the leading-zero issue. A version field of 1.10 becomes the float 1.1 on parse and the trailing zero is gone forever. Postman, OpenAPI, and Helm all generate version strings; quote them aggressively to keep them strings.

Universal fix: when in doubt, quote. version: "1.10" is unambiguously a string and never coerces.

Special Characters in Strings

YAML has two flavors of quoted string. Single-quoted strings ('text') are literal — no escape sequences are interpreted, and the only escape inside is doubling the single quote (''). Double-quoted strings ("text") interpret backslash escapes like JSON: \n, \t, \uXXXX.

The unquoted (plain scalar) form is the source of every YAML surprise. A plain scalar that begins with @, `, or any of & * ! | > ' " % # is a parse error. A plain scalar containing : followed by space is parsed as a mapping. The rules for what is and is not a legal plain scalar fill several pages of the YAML 1.2 spec.

Defensive rule: if the value contains anything other than alphanumerics, underscores, dashes, dots, or spaces, quote it. Single quotes are simpler unless you actually need backslash escapes; double quotes if you need \n or Unicode escapes.

Kubernetes YAML Gotchas

Kubernetes manifests are the highest-volume YAML in production today. They surface a few extra gotchas worth calling out specifically:

  • warningResource quantities are strings. memory: 512Mi is a string, not a number. Quote it if your editor is being aggressive: memory: "512Mi".
  • warningEnv values are always strings. value: 8080 works at apply time but kubectl edit will silently quote it. Quote from the start: value: "8080".
  • warningMulti-document files use ---. Three dashes alone on a line separate documents. A stray --- in the middle of a manifest creates an empty second document that kubectl apply may complain about.
  • warningHelm templates are YAML-shaped, not YAML. {{ .Values.x }} renders to text and only becomes valid YAML after templating. Use helm template to inspect the rendered output.
  • warningConfigMap data values are strings. port: 8080 in configMap.data is invalid because data values must be strings. port: "8080" works.

Tooling That Catches the Bugs

The best defense against YAML pitfalls is making them impossible to commit. Five tools cover most cases:

ToolPurposeIntegrationNotes
yamllintStyle + indentation rulespip install yamllint, pre-commit hookConfigurable rules; good defaults out of the box
yqQuery / transform YAML like jqgo install / brew installMike Farah variant; Python variant by Andrey Kislyuk also exists
kubeval / kubeconformKubernetes resource validationCI stepkubeconform is the actively maintained fork
helm lintHelm chart structural checkshelm lint ./chartCatches templating errors before install
spectralOpenAPI / AsyncAPI YAML lintingnpm packageCustom rule sets for API contracts

Minimal viable setup: yamllint in pre-commit + kubeconform in CI for any Kubernetes manifest. That alone catches indent bugs, the Norway problem, and 90% of K8s schema violations.

Validate any YAML in your browser

Paste your manifest, get parse errors and type warnings instantly — no upload required.

Open YAML Validator →

Related Tools

Common YAML Pitfalls and How to Avoid Them