An underused fact: YAML is (almost) a superset of JSON
Every mainstream CI/CD tool — GitHub Actions, GitLab CI, CircleCI, Docker Compose — documents its config format as YAML. What's less advertised is that YAML 1.1/1.2's grammar is designed to be a near-superset of JSON, so a syntactically valid JSON document is, in almost every practical case, also syntactically valid YAML. That means you can write a GitHub Actions workflow, a Docker Compose file, or most other "YAML config" as plain JSON and it will parse and run identically — the tool never actually required YAML's extra syntax, only YAML's *parser*, which happens to also accept JSON.
{
"version": "3.9",
"services": {
"web": {
"image": "nginx:latest",
"ports": ["8080:80"]
}
}
}Save that as docker-compose.json... except Compose specifically looks for docker-compose.yml/.yaml by filename convention, so in practice you'd need -f docker-compose.json on the command line — the *parser* accepts JSON, but the *filename convention* still assumes YAML. This gap between "the format is accepted" and "the tooling expects it by default" is the crux of when JSON is actually usable here.
Where JSON genuinely wins
Generated or templated pipelines. If a script is *producing* the CI config — say, generating a matrix of jobs from a list of services in a monorepo — emitting JSON from code is simpler and less error-prone than emitting YAML, since JSON has one unambiguous way to represent a string, a list, or a nested object, while YAML has several (block vs. flow style, multiple ways to quote a string, indentation-sensitive nesting that a naive string-templating script can easily corrupt).
// Generating a GitHub Actions matrix programmatically — trivial in JSON,
// error-prone if you're hand-string-templating YAML's indentation rules
const jobs = services.map((s) => ({ service: s.name, dockerfile: s.path }));
console.log(JSON.stringify({ matrix: { include: jobs } }));Programmatic editing after the fact. A tool that reads an existing config, modifies one field, and writes it back has a much easier time with JSON — parse, mutate the object, JSON.stringify — than with YAML, where a naive round-trip can silently reformat comments, anchors, or the author's original block/flow style choices even when no field values actually changed. (YAML-preserving editors exist, but they're a meaningfully bigger dependency than JSON.parse/JSON.stringify.)
Strict, unambiguous parsing. YAML has had real security and correctness footguns from its own flexibility — the Norway problem (an unquoted no parses as boolean false in YAML 1.1, not the country code), implicit type coercion surprises, and tab-vs-space indentation errors that fail silently in some parsers. JSON's much smaller grammar has none of these ambiguities.
Where YAML still wins for CI/CD specifically
Comments. JSON has none; a CI pipeline that a human hand-edits and needs to leave notes in (# temporarily skip this job, remove after MYAPP-4521 is fixed) is far more usable in YAML.
Anchors and aliases — YAML's &anchor/*alias/<<: *merge let you define a block once and reuse it across multiple jobs, which is common in larger CI configs to avoid repeating the same steps: block in every job:
.base_job: &base_job
image: node:20
before_script: [npm ci]
test:
<<: *base_job
script: [npm test]
build:
<<: *base_job
script: [npm run build]JSON has no equivalent — every job's steps would need to be repeated in full, or you'd need to add your own templating layer on top (which is exactly what tools like GitHub Actions' reusable workflows, and GitLab's extends keyword, eventually grew into as YAML-native alternatives to raw anchors).
It's simply what every tool's docs, examples, and Stack Overflow answers use. Even where JSON would parse fine, deviating from YAML means every example you copy from documentation needs manual translation — a real, if unglamorous, cost.
A practical rule of thumb
| Situation | Prefer |
|---|---|
| Hand-written, hand-edited pipeline | YAML — comments and anchors pay for themselves |
| Machine-generated pipeline (templated per-service, per-environment) | JSON — safer to generate correctly |
| A config file another tool needs to programmatically patch | JSON — safer to round-trip without formatting drift |
| Following the tool's own documented examples closely | YAML — matches what you'll find everywhere |