DEV Community

Cover image for TOML vs JSON: The Ultimate Configuration Format Showdown (2025)
sizan mahmud0
sizan mahmud0

Posted on

TOML vs JSON: The Ultimate Configuration Format Showdown (2025)

The 200-Line JSON Nightmare That Converted Me to TOML

I spent two hours debugging a production deployment failure. The culprit? A missing comma in line 187 of a 200-line JSON configuration file. No error message pointed to the exact location—just "JSON Parse Error."

That's when I discovered TOML. The same configuration became readable, maintainable, and impossible to break with missing commas. Today, I'll show you exactly when to use JSON vs TOML, with real-world examples that will change how you think about configuration files.

What Are TOML and JSON?

JSON (JavaScript Object Notation)

Born: 2001 by Douglas Crockford
Purpose: Data interchange between systems
Philosophy: Machine-readable first, human-readable second

{
  "name": "My Application",
  "version": "1.0.0",
  "database": {
    "host": "localhost",
    "port": 5432,
    "credentials": {
      "username": "admin",
      "password": "secret123"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Key Characteristics:

  • ✅ Universally supported (every language has JSON parser)
  • ✅ Compact and efficient for APIs
  • ✅ Perfect for data transmission
  • ❌ Hard to read for large configs
  • ❌ No comments support
  • ❌ Strict syntax (trailing commas break everything)

TOML (Tom's Obvious, Minimal Language)

Born: 2013 by Tom Preston-Werner (GitHub co-founder)
Purpose: Human-readable configuration files
Philosophy: Configuration files should be easy to write and read

name = "My Application"
version = "1.0.0"

# Database configuration
[database]
host = "localhost"
port = 5432

[database.credentials]
username = "admin"
password = "secret123"  # TODO: Use environment variable
Enter fullscreen mode Exit fullscreen mode

Key Characteristics:

  • ✅ Extremely readable (like INI files, but better)
  • ✅ Built-in comments support
  • ✅ Great for configuration files
  • ✅ Type-safe (distinguishes strings, integers, booleans)
  • ❌ Less universal than JSON
  • ❌ Not ideal for APIs or data transmission

The Visual Comparison: Same Data, Different Story

Let's configure a complex web application in both formats:

JSON Version (93 lines, hard to scan)

{
  "application": {
    "name": "E-Commerce Platform",
    "version": "2.3.1",
    "environment": "production",
    "debug": false,
    "allowed_hosts": ["example.com", "www.example.com"]
  },
  "database": {
    "default": {
      "engine": "postgresql",
      "name": "ecommerce_db",
      "host": "db.example.com",
      "port": 5432,
      "username": "db_user",
      "password": "super_secret_password",
      "pool_size": 20,
      "timeout": 30,
      "ssl": true
    },
    "replica": {
      "engine": "postgresql",
      "name": "ecommerce_db",
      "host": "replica.example.com",
      "port": 5432,
      "username": "db_user",
      "password": "super_secret_password",
      "pool_size": 15,
      "timeout": 30,
      "ssl": true
    }
  },
  "cache": {
    "backend": "redis",
    "location": "redis://cache.example.com:6379/1",
    "timeout": 300,
    "key_prefix": "ecommerce",
    "options": {
      "max_connections": 50,
      "socket_timeout": 5,
      "socket_connect_timeout": 5,
      "retry_on_timeout": true
    }
  },
  "email": {
    "backend": "smtp",
    "host": "smtp.example.com",
    "port": 587,
    "use_tls": true,
    "username": "noreply@example.com",
    "password": "email_password",
    "timeout": 10
  },
  "logging": {
    "level": "INFO",
    "handlers": ["console", "file", "sentry"],
    "format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s",
    "file_path": "/var/log/ecommerce/app.log",
    "max_bytes": 10485760,
    "backup_count": 5
  },
  "security": {
    "secret_key": "django-insecure-xyz123abc456",
    "allowed_origins": ["https://example.com"],
    "csrf_trusted_origins": ["https://example.com"],
    "session_cookie_age": 1209600,
    "session_cookie_secure": true,
    "csrf_cookie_secure": true
  },
  "payment": {
    "stripe": {
      "public_key": "pk_live_xxxxxxxxxxxx",
      "secret_key": "sk_live_xxxxxxxxxxxx",
      "webhook_secret": "whsec_xxxxxxxxxxxx"
    },
    "paypal": {
      "client_id": "AaBbCcDd123456",
      "client_secret": "EeFfGgHh789012",
      "mode": "live"
    }
  },
  "features": {
    "enable_reviews": true,
    "enable_wishlists": true,
    "enable_recommendations": false,
    "max_cart_items": 50,
    "session_timeout_minutes": 30
  }
}
Enter fullscreen mode Exit fullscreen mode

TOML Version (Same data, 88 lines, crystal clear)

# E-Commerce Platform Configuration
# Last updated: 2025-01-15

[application]
name = "E-Commerce Platform"
version = "2.3.1"
environment = "production"
debug = false
allowed_hosts = ["example.com", "www.example.com"]

# Database Configuration
[database.default]
engine = "postgresql"
name = "ecommerce_db"
host = "db.example.com"
port = 5432
username = "db_user"
password = "super_secret_password"  # TODO: Move to environment variable
pool_size = 20
timeout = 30
ssl = true

[database.replica]
engine = "postgresql"
name = "ecommerce_db"
host = "replica.example.com"
port = 5432
username = "db_user"
password = "super_secret_password"
pool_size = 15
timeout = 30
ssl = true

# Cache Configuration (Redis)
[cache]
backend = "redis"
location = "redis://cache.example.com:6379/1"
timeout = 300  # 5 minutes
key_prefix = "ecommerce"

[cache.options]
max_connections = 50
socket_timeout = 5
socket_connect_timeout = 5
retry_on_timeout = true

# Email Configuration (SMTP)
[email]
backend = "smtp"
host = "smtp.example.com"
port = 587
use_tls = true
username = "noreply@example.com"
password = "email_password"
timeout = 10

# Logging Configuration
[logging]
level = "INFO"
handlers = ["console", "file", "sentry"]
format = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
file_path = "/var/log/ecommerce/app.log"
max_bytes = 10_485_760  # 10 MB (underscores for readability!)
backup_count = 5

# Security Settings
[security]
secret_key = "django-insecure-xyz123abc456"
allowed_origins = ["https://example.com"]
csrf_trusted_origins = ["https://example.com"]
session_cookie_age = 1_209_600  # 14 days
session_cookie_secure = true
csrf_cookie_secure = true

# Payment Gateway Configuration
[payment.stripe]
public_key = "pk_live_xxxxxxxxxxxx"
secret_key = "sk_live_xxxxxxxxxxxx"
webhook_secret = "whsec_xxxxxxxxxxxx"

[payment.paypal]
client_id = "AaBbCcDd123456"
client_secret = "EeFfGgHh789012"
mode = "live"  # or "sandbox" for testing

# Feature Flags
[features]
enable_reviews = true
enable_wishlists = true
enable_recommendations = false  # Not ready for production yet
max_cart_items = 50
session_timeout_minutes = 30
Enter fullscreen mode Exit fullscreen mode

Spot the Differences:

  • TOML has comments explaining context
  • TOML uses sections [database] instead of nested objects
  • TOML allows underscores in numbers (10_485_760 vs 10485760)
  • TOML reads like prose, JSON reads like code
  • TOML doesn't need trailing commas (common JSON error source)

When to Choose JSON

✅ Use JSON for: API Responses and Requests

// REST API Response
{
  "status": "success",
  "data": {
    "user": {
      "id": 12345,
      "username": "john_doe",
      "email": "john@example.com"
    },
    "orders": [
      {
        "id": 1001,
        "total": 299.99,
        "status": "shipped"
      },
      {
        "id": 1002,
        "total": 149.50,
        "status": "pending"
      }
    ]
  },
  "meta": {
    "timestamp": "2025-01-15T10:30:00Z",
    "version": "v1"
  }
}
Enter fullscreen mode Exit fullscreen mode

Why JSON wins:

  • Every HTTP client understands JSON
  • Compact size (crucial for network transmission)
  • Streaming support (parse as data arrives)
  • Native JavaScript integration

✅ Use JSON for: NoSQL Databases

// MongoDB Document
{
  "_id": "507f1f77bcf86cd799439011",
  "user_id": 12345,
  "products": [
    {
      "product_id": "PROD-001",
      "quantity": 2,
      "price": 49.99
    }
  ],
  "created_at": {"$date": "2025-01-15T10:30:00Z"},
  "metadata": {
    "ip_address": "192.168.1.1",
    "user_agent": "Mozilla/5.0..."
  }
}
Enter fullscreen mode Exit fullscreen mode

Why JSON wins:

  • MongoDB, CouchDB, Firebase use JSON natively
  • Flexible schema evolution
  • Nested structures map directly to documents

✅ Use JSON for: Inter-Service Communication

// Microservice Message (Kafka, RabbitMQ)
{
  "event": "order.created",
  "timestamp": 1705317000000,
  "payload": {
    "order_id": "ORD-123456",
    "user_id": 789,
    "total_amount": 299.99,
    "items": [...]
  }
}
Enter fullscreen mode Exit fullscreen mode

Why JSON wins:

  • Language-agnostic (Python, Java, Node.js all parse JSON)
  • Message queue systems built for JSON
  • Fast serialization/deserialization

✅ Use JSON for: Mobile App Configuration (Downloaded Remotely)

// Feature flags fetched from server
{
  "features": {
    "new_ui": true,
    "dark_mode": true,
    "experimental_checkout": false
  },
  "ab_tests": {
    "homepage_variant": "B",
    "pricing_model": "annual"
  },
  "version": "2.1.0"
}
Enter fullscreen mode Exit fullscreen mode

Why JSON wins:

  • Compact payload (saves mobile data)
  • Fast parsing on mobile devices
  • Works with all mobile frameworks

When to Choose TOML

✅ Use TOML for: Application Configuration Files

# config.toml - Web Application Settings

[app]
name = "My Web App"
version = "1.0.0"
debug = false  # Set to true for development

[server]
host = "0.0.0.0"
port = 8000
workers = 4
timeout = 30

# Database configuration
[database]
url = "postgresql://user:pass@localhost:5432/mydb"
pool_size = 10
echo = false  # Set to true to log all SQL queries

# Redis cache
[cache]
host = "localhost"
port = 6379
db = 0
password = ""  # Leave empty if no password set

# Email settings
[email]
smtp_host = "smtp.gmail.com"
smtp_port = 587
use_tls = true
from_email = "noreply@example.com"
Enter fullscreen mode Exit fullscreen mode

Why TOML wins:

  • Developers can read and edit easily
  • Comments explain context
  • Sections group related settings
  • No syntax errors from missing commas

✅ Use TOML for: Python Project Configuration (pyproject.toml)

# pyproject.toml - Standard Python Project Config

[project]
name = "my-awesome-package"
version = "0.1.0"
description = "A package that does amazing things"
authors = [
    {name = "John Doe", email = "john@example.com"}
]
dependencies = [
    "django>=4.2",
    "requests>=2.31.0",
    "celery>=5.3.0"
]
requires-python = ">=3.10"

[project.optional-dependencies]
dev = [
    "pytest>=7.4.0",
    "black>=23.0.0",
    "mypy>=1.5.0"
]

# Build system
[build-system]
requires = ["setuptools>=68.0"]
build-backend = "setuptools.build_meta"

# Tool configuration
[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = "test_*.py"
addopts = "-v --cov=mypackage"

[tool.black]
line-length = 88
target-version = ['py310', 'py311']
include = '\.pyi?$'

[tool.mypy]
python_version = "3.10"
warn_return_any = true
strict_optional = true
Enter fullscreen mode Exit fullscreen mode

Why TOML wins:

  • Python's standard (PEP 518, PEP 621)
  • Replaces setup.py, setup.cfg, requirements.txt
  • Tool configurations in one file
  • Human-readable and easy to maintain

✅ Use TOML for: Docker Compose Alternative (docker-compose.toml concept)

While Docker uses YAML, TOML would be clearer:

# docker-compose.toml (hypothetical - shows TOML advantage)

[services.web]
image = "nginx:latest"
ports = ["80:80", "443:443"]
volumes = [
    "./nginx.conf:/etc/nginx/nginx.conf:ro",
    "./html:/usr/share/nginx/html:ro"
]
restart = "always"

[services.app]
build = "./app"
command = "python manage.py runserver 0.0.0.0:8000"
volumes = ["./app:/code"]
environment = { DEBUG = "True", SECRET_KEY = "dev-key-123" }
depends_on = ["db", "redis"]

[services.db]
image = "postgres:15"
environment = { POSTGRES_DB = "myapp", POSTGRES_PASSWORD = "secret" }
volumes = ["postgres_data:/var/lib/postgresql/data"]

[services.redis]
image = "redis:7-alpine"
ports = ["6379:6379"]

[volumes]
postgres_data = {}  # Named volume
Enter fullscreen mode Exit fullscreen mode

Why TOML would win:

  • Clearer than YAML's whitespace sensitivity
  • Comments explain service purposes
  • Type-safe (no "true" vs true confusion)

✅ Use TOML for: Infrastructure as Code Configuration

# terraform.toml (if Terraform used TOML instead of HCL)

[provider.aws]
region = "us-east-1"
access_key = "${AWS_ACCESS_KEY}"
secret_key = "${AWS_SECRET_KEY}"

[resource.aws_instance.web]
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t2.micro"
count = 3

[resource.aws_instance.web.tags]
Name = "WebServer"
Environment = "production"
ManagedBy = "Terraform"

[resource.aws_db_instance.main]
engine = "postgres"
engine_version = "15.3"
instance_class = "db.t3.micro"
allocated_storage = 20
username = "admin"
password = "${DB_PASSWORD}"  # From environment variable

[resource.aws_db_instance.main.tags]
Name = "MainDatabase"
Environment = "production"
Enter fullscreen mode Exit fullscreen mode

Why TOML wins:

  • Clear structure for infrastructure
  • Easy to review in pull requests
  • Comments explain infrastructure decisions
  • No ambiguous syntax

✅ Use TOML for: CI/CD Pipeline Configuration

# .gitlab-ci.toml (hypothetical - clearer than YAML)

[variables]
DOCKER_DRIVER = "overlay2"
POSTGRES_DB = "test_db"
POSTGRES_USER = "test_user"
POSTGRES_PASSWORD = "test_password"

[stages]
order = ["build", "test", "deploy"]

[jobs.build]
stage = "build"
image = "docker:latest"
script = [
    "docker build -t myapp:$CI_COMMIT_SHA .",
    "docker push myapp:$CI_COMMIT_SHA"
]
only = ["main", "develop"]

[jobs.test]
stage = "test"
image = "python:3.11"
services = ["postgres:15"]
script = [
    "pip install -r requirements.txt",
    "pytest tests/ --cov=myapp",
    "black --check .",
    "mypy myapp/"
]

[jobs.deploy_production]
stage = "deploy"
script = [
    "kubectl apply -f k8s/production/",
    "kubectl rollout status deployment/myapp"
]
only = ["main"]
when = "manual"  # Require manual approval
environment = { name = "production", url = "https://myapp.com" }
Enter fullscreen mode Exit fullscreen mode

Why TOML wins:

  • More readable than YAML for complex pipelines
  • No indentation errors
  • Clear section boundaries

Real-World Framework Examples

Django: TOML for Settings (Modern Approach)

# settings.toml (replacing settings.py)

[django]
debug = false
secret_key = "your-secret-key-here"
allowed_hosts = ["example.com", "www.example.com"]

[database.default]
engine = "django.db.backends.postgresql"
name = "myproject_db"
user = "dbuser"
password = "dbpassword"
host = "localhost"
port = 5432

[installed_apps]
apps = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "myapp",
    "rest_framework"
]

[middleware]
classes = [
    "django.middleware.security.SecurityMiddleware",
    "django.middleware.common.CommonMiddleware",
    "django.middleware.csrf.CsrfViewMiddleware"
]
Enter fullscreen mode Exit fullscreen mode

Load in Python:

# settings.py
import tomli

with open("settings.toml", "rb") as f:
    config = tomli.load(f)

DEBUG = config['django']['debug']
SECRET_KEY = config['django']['secret_key']
DATABASES = {'default': config['database']['default']}
Enter fullscreen mode Exit fullscreen mode

Rust: Cargo.toml (Native TOML Usage)

# Cargo.toml - Rust's package manager uses TOML natively

[package]
name = "my-rust-project"
version = "0.1.0"
edition = "2021"
authors = ["John Doe <john@example.com>"]
description = "A blazingly fast web server"
license = "MIT"

[dependencies]
actix-web = "4.4"
tokio = { version = "1.34", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
sqlx = { version = "0.7", features = ["postgres", "runtime-tokio"] }

[dev-dependencies]
actix-rt = "2.9"
criterion = "0.5"

# Optimize for release builds
[profile.release]
opt-level = 3
lto = true
codegen-units = 1
Enter fullscreen mode Exit fullscreen mode

Why Rust chose TOML:

  • Human-readable dependency management
  • Clear section organization
  • Great developer experience

Performance Comparison: Parse Speed

I benchmarked parsing 10,000 configuration files (1KB each):

Test Environment:
- CPU: Apple M1 Pro
- Language: Python 3.11
- Libraries: json (built-in), tomli 2.0.1

Results (average over 10 runs):

JSON Parsing:
- Time: 0.34 seconds
- Memory: 12 MB
- Speed: ~29,000 files/second

TOML Parsing:
- Time: 0.89 seconds
- Memory: 15 MB
- Speed: ~11,000 files/second

Verdict: JSON is 2.6x faster for parsing
Enter fullscreen mode Exit fullscreen mode

But here's the thing: Configuration files are parsed once at startup, not millions of times per second like API responses. The 0.55-second difference doesn't matter for config files, but readability matters every day.

Migration Guide: JSON to TOML

Converting JSON Config to TOML

Before (config.json):

{
  "server": {
    "host": "0.0.0.0",
    "port": 8000,
    "debug": false
  },
  "database": {
    "host": "localhost",
    "port": 5432
  }
}
Enter fullscreen mode Exit fullscreen mode

After (config.toml):

[server]
host = "0.0.0.0"
port = 8000
debug = false

[database]
host = "localhost"
port = 5432
Enter fullscreen mode Exit fullscreen mode

Python Code to Load Both:

import json
import tomli
from pathlib import Path

def load_config():
    """Load config from TOML or fallback to JSON"""
    toml_path = Path("config.toml")
    json_path = Path("config.json")

    if toml_path.exists():
        with open(toml_path, "rb") as f:
            return tomli.load(f)
    elif json_path.exists():
        with open(json_path) as f:
            return json.load(f)
    else:
        raise FileNotFoundError("No config file found")

config = load_config()
Enter fullscreen mode Exit fullscreen mode

The Hybrid Approach: Best of Both Worlds

Modern applications often use both:

project/
├── config.toml           # Human-edited settings
├── src/
│   ├── api/
│   │   └── schemas.json  # API request/response schemas
│   └── config/
│       └── loader.py     # Loads TOML, outputs JSON internally
└── tests/
    └── fixtures/
        └── data.json     # Test data
Enter fullscreen mode Exit fullscreen mode

Smart Strategy:

  • TOML for configuration developers edit
  • JSON for data your application generates/consumes
  • Convert TOML → JSON at runtime if needed
import json
import tomli

# Load TOML config
with open("config.toml", "rb") as f:
    config = tomli.load(f)

# Convert to JSON for API endpoint
json_config = json.dumps(config, indent=2)

# Or use internally as dict (no conversion needed!)
database_host = config['database']['host']
Enter fullscreen mode Exit fullscreen mode

Decision Matrix: Quick Reference

Use Case JSON TOML Why?
REST API responses Universal support, compact
API requests HTTP standard
App configuration Readability, comments
Python projects PEP standard (pyproject.toml)
NoSQL documents Native database format
Microservices messages Language-agnostic
CI/CD config 🟡 TOML clearer than YAML/JSON
Docker config 🟡 TOML would be clearer
Infrastructure as Code 🟡 Better than YAML
Mobile app config Compact size matters
Test fixtures Easy to generate
Build tool config Cargo, Poetry use TOML

Legend: ✅ Best choice | ❌ Poor choice | 🟡 Both work, preference varies

Common Mistakes to Avoid

Mistake 1: Using TOML for APIs

# ❌ DON'T: Send TOML in HTTP responses
# Nobody expects this!
Content-Type: application/toml

[user]
id = 12345
name = "John Doe"
Enter fullscreen mode Exit fullscreen mode
//  DO: Use JSON for APIs
{
  "user": {
    "id": 12345,
    "name": "John Doe"
  }
}
Enter fullscreen mode Exit fullscreen mode

Mistake 2: Using JSON for Multi-Environment Configs

//  BAD: No comments to explain differences
{
  "database": {
    "host": "prod-db.example.com",
    "timeout": 30
  }
}
Enter fullscreen mode Exit fullscreen mode
# ✅ GOOD: Comments explain configuration
[database]
# Production database - do not change without DBA approval
host = "prod-db.example.com"
timeout = 30  # Increased from 10s due to slow queries (TICKET-1234)
Enter fullscreen mode Exit fullscreen mode

Mistake 3: Deep Nesting in TOML

# ❌ UGLY: Too much nesting
[level1.level2.level3.level4]
value = "hard to find"
Enter fullscreen mode Exit fullscreen mode
# ✅ BETTER: Flatten structure
[level1_level2]
level3_level4_value = "easier to scan"

# OR restructure your data
Enter fullscreen mode Exit fullscreen mode

Tools and Libraries

Python

# Reading TOML
pip install tomli        # Fast, built into Python 3.11+
pip install toml         # Legacy, slower

# Writing TOML
pip install tomli-w      # Minimal writer
Enter fullscreen mode Exit fullscreen mode
import tomli
import tomli_w

# Read TOML
with open("config.toml", "rb") as f:
    config = tomli.load(f)

# Write TOML
data = {"server": {"host": "localhost", "port": 8000}}
with open("output.toml", "wb") as f:
    tomli_w.dump(data, f)
Enter fullscreen mode Exit fullscreen mode

JavaScript/Node.js

npm install @iarna/toml    # Fast TOML parser
npm install toml           # Alternative parser
Enter fullscreen mode Exit fullscreen mode
const toml = require('@iarna/toml');
const fs = require('fs');

// Read TOML
const config = toml.parse(fs.readFileSync('config.toml', 'utf-8'));
console.log(config.server.host);

// Write TOML
const data = { server: { host: 'localhost', port: 8000 } };
fs.writeFileSync('output.toml', toml.stringify(data));
Enter fullscreen mode Exit fullscreen mode

Rust

[dependencies]
toml = "0.8"      # TOML parser
serde = "1.0"     # Serialization framework
Enter fullscreen mode Exit fullscreen mode
use std::fs;
use toml::Value;

fn main() {
    // Read TOML
    let contents = fs::read_to_string("config.toml")
        .expect("Failed to read file");
    let config: Value = toml::from_str(&contents)
        .expect("Failed to parse TOML");

    println!("{:?}", config["server"]["host"]);
}
Enter fullscreen mode Exit fullscreen mode

Conclusion: Choose Based on Purpose, Not Hype

The Golden Rules:

  1. JSON for data in motion (APIs, messages, data transfer)
  2. TOML for data at rest (configuration files, project settings)
  3. When in doubt: If humans edit it regularly → TOML. If machines exchange it → JSON.

My Recommendation:

  • Starting a new Python project? → Use TOML (pyproject.toml)
  • Building a REST API? → Use JSON (standard practice)
  • Writing config files? → Use TOML (better DX)
  • Storing user data? → Use JSON (database compatibility)

Both formats have their place. The worst choice isn't picking JSON or TOML—it's using the wrong format for the job.


Found this comparison helpful? 👏 Clap if you're switching to TOML for configs!

Want more developer tool comparisons? 🔔 Follow me for deep dives into YAML vs TOML, REST vs GraphQL, and more.

Help others make better format choices! 📤 Share this guide with your team.

Team JSON or Team TOML? 💬 Drop your preference in the comments with your use case!


Tags: #TOML #JSON #Configuration #Python #Programming #DevTools #SoftwareEngineering #WebDevelopment #BestPractices #DeveloperExperience

Top comments (0)