BytePane

YAML Validator: Check & Fix YAML Syntax Online

DevOps13 min read

The CI Pipeline Was Fine — Until It Wasn't

It's 4pm on a Friday. A developer pushes a change to a Kubernetes deployment manifest. The CI/CD pipeline starts. Three minutes later, kubectl apply fails with: error: error parsing deployment.yaml: error converting YAML to JSON: yaml: line 47: did not find expected ':'.

The culprit: a two-space indentation became three spaces on one line due to a copy-paste from a Stack Overflow answer. YAML parsed it as a different structure entirely. The actual syntax error was five lines above line 47 — YAML's error reporting is notoriously imprecise about the true location of the problem.

This scenario plays out constantly in teams that rely on YAML-heavy tooling. According to the CNCF Annual Survey 2025, Kubernetes is used in production by 84% of respondents — and every Kubernetes workload is defined in YAML. GitHub Actions, the most popular CI platform per State of the Octoverse 2025 with over 90 million workflow runs daily, is entirely YAML-configured. Helm charts, Ansible playbooks, Docker Compose files — all YAML.

YAML validation is not optional infrastructure. It's a prerequisite for stable deployments.

Key Takeaways

  • YAML validation has two distinct layers: syntax validation (is the YAML parseable?) and schema validation (does the parsed data match the expected structure?).
  • The most dangerous YAML errors are silent type coercions — "NO" becoming false, "0755" becoming integer 493, unquoted version strings becoming floats.
  • yamllint catches syntax and style problems parsers ignore: duplicate keys, trailing spaces, missing document start markers.
  • For Kubernetes manifests, use kubeconform — it validates against official Kubernetes JSON Schema definitions, not just YAML syntax.
  • YAML tabs are always an error — the YAML spec forbids tabs for indentation. Configure your editor to use spaces only for YAML files.

Why YAML Is Uniquely Error-Prone

JSON and XML have explicit delimiters — braces, brackets, and angle brackets make structure unambiguous. YAML replaces all of these with indentation and inference. That makes it easier to write by hand, but it introduces an entirely new class of errors:

1. Indentation-as-Structure

In YAML, adding or removing one space changes the data structure — not just the formatting. Consider:

# VALID: env is a child of container
spec:
  containers:
    - name: app
      image: myapp:latest
      env:
        - name: PORT
          value: "8080"

# BROKEN: off-by-one-space — env is now a sibling of containers, not a child
spec:
  containers:
    - name: app
      image: myapp:latest
     env:           # ← 5 spaces instead of 6
       - name: PORT
         value: "8080"
# Error: yaml: line 8: mapping values are not allowed in this context

The YAML spec forbids using tabs for indentation entirely (Section 8.1 of the YAML 1.2 specification). Most text editors allow tabs, and a visually identical tab character causes a parser error. Configure your editor:

# .editorconfig — prevents tabs in YAML files
[*.{yaml,yml}]
indent_style = space
indent_size = 2
trim_trailing_whitespace = true
insert_final_newline = true

# VS Code settings.json
{
  "[yaml]": {
    "editor.insertSpaces": true,
    "editor.tabSize": 2,
    "editor.detectIndentation": false
  }
}

2. Implicit Type Coercion

YAML automatically infers types from unquoted values. What looks like a string might be parsed as a boolean, integer, float, or null. The YAML 1.1 specification (still the default in PyYAML, many Ruby gems, and older Java libraries) is particularly aggressive with coercion:

# YAML 1.1 type coercions that bite developers

# Version strings → floats (the most common config bug)
version: 1.10       # → float 1.1 (not "1.10"!) — this is a major version, broken
version: "1.10"     # → string "1.10" (safe)

# Country codes / environment names → booleans (Norway Problem)
country: NO         # → false  (YAML 1.1)
country: "NO"       # → "NO"   (safe)
env: ON             # → true   (YAML 1.1)
env: "ON"           # → "ON"   (safe)

# Octal numbers (YAML 1.1 only)
mode: 0755          # → integer 493 (octal interpretation)
mode: "0755"        # → string "0755" (safe)

# Null coercion
key: ~              # → null
key: null           # → null (both YAML 1.1 and 1.2)
key: Null           # → null (YAML 1.1 only; string in YAML 1.2)

# Infinity and NaN
value: .inf         # → +Infinity (float)
value: .nan         # → NaN (float) — breaks JSON serialization

3. Multiline String Gotchas

# Literal block (|) — preserves all newlines
sql_query: |
  SELECT id, name
  FROM users
  WHERE active = true
# Value: "SELECT id, name
FROM users
WHERE active = true
" (trailing newline included)

# Folded block (>) — newlines become spaces (except blank lines)
description: >
  This is a long
  description that wraps
  across multiple lines.
# Value: "This is a long description that wraps across multiple lines.
"

# Chomping modifiers (often forgotten):
key: |    # default: keep one trailing newline
key: |-   # strip all trailing newlines
key: |+   # keep all trailing newlines

# Flow scalars: be careful with special characters
command: kubectl apply -f config.yaml  # fine
command: kubectl apply -f "file:name"  # YAML parses the colon as a key!
command: "kubectl apply -f "file:name""  # must escape inner quotes

Two Layers of YAML Validation

Layer 1: Syntax Validation

Syntax validation answers: "Can this YAML be parsed?" It catches malformed YAML — wrong indentation, invalid characters, unclosed quotes, duplicate keys (when the parser is strict). You can syntax-validate with any YAML parser:

# Python — basic syntax check
python3 -c "
import yaml, sys
try:
    yaml.safe_load(open(sys.argv[1]).read())
    print('Valid YAML')
except yaml.YAMLError as e:
    print(f'Error: {e}')
    sys.exit(1)
" config.yaml

# Node.js — with js-yaml
node -e "
const yaml = require('js-yaml');
const fs = require('fs');
try {
  yaml.load(fs.readFileSync(process.argv[2], 'utf8'));
  console.log('Valid YAML');
} catch(e) {
  console.error('Error at line', e.mark?.line + 1 + ':', e.reason);
  process.exit(1);
}
" config.yaml

# yq — simplest one-liner
yq eval config.yaml > /dev/null && echo "Valid" || echo "Invalid"

Layer 2: Schema Validation

Schema validation answers: "Does this YAML contain the right fields with the right values?" A syntactically valid YAML file can still be semantically wrong — missing a required field, a string where an integer is expected, an invalid enum value. Schema validation catches these:

# Python: YAML + JSON Schema validation with jsonschema
import yaml
import jsonschema
import json

# JSON Schema defining the expected structure
schema = {
    "type": "object",
    "required": ["version", "services"],
    "properties": {
        "version": {"type": "string", "pattern": "^[0-9]+\.[0-9]+$"},
        "services": {
            "type": "object",
            "additionalProperties": {
                "type": "object",
                "required": ["image"],
                "properties": {
                    "image": {"type": "string"},
                    "ports": {"type": "array", "items": {"type": "string"}},
                    "environment": {"type": "object"}
                }
            }
        }
    }
}

with open("docker-compose.yaml") as f:
    data = yaml.safe_load(f)

try:
    jsonschema.validate(data, schema)
    print("Valid!")
except jsonschema.ValidationError as e:
    print(f"Schema error at {' -> '.join(str(p) for p in e.absolute_path)}:")
    print(f"  {e.message}")
// TypeScript: YAML + Zod schema validation
import yaml from 'js-yaml'
import { z } from 'zod'
import fs from 'fs'

const ServiceSchema = z.object({
  image: z.string(),
  ports: z.array(z.string()).optional(),
  environment: z.record(z.string()).optional(),
  restart: z.enum(['no', 'always', 'on-failure', 'unless-stopped']).optional(),
})

const ComposeSchema = z.object({
  version: z.string().regex(/^d+.d+$/),
  services: z.record(ServiceSchema),
  networks: z.record(z.object({ driver: z.string() })).optional(),
})

const raw = yaml.load(fs.readFileSync('docker-compose.yaml', 'utf-8'))
const result = ComposeSchema.safeParse(raw)

if (!result.success) {
  console.error('Validation errors:')
  result.error.issues.forEach(issue => {
    console.error(`  ${issue.path.join('.')}: ${issue.message}`)
  })
  process.exit(1)
}

console.log('Config valid:', result.data)

yamllint: Beyond Parser-Level Validation

A YAML parser only catches errors that prevent parsing. yamllint is a Python-based linter that catches problems the parser ignores — style inconsistencies, dangerous patterns, and structural issues that cause silent bugs:

# Install
pip install yamllint

# Lint a single file
yamllint config.yaml

# Lint all YAML in a directory
yamllint .

# Output example:
# config.yaml
#   5:1     warning  missing document start "---"  (document-start)
#   12:17   error    trailing spaces  (trailing-spaces)
#   23:3    error    duplication of key "name" in mapping  (key-duplicates)
#   31:1    warning  too many blank lines (3 > 2)  (empty-lines)
#   47:82   warning  line too long (85 > 80 characters)  (line-length)

Configure yamllint with a .yamllint or .yamllint.yaml file:

# .yamllint — project-level yamllint configuration
extends: default

rules:
  # Catch dangerous boolean coercion (YES/NO/ON/OFF as values)
  truthy:
    allowed-values: ['true', 'false']
    check-keys: false   # don't flag truthy-looking keys

  # Catch duplicate keys (silently overwritten)
  key-duplicates: enable

  # Line length — adjust for your team's preference
  line-length:
    max: 120
    allow-non-breakable-words: true

  # Kubernetes manifests don't need document start markers
  document-start: disable

  # Allow multi-document YAML files
  document-end: disable

  # Enforce consistent indentation
  indentation:
    spaces: 2
    indent-sequences: consistent

YAML Validation Tools Compared

ToolWhat It ValidatesBest ForLimitations
yaml.safe_load() (PyYAML)Syntax onlyQuick script-level checksNo style rules; YAML 1.1 only
js-yaml yaml.load()Syntax onlyNode.js pipelinesNo style rules; YAML 1.2-ish
yq evalSyntax onlyShell script pipelinesNo style rules; single-file focus
yamllintSyntax + style + structureCI pre-commit hooks, code reviewNo domain-specific schema
kubeconformKubernetes schemaK8s GitOps pipelinesKubernetes-specific only
kubeval (deprecated)Kubernetes schema(Use kubeconform instead)Unmaintained since 2022
Redocly CLIOpenAPI schema (YAML/JSON)API spec validationOpenAPI-specific
jsonschema + PyYAMLCustom JSON SchemaApplication config validationRequires schema authoring
Zod + js-yamlTypeScript-first schemaTS/Node.js config validationRequires schema authoring
BytePane YAML ValidatorSyntax (online, in-browser)Quick one-off validationNo schema support; manual

For most teams, the right validation stack is: yamllint as a pre-commit hook for syntax/style, domain-specific validators (kubeconform for K8s, Redocly for OpenAPI) in CI, and JSON Schema/Zod for application-level config validation.

Kubernetes Manifest Validation with kubeconform

kubeconform validates Kubernetes manifests against the official Kubernetes JSON Schema — it catches not just YAML syntax errors but semantic errors: invalid API versions, unknown fields, missing required fields, and incorrect value types. According to kubeconform's GitHub repository, it processes schemas up to 10× faster than kubeval and handles multi-document YAML correctly:

# Install kubeconform
# macOS: brew install kubeconform
# Go: go install github.com/yannh/kubeconform/cmd/kubeconform@latest

# Validate a single manifest
kubeconform -strict deployment.yaml

# Validate all manifests in a directory
kubeconform -strict k8s/

# Specify Kubernetes version for schema
kubeconform -strict -kubernetes-version 1.29.0 k8s/

# Output as JSON for CI reporting
kubeconform -strict -output json k8s/ | jq '.summary'

# In a GitHub Actions workflow:
# .github/workflows/validate.yml
- name: Validate Kubernetes manifests
  uses: docker://ghcr.io/yannh/kubeconform:latest
  with:
    args: -strict -kubernetes-version 1.29.0 k8s/

# Example output for invalid manifest:
# deployment.yaml - Deployment api-server is invalid:
#   spec.template.spec.containers[0].image: Required value: must have an image

For teams using Helm, validate the rendered manifests before deployment — not just the templates:

# Render Helm chart and pipe to kubeconform
helm template my-release ./my-chart --values values.yaml   | kubeconform -strict -kubernetes-version 1.29.0 -

# With kubeval (older approach, still common):
# helm template my-release ./my-chart | kubeval --strict --ignore-missing-schemas

YAML Validation in CI/CD Pipelines

Validation should run in two places: locally as a pre-commit hook (fast feedback loop) and in CI (enforced gate before merge). The pre-commit framework (installed on 8+ million developer machines per PyPI download stats) makes this straightforward:

# .pre-commit-config.yaml
repos:
  # YAML syntax and style linting
  - repo: https://github.com/adrienverge/yamllint
    rev: v1.35.1
    hooks:
      - id: yamllint
        args: [-c=.yamllint]

  # Kubernetes manifest validation
  - repo: https://github.com/yannh/kubeconform
    rev: v0.6.4
    hooks:
      - id: kubeconform
        args:
          - -strict
          - -kubernetes-version=1.29.0

# Install: pip install pre-commit && pre-commit install
# Run manually: pre-commit run --all-files

For GitHub Actions, add validation as a required check on pull requests:

# .github/workflows/validate-yaml.yml
name: Validate YAML

on:
  pull_request:
    paths: ['**.yaml', '**.yml', 'k8s/**']

jobs:
  yaml-lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Lint YAML files
        uses: ibiqlik/action-yamllint@v3
        with:
          config_file: .yamllint
          file_or_dir: .
          strict: true  # fail on warnings too

  k8s-validate:
    runs-on: ubuntu-latest
    needs: yaml-lint
    steps:
      - uses: actions/checkout@v4

      - name: Validate Kubernetes manifests
        run: |
          curl -sLo kubeconform.tar.gz https://github.com/yannh/kubeconform/releases/latest/download/kubeconform-linux-amd64.tar.gz
          tar xf kubeconform.tar.gz
          ./kubeconform -strict -kubernetes-version 1.29.0 k8s/

For understanding how YAML fits into the broader CI/CD picture, the CI/CD pipeline guide covers GitHub Actions, GitLab CI, and Jenkins in detail.

Programmatic YAML Validation

When your application loads a YAML config file at startup, validation should happen immediately — fail loud and early rather than silently loading wrong values.

Go: Strict Struct Unmarshaling

Go's gopkg.in/yaml.v3 can unmarshal directly into typed structs, with KnownFields(true) rejecting unknown fields:

package main

import (
    "fmt"
    "os"

    "gopkg.in/yaml.v3"
)

type Config struct {
    Server   ServerConfig   `yaml:"server"`
    Database DatabaseConfig `yaml:"database"`
    LogLevel string         `yaml:"log_level"`
}

type ServerConfig struct {
    Port    int    `yaml:"port"`
    Host    string `yaml:"host"`
    Timeout int    `yaml:"timeout_seconds"`
}

type DatabaseConfig struct {
    URL         string `yaml:"url"`
    MaxConns    int    `yaml:"max_connections"`
    SSLMode     string `yaml:"ssl_mode"`
}

func LoadConfig(path string) (*Config, error) {
    data, err := os.ReadFile(path)
    if err != nil {
        return nil, fmt.Errorf("read config: %w", err)
    }

    dec := yaml.NewDecoder(bytes.NewReader(data))
    dec.KnownFields(true)  // error on unknown fields

    var cfg Config
    if err := dec.Decode(&cfg); err != nil {
        return nil, fmt.Errorf("parse config: %w", err)
    }

    // Additional business-logic validation
    if cfg.Server.Port < 1 || cfg.Server.Port > 65535 {
        return nil, fmt.Errorf("server.port must be 1-65535, got %d", cfg.Server.Port)
    }
    if cfg.Database.MaxConns < 1 {
        return nil, fmt.Errorf("database.max_connections must be >= 1")
    }

    return &cfg, nil
}

Quick Reference: Most Common YAML Errors and Fixes

yaml: line N: did not find expected ':'

Cause: A colon inside an unquoted value is interpreted as a key-value separator

Fix: Quote the value: value: "http://example.com" instead of value: http://example.com

yaml: line N: found character that cannot start any token

Cause: A tab character used for indentation (YAML forbids tabs)

Fix: Replace all tab characters with spaces. Configure your editor to use spaces for YAML.

yaml: line N: mapping values are not allowed in this context

Cause: Indentation error — a key-value pair is at the wrong nesting level

Fix: Check the indentation of the flagged line and the lines above it. Off-by-one-space is the usual cause.

yaml: line N: did not find expected '-' indicator

Cause: An item in a sequence list is missing its leading dash, or the dash is misaligned

Fix: Ensure every list item starts with "- " at the correct indentation level.

Silent: string "1.0" becomes float 1.0

Cause: YAML parses unquoted numeric strings as numbers

Fix: Quote version strings and any value that should remain a string: version: "1.0"

Silent: duplicate key overwrites earlier value

Cause: Most parsers accept duplicate keys silently, keeping the last value

Fix: Use yamllint with key-duplicates: enable to catch duplicates before they cause bugs.

YAML Validation vs. YAML-to-JSON Conversion

A common debugging workflow: when a YAML file produces unexpected behavior, convert it to JSON to see exactly how the parser interpreted it. If the JSON output looks wrong (strings became numbers, keys are missing, structure is nested differently than expected), you've found the bug.

Use the BytePane YAML to JSON converter to convert your YAML and inspect the parsed output. The converted JSON reveals exactly what your YAML parser sees — no more guessing about type coercions. For a deep-dive on YAML-to-JSON conversion edge cases including the Norway Problem, anchors, and multi-document files, see the YAML to JSON converter guide.

And for quick JSON formatting and validation after conversion, the BytePane JSON formatter validates JSON syntax and pretty-prints the output.

Frequently Asked Questions

How do I validate YAML syntax on the command line?

With Python: python3 -c "import yaml; yaml.safe_load(open('config.yaml').read()); print('Valid')" — raises YAMLError on syntax errors. With yq: yq eval config.yaml returns non-zero on error. With yamllint: yamllint config.yaml provides line-by-line diagnostics with configurable rules.

What is the difference between YAML syntax validation and schema validation?

Syntax validation checks that YAML is parseable — correct indentation, valid types, no duplicate keys. Schema validation checks that the parsed data matches expected structure — required fields, value ranges, correct types. A file can be syntactically valid but semantically wrong. Use yamllint for syntax; JSON Schema or Zod for structure.

Why does YAML indentation matter so much?

YAML uses indentation to define structure — there are no delimiters like braces or brackets. Inconsistent indentation changes the data structure silently or raises a parser error. The YAML spec forbids tabs for indentation entirely. Most YAML errors in practice are indentation-related: off-by-one-space mistakes, tabs from copy-paste.

How do I validate Kubernetes YAML manifests?

Use kubeconform — it validates Kubernetes manifests against the official Kubernetes JSON Schema, catching invalid API versions, unknown fields, and missing required fields. Install: go install github.com/yannh/kubeconform/cmd/kubeconform@latest. Run: kubeconform -strict -kubernetes-version 1.29.0 k8s/. Kubeconform replaced the unmaintained kubeval.

Can YAML have duplicate keys?

The YAML spec says duplicate keys in a mapping are an error, but most parsers handle them permissively — typically keeping the last value. PyYAML and js-yaml both accept duplicates silently. This is dangerous: a duplicate key in a Kubernetes manifest silently overwrites the earlier value. yamllint with forbid-duplicated-keys: true catches this.

What is yamllint and how do I configure it?

yamllint is a Python YAML linter that checks syntax, style, and common mistakes beyond what parsers catch — trailing spaces, line length, truthy value coercions, duplicate keys. Configure with .yamllint file. Install: pip install yamllint. Integrates with pre-commit hooks and GitHub Actions for CI enforcement.

How do I validate YAML against a JSON Schema?

Parse YAML to a dict/object first, then validate against JSON Schema. Python: yaml.safe_load() then jsonschema.validate(data, schema). Node.js: yaml.load() then ajv.validate(schema, data). The YAML parser handles syntax; JSON Schema handles structure. This two-step approach handles both validation layers cleanly.

Validate Your YAML Instantly

Paste your YAML and get syntax validation in your browser — no server uploads, no file size limits. Also convert YAML to JSON to inspect exactly how your parser interprets the values.