Every project needs configuration, and you've got three main options: JSON, YAML, or TOML. I've used all three extensively, and each one drives me crazy in different ways. Here's my honest take on when to use what.

JSON: The Universal Soldier

You already know JSON. It's everywhere. Every language can parse it. But let's be real about its config-file weaknesses:

  • No comments. You literally cannot explain what a setting does. That's bananas for a config file.
  • No trailing commas. Add a new line at the end, forget to add a comma to the previous line, and your config breaks.
  • Quotes everywhere. Every key needs double quotes: {"port": 8080} instead of just port: 8080.

Despite all this, JSON is used for config by npm (package.json), TypeScript (tsconfig.json), VS Code (settings.json), and more. The reason? Zero ambiguity. JSON is so strict that there's only one way to interpret it.

YAML: Beautiful but Dangerous

YAML looks amazing. Clean, minimal, human-readable. But it has some notorious footguns:

yaml

That's the "Norway problem" I mentioned in the YAML article. YAML 1.1 treats NO, YES, ON, OFF as booleans. It's been fixed in YAML 1.2, but many parsers still default to 1.1 behavior.

Then there's the indentation sensitivity. A single misplaced space can completely change the meaning of your file — and the error message will be terrible.

That said, YAML is the standard for Docker Compose, Kubernetes, GitHub Actions, and Ansible. If you're in the DevOps/cloud space, YAML fluency is mandatory.

TOML: The Config File Specialist

TOML (Tom's Obvious Minimal Language) was designed specifically for configuration. It aims to be obvious — meaning there's (almost) no way to misread a TOML file.

Here's what a TOML config looks like:

toml

Things TOML does really well: native date/time types (created = 2026-03-08T10:30:00Z), no indentation issues, comments with #, and a syntax that's genuinely hard to mess up.

The downside? Deeply nested structures get verbose. TOML is great for flat-ish configs, but if you need five levels of nesting, YAML or JSON might be more readable.

TOML is used by Rust (Cargo.toml), Python (pyproject.toml), Hugo, and an increasing number of tools.

My Recommendations

ScenarioPickWhy
Machine-generated configJSONStrictest, most compatible
DevOps/infrastructureYAMLEcosystem expects it
App configurationTOMLComments + minimal ambiguity
Simple key-value settingsTOMLCleanest for flat configs
Complex nested structuresYAMLBest nested syntax

The Bottom Line

The Same Config in All Three Formats

Seeing the same data in all three formats really highlights the differences. Here's a simple app configuration:

JSON:

json

YAML:

yaml

TOML:

toml

Notice how JSON requires all those quotes and braces, YAML is the most concise but relies on indentation, and TOML strikes a middle ground with explicit section headers and no indentation dependency.

Common Config File Mistakes

YAML: Accidental type coercion. Beyond the Norway problem, YAML will also interpret 3.10 as the number 3.1 (dropping the trailing zero), and 1_000 as 1000. If your config values are version strings like 3.10, always quote them: version: "3.10".

JSON: Forgetting that order doesn't matter. JSON objects are unordered by spec. If your config processing depends on key order, you're building on a fragile foundation. Some parsers preserve insertion order, others don't.

TOML: Confusion with arrays of tables. TOML uses [[double.brackets]] for arrays of tables, which trips up newcomers. Here's how you define multiple servers:

toml

Environment-Specific Configs

One area where all three formats struggle is handling environment-specific overrides (dev vs staging vs production). Common solutions include:

  • Multiple files: config.base.yaml + config.production.yaml with deep merging
  • Environment variable interpolation: Some YAML tools support ${DB_HOST} syntax, but it's not standard
  • External tools: Doppler, HashiCorp Vault, or cloud-native config services

Personally, I lean toward a simple approach: one config file with sensible defaults, and environment variables for anything that changes between environments. All three formats can read from env vars at the application level, so the config format itself doesn't need to support interpolation.

Format Popularity by Ecosystem

EcosystemPrimary FormatNotable Examples
JavaScript/Node.jsJSONpackage.json, tsconfig.json, .eslintrc.json
PythonTOMLpyproject.toml, Cargo.toml (Rust)
DevOps/CloudYAMLdocker-compose.yml, k8s manifests, GitHub Actions
GoTOML/YAMLBoth widely used, no single standard
.NETJSONappsettings.json (replaced XML-based web.config)

The Bottom Line

There's no single right answer. Use JSON when you need maximum compatibility. Use YAML when the ecosystem demands it (Kubernetes isn't going to start accepting TOML). Use TOML when you want comments and minimal surprises.

JSONC and JSON5: JSON With Training Wheels

Okay, so we've been dunking on JSON for not supporting comments and trailing commas. But here's the thing nobody tells you: there are actually variants of JSON that fix these annoyances, and you've probably been using one of them without even realizing it.

First up: JSONC (JSON with Comments). If you've ever opened a tsconfig.json and thought "wait, there are comments in here... I thought JSON didn't allow comments?" — yeah, that's JSONC. It's basically JSON but you can use // single-line comments and /* */ block comments. VS Code uses JSONC for its settings.json, launch.json, and keybindings.json files. TypeScript's tsconfig.json is also technically JSONC, not strict JSON.

Here's what JSONC looks like:

jsonc

Then there's JSON5, which goes further. JSON5 relaxes a bunch of JSON's strictest rules:

  • Single-quoted strings: {'name': 'Sarah'} — finally!
  • Trailing commas: {"a": 1, "b": 2,} — no more diff noise
  • Unquoted keys (if they're valid identifiers): {name: "Sarah"}
  • Hexadecimal numbers: 0xFF
  • Multi-line strings with backslash continuation
  • Infinity, -Infinity, and NaN as valid numbers

JSON5 is great for config files where humans are the primary audience. Some tools support it natively — for instance, Babel's .babelrc can be JSON5. But don't go using JSON5 for API responses or data interchange. The whole point of strict JSON is that everyone agrees on the format. JSON5 is for humans, not machines.

My rule of thumb: if a tool supports JSONC or JSON5, use it. There's literally no downside to having comments in your config files. But if you're writing a library that reads config, support standard JSON first and JSONC/JSON5 as optional extras.

YAML Anchors and Aliases: DRY Config

Here's YAML's actual killer feature that a lot of developers never discover: anchors and aliases. They let you define a block of config once and reuse it everywhere. Basically DRY (Don't Repeat Yourself) for config files.

An anchor is marked with &name and an alias references it with *name. Here's a practical Docker Compose example:

yaml

See what happened there? We defined common settings once with &common and pulled them into three services with <<: *common. Without anchors, you'd be copy-pasting that restart policy and logging config into every single service. And when you need to change the log rotation? One place instead of twelve.

You can also use anchors for simpler values — not just maps:

yaml

Now, the gotchas. The merge key << that makes anchors really powerful? It's not actually part of the YAML 1.2 spec. It was a YAML 1.1 type extension, and while most popular parsers still support it, it's technically non-standard. So if you're using a strict YAML 1.2 parser, merge keys might not work.

Also, anchors only work within a single file. You can't reference an anchor defined in another YAML file. And the error messages when you mess up an alias name? Usually something like "undefined alias" with zero context about where the anchor was supposed to be. Classic YAML.

TOML Deep Dive: Advanced Features

Most people know TOML basics — sections with [brackets], key-value pairs, comments with #. But TOML has some genuinely cool features that don't get enough love. Let me walk you through them.

Dotted keys let you define nested structures without section headers:

toml

This is handy when you just have a couple of nested values and don't want to create a whole section for them.

Inline tables give you JSON-like compact syntax for small objects:

toml

Use inline tables sparingly though — they can't span multiple lines, and they become unreadable fast if you stuff too much into them.

Multiline strings come in two flavors, and this is where TOML gets surprisingly thoughtful:

toml

The triple-quoted basic string (""") processes escape sequences, while the literal version (''') treats everything as raw text. This is perfect for regex patterns or Windows file paths where you don't want backslashes interpreted as escapes.

Native date/time types are something neither JSON nor YAML handles this cleanly:

toml

These are first-class types in TOML, not strings pretending to be dates. Your TOML parser will give you actual date/time objects, not strings you have to parse yourself.

Why did Rust's Cargo choose TOML? Because Cargo configs are exactly the sweet spot for TOML: moderately nested, human-edited, needs comments, and benefits from strict typing. You don't want your dependency versions silently interpreted as floats (looking at you, YAML).

Security Considerations

Okay, this is the section where things get a little scary. If you're loading config files from untrusted sources — or even if you think you're not — you need to know about deserialization attacks.

The poster child for this is PyYAML's yaml.load(). In older versions, this innocent-looking function could execute arbitrary Python code embedded in a YAML file. I'm not kidding. Check this out:

yaml

If someone slips this into a YAML config and your Python app loads it with yaml.load() instead of yaml.safe_load(), it will literally execute that system command. Delete everything. Install a backdoor. Whatever the attacker wants.

The fix is dead simple — always use yaml.safe_load() in Python:

python

The PyYAML documentation now warns about this, and newer versions show a deprecation warning if you use yaml.load() without specifying a Loader. But there are still countless tutorials and Stack Overflow answers showing the unsafe version. It's a land mine hiding in plain sight.

Then there's the "billion laughs" attack (also called an XML bomb, but it works on YAML too). The idea is recursive expansion — you define entities that reference other entities, creating exponential growth:

yaml

Each level multiplies the data by 5, so by level 8 or 9 you're looking at gigabytes of data from a few lines of YAML. Most modern YAML parsers have depth and expansion limits to prevent this, but it's worth knowing about.

JSON is inherently safer because it has no execution semantics whatsoever. There's no way to embed code, no type constructors, no entity expansion. A JSON parser just reads data — strings, numbers, booleans, arrays, and objects. That's it. This is one of the underappreciated advantages of JSON's simplicity. When security matters, JSON's lack of features is actually a feature.

TOML is also pretty safe — it has no code execution capabilities and no recursive expansion. But it's less battle-tested than JSON parsers, so keep your TOML libraries up to date.

Bottom line: if you're accepting config files from users or external sources, JSON is the safest choice. If you must use YAML, always use safe loading functions and consider running a YAML linter to catch suspicious constructs.

Real-World Config File Examples

Theory is great, but let's look at actual config files you'll encounter in the wild. I've annotated each one to highlight format-specific patterns.

GitHub Actions workflow (YAML):

yaml

This is where YAML really shines. The GitHub Actions workflow syntax would be painful in JSON — all those nested lists and the indentation-based structure maps naturally to YAML. You can also see how comments help explain the matrix strategy.

Rust's Cargo.toml (TOML):

toml

Notice how the Cargo manifest uses inline tables for dependencies with features. The [[bin]] double-bracket syntax defines an array of tables — each [[bin]] entry adds another binary target. TOML keeps this flat and readable without any indentation games.

package.json (JSON):

json

The classic. No comments, lots of quotes, but every tool on the planet can read it. The thing that makes package.json work despite JSON's limitations is that the schema is so well-known — you don't need comments to explain what scripts.build does because every JavaScript developer already knows.

.prettierrc (JSON):

json

Simple, flat key-value config. Honestly, this would be slightly nicer as TOML (comments!) or even JSONC, and Prettier does support other formats. But JSON is the default, and for something this simple, it doesn't matter much.

Migration Between Formats

So you've decided your project's config format was a mistake and you want to switch. Or maybe you're pulling in config from a tool that uses a different format. Either way, you need to convert between TOML, YAML, and JSON. Let me tell you, it's not always as smooth as you'd hope.

Tools for conversion:

  • yq — The Swiss Army knife for YAML. Can convert between YAML, JSON, TOML, and XML. yq -o=json config.yaml gives you JSON output. It's like jq but for YAML.
  • toml-cli and taplo — Command-line TOML processors. Taplo is particularly good for TOML formatting and validation.
  • Python one-liners — In a pinch, python -c "import yaml, json, sys; print(json.dumps(yaml.safe_load(sys.stdin), indent=2))" < config.yaml converts YAML to JSON. Not pretty, but it works.
  • Online converters — For quick one-off conversions, our formatters can help. Paste your config into the appropriate formatter, clean it up, and manually rewrite it in the target format.

When to migrate:

  • Your team keeps making YAML indentation errors in CI configs? Maybe switching to TOML reduces those headaches.
  • You need comments in a JSON config file? Consider migrating to JSONC (if the tool supports it) or TOML.
  • You're building a new Rust/Python project and the ecosystem expects TOML? Don't fight it — go with TOML.

Common gotchas when migrating:

YAML's implicit date parsing will bite you. If you have a YAML value like release: 2024-03-08, YAML interprets that as a date object, not a string. Convert that to JSON and you might get "release": "2024-03-08T00:00:00Z" or "release": "2024-03-08" depending on your converter. Always test your output.

TOML's strict typing means you can't have mixed-type arrays. In JSON and YAML, [1, "two", true] is perfectly fine. In TOML, every element in an array must be the same type. If your source data has mixed arrays, you'll need to restructure.

And here's the big one: comments get lost. JSON doesn't support comments, so if you're converting from TOML or YAML to JSON, all your carefully written comments disappear. Going the other direction (JSON to YAML/TOML), you'll want to manually add comments to explain any non-obvious settings, because the JSON version certainly didn't have them.

One more thing — YAML's anchors and aliases don't have equivalents in JSON or TOML. If your YAML config relies on anchors for DRY config, converting to another format means you'll have to manually duplicate those shared blocks. That can be a significant expansion in file size for complex configs.

Try It Yourself

Whichever format you choose, keeping your configs well-formatted makes them easier to review and debug. Our TOML Formatter cleans up messy TOML files instantly. The YAML Formatter fixes indentation issues before they cause problems. And the JSON Formatter makes even deeply nested JSON configs readable at a glance.