YAML Validator: Check & Fix YAML Syntax Online
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 contextThe 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: consistentYAML Validation Tools Compared
| Tool | What It Validates | Best For | Limitations |
|---|---|---|---|
| yaml.safe_load() (PyYAML) | Syntax only | Quick script-level checks | No style rules; YAML 1.1 only |
| js-yaml yaml.load() | Syntax only | Node.js pipelines | No style rules; YAML 1.2-ish |
| yq eval | Syntax only | Shell script pipelines | No style rules; single-file focus |
| yamllint | Syntax + style + structure | CI pre-commit hooks, code review | No domain-specific schema |
| kubeconform | Kubernetes schema | K8s GitOps pipelines | Kubernetes-specific only |
| kubeval (deprecated) | Kubernetes schema | (Use kubeconform instead) | Unmaintained since 2022 |
| Redocly CLI | OpenAPI schema (YAML/JSON) | API spec validation | OpenAPI-specific |
| jsonschema + PyYAML | Custom JSON Schema | Application config validation | Requires schema authoring |
| Zod + js-yaml | TypeScript-first schema | TS/Node.js config validation | Requires schema authoring |
| BytePane YAML Validator | Syntax (online, in-browser) | Quick one-off validation | No 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 imageFor 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-filesFor 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 tokenCause: 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 contextCause: 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 '-' indicatorCause: 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.0Cause: 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 valueCause: 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.