Every application needs configuration, and every team argues about how to store it. The configuration format landscape in 2026 includes at least six serious contenders: INI, JSON, YAML, TOML, ENV/dotenv, and HCL. Each has a sweet spot, a community, and a set of tradeoffs that its advocates rarely mention.
This guide compares all six formats on the dimensions that matter for configuration: readability, type safety, comment support, nesting depth, ecosystem alignment, and the specific failure modes each format invites. The goal is a clear decision framework so you can pick a format in five minutes and move on to building features.
INI: Simple, Unspecified, Legacy
[server]
host = localhost
port = 8080
debug = true
[database]
url = postgres://localhost/mydb
pool_size = 25INI files are the grandfather of configuration formats. They originated in MS-DOS and Windows (win.ini, system.ini) and remain in use through php.ini, MySQL's my.cnf, pip.conf, and many legacy systems.
INI's fundamental problem: there is no specification. Different parsers handle edge cases differently:
- Are keys case-sensitive? (Windows: no. Python's configparser: no. Most Unix tools: yes.)
- Can values span multiple lines? (Some parsers: yes with continuation characters. Others: no.)
- What's the comment character? (
;in Windows,#in Unix, both in some parsers.) - Can sections nest? (Standard: no. Some tools support dotted section names:
[section.subsection].) - Are there types? (No. Everything is a string.
port = 8080is the string"8080".)
INI works for trivially simple configs (key-value pairs grouped into sections). For anything more complex, use TOML — it's essentially INI with a specification, types, and nesting. Converting INI to TOML is straightforward because TOML's table syntax is deliberately similar to INI's section syntax.
JSON: Strict, Universal, Comment-less
{
"server": {
"host": "localhost",
"port": 8080,
"debug": true
},
"database": {
"url": "postgres://localhost/mydb",
"pool_size": 25
}
}JSON is the most widely supported data format but a mediocre configuration format. Its type system (strings, numbers, booleans, null, arrays, objects) is an advantage over INI, and its strict syntax prevents ambiguity. But three missing features make it frustrating for human-edited configs:
- No comments. You can't explain why a value is set, document valid ranges, or comment out a line while debugging. This alone disqualifies JSON for most config use cases.
- No trailing commas. Adding or removing the last item in an array or object requires editing two lines, creating noisy diffs.
- Verbose for nesting. Braces and quotes add visual noise. A 20-line YAML config becomes a 30-line JSON config.
JSON for config works when: the config is machine-generated and machine-consumed (build artifacts, lock files), the ecosystem mandates it (package.json, tsconfig.json), or you need guaranteed parse consistency across all tools.
JSONC (VS Code's settings.json) and JSON5 add comments and trailing commas but aren't universally supported. Converting JSON to YAML or TOML lets you add comments and improve readability.
YAML: Expressive, Risky, Dominant in DevOps
# Server configuration
server:
host: localhost
port: 8080
debug: true
ssl:
enabled: true
cert: /etc/ssl/cert.pem
# Database settings
database:
url: postgres://localhost/mydb
pool_size: 25YAML is the most feature-rich configuration format: comments, multiline strings, anchors for DRY configs, deep nesting, and a broad type system. It's the mandatory choice for Kubernetes, Docker Compose, GitHub Actions, Ansible, and most infrastructure tooling.
YAML's risks for configuration:
- Implicit type coercion turns
NOinto false,3.10into 3.1, andoninto true. Quote strings defensively. - Indentation errors create valid-but-wrong YAML. A misaligned key silently changes the config structure.
- Tabs are illegal for indentation, but invisible to most editors.
- Specification complexity (86 pages) means different parsers may behave differently on edge cases.
Use YAML when the tool requires it or when your config has deep nesting (4+ levels). For simpler configs, TOML avoids YAML's footguns while keeping comments and readability.
TOML: Explicit, Safe, Growing
# Server configuration
[server]
host = "localhost"
port = 8080
debug = false
[server.ssl]
enabled = true
cert = "/etc/ssl/cert.pem"
# Database settings
[database]
url = "postgres://localhost/mydb"
pool_size = 25TOML is designed specifically for configuration. It has everything JSON lacks (comments, native date types, multiline strings) without YAML's footguns (no implicit type coercion, no indentation rules, 30-page spec vs. 86).
TOML is the standard for Rust (Cargo.toml), Python (pyproject.toml), Hugo, and Netlify. Its adoption is accelerating as newer ecosystems choose it over YAML for application-level config.
TOML limitations: deep nesting is verbose (long dotted table headers), arrays must be homogeneous (no mixed types), and no null type. These are non-issues for typical config files but matter if your config has complex structure.
For new projects where you control the config format, TOML is the recommended default. Convert from YAML or JSON if migrating existing configs.
ENV / Dotenv: Secrets and 12-Factor Apps
# .env file
DATABASE_URL=postgres://localhost/mydb
REDIS_URL=redis://localhost:6379
API_KEY=sk-live-abc123def456
DEBUG=true
PORT=8080ENV files (commonly called dotenv files, from the .env convention) are the simplest possible format: KEY=VALUE pairs, one per line. They align with the 12-Factor App methodology, which stores config in environment variables for portability across deployment environments.
ENV characteristics:
- No types. Everything is a string. Your application must parse
"8080"to an integer and"true"to a boolean. - No nesting. Flat key-value only. Convention uses underscores for hierarchy:
DATABASE_POOL_SIZE=25. - No arrays. Convention uses comma-separated strings:
ALLOWED_ORIGINS=https://a.com,https://b.com. - Comments with
#. Lines starting with#are ignored. - Quoting varies: some libraries support
KEY="value with spaces", others don't.
ENV files are best for: secrets (API keys, database passwords), deployment-specific values (URLs, ports, feature flags), and anything that changes between environments. They should not be your primary config format — use them alongside a structured config file (TOML, YAML) that holds non-secret, non-environment-specific settings.
Convert ENV to JSON or YAML when you need to process environment variables as structured data. Convert JSON to ENV to flatten a structured config into environment variables for deployment.
HCL: Terraform's Domain-Specific Language
resource "aws_instance" "web" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t2.micro"
tags = {
Name = "WebServer"
}
ebs_block_device {
device_name = "/dev/sdf"
volume_size = 50
}
}HCL (HashiCorp Configuration Language) is a domain-specific language created for Terraform, Vault, Consul, Nomad, and other HashiCorp tools. It's neither JSON nor YAML — it's a purpose-built language with types, expressions, functions, loops, and conditionals.
HCL is not a general-purpose config format. You wouldn't use it for application settings or CI/CD pipelines. But within the HashiCorp ecosystem, it's the only format that fully supports Terraform's features (expressions, modules, data sources, provisioners). HCL files can be converted to JSON (terraform show -json) for processing, but the JSON representation lacks HCL's expressiveness.
Complete Comparison
| Feature | INI | JSON | YAML | TOML | ENV | HCL |
|---|---|---|---|---|---|---|
| Comments | # or ; | No | # | # | # | #, //, /* */ |
| Types | None (strings only) | 6 types | 11+ types | 10 types (incl. dates) | None (strings only) | Rich (incl. expressions) |
| Nesting | 1 level (sections) | Unlimited | Unlimited | Unlimited (verbose past 3) | None | Unlimited |
| Arrays | No standard | Yes (heterogeneous) | Yes (heterogeneous) | Yes (homogeneous only) | Convention only | Yes |
| Spec exists | No | RFC 8259 | YAML 1.2 | TOML v1.0.0 | No formal spec | Spec by HashiCorp |
| Parse safety | Low (no spec) | High (strict syntax) | Medium (coercion risks) | High (explicit types) | High (trivial format) | High (typed) |
| Ecosystem | PHP, MySQL, Python, Windows | Node.js, TS, .NET, web | K8s, Docker, CI/CD, Ansible | Rust, Python, Hugo, Go | 12-factor, Docker, all langs | Terraform, Vault, Consul |
Decision Framework: Which Format for Your Config?
Follow this flowchart:
- Is the format mandated by a tool? (package.json, docker-compose.yml, Cargo.toml) Use what's required.
- Is it secrets or environment-specific settings? Use ENV. Never commit secrets to a config file that's in version control.
- Is it infrastructure config with deep nesting? (Kubernetes, CI/CD) Use YAML.
- Is it Terraform/Vault/Consul? Use HCL.
- Is it application config with moderate structure? Use TOML. It's the safest, simplest choice for general config files.
- Is it machine-generated config that code reads? Use JSON. Strict syntax, universal parsers.
In practice, most projects use multiple formats: .env for secrets, TOML or YAML for application config, and whatever the infrastructure tools require.
Migration Paths Between Formats
| From | To | Conversion | What Changes |
|---|---|---|---|
| INI | TOML | INI to TOML | Gain: types, nested tables, spec compliance. Strings now need quotes. |
| JSON | YAML | JSON to YAML | Gain: comments, readability. Risk: YAML type coercion on unquoted values. |
| JSON | TOML | JSON to TOML | Gain: comments, native dates. Works well for shallow JSON. Deep nesting becomes verbose. |
| YAML | TOML | YAML to TOML | Gain: explicit types, no indentation bugs. Lose: anchors, deep nesting elegance, null. |
| ENV | JSON | ENV to JSON | Gain: types, nesting. All values start as strings — manual type annotation needed. |
| ENV | YAML | ENV to YAML | Same as ENV to JSON, with comments. |
| TOML | ENV | TOML to ENV | Lose: types, nesting, arrays. Only flat key-value pairs survive. |
All format conversions lose comments. After converting, re-add comments manually. This is the single most annoying aspect of config format migration.
The config format choice usually makes itself. Your tools dictate most of the decision (package.json, docker-compose.yml, Cargo.toml). For the config files you control, the recommendation is simple: ENV for secrets, TOML for application config. If your config requires deep nesting or your team is fluent in YAML, use YAML. If the config is machine-generated, use JSON.
The worst choice is no choice — ending up with a mix of undocumented formats because each developer used their favorite. Pick a format, document the convention, and make sure everyone on the team can read it.