json.dumps() in Python

json.dumps() converts a Python dictionary, list, or any JSON-serializable object into a JSON string — the function is part of Python's built-in json module (no pip install needed since Python 2.6). Its counterpart json.dump() writes directly to a file; json.dumps() (the "s" stands for string) returns the JSON as a Python str object. The 3 most-used parameters are indent (default None = compact one-line output), sort_keys (default False), and default (a callable for handling non-serializable types). This guide covers all 8 parameters, custom encoders for datetime, Decimal, and arbitrary classes, and common pitfalls like circular references and non-string keys.

Validate and format your JSON output directly in Jsonic.

Open JSON Formatter

json.dumps() syntax and parameters

The full function signature is: json.dumps(obj, *, skipkeys=False, ensure_ascii=True, check_circular=True, allow_nan=True, cls=None, indent=None, separators=None, default=None, sort_keys=False, **kw). All parameters after obj are keyword-only (enforced by the * separator). In practice you will only use a handful of them — the rest exist for edge cases or advanced encoder customization.

import json

# Basic dict to JSON string
data = {"name": "Alice", "age": 30, "active": True, "score": None}
print(json.dumps(data))
# {"name": "Alice", "age": 30, "active": true, "score": null}

# Nested object
nested = {
    "user": {"id": 1, "roles": ["admin", "editor"]},
    "meta": {"page": 1, "total": 42}
}
print(json.dumps(nested))
# {"user": {"id": 1, "roles": ["admin", "editor"]}, "meta": {"page": 1, "total": 42}}

# List of dicts
items = [{"id": 1, "name": "foo"}, {"id": 2, "name": "bar"}]
print(json.dumps(items))
# [{"id": 1, "name": "foo"}, {"id": 2, "name": "bar"}]

# Scalar values are also valid
print(json.dumps(42))       # 42
print(json.dumps("hello"))  # "hello"
print(json.dumps(True))     # true
print(json.dumps(None))     # null

The table below lists every parameter with its type, default value, and effect. For most API response and logging use cases you only need indent, sort_keys, and default.

ParameterTypeDefaultEffect
indentint / strNoneNumber of spaces for pretty-printing; use 2 or 4. Pass a string like '\t' for tabs.
sort_keysboolFalseSorts dict keys alphabetically for consistent, diffable output.
separatorstuple(', ', ': ')Custom (item_sep, key_sep); use (',', ':') for compact output.
ensure_asciiboolTrueEscapes non-ASCII as \uXXXX when True; set False to output UTF-8 directly.
defaultcallableNoneCalled for non-serializable objects; should return a serializable value or raise TypeError.
skipkeysboolFalseSkip non-string dict keys instead of raising TypeError.
check_circularboolTrueDetect circular references; set False only when you are certain there are none.
allow_nanboolTrueAllow float('nan'), float('inf') — technically invalid JSON but accepted by many parsers.

Pretty-printing with indent and sort_keys

Passing indent=2 or indent=4 switches json.dumps() from compact single-line output to human-readable multi-line output. This is the first thing most developers reach for when logging or writing a config file. Combining indent with sort_keys=True produces output that is consistent across Python versions and dict insertion orders — essential for reproducible diffs in version control. You can also format JSON in Python programmatically for display or storage purposes.

import json

data = {"zebra": 3, "apple": 1, "mango": 2, "nested": {"b": 2, "a": 1}}

# indent=2 (compact but readable)
print(json.dumps(data, indent=2))
# {
#   "zebra": 3,
#   "apple": 1,
#   "mango": 2,
#   "nested": {
#     "b": 2,
#     "a": 1
#   }
# }

# indent=4 (Python default style)
print(json.dumps(data, indent=4))

# indent='	' (tab-indented output)
print(json.dumps(data, indent='	'))

# sort_keys=True — alphabetical order regardless of dict insertion order
print(json.dumps(data, indent=2, sort_keys=True))
# {
#   "apple": 1,
#   "mango": 2,
#   "nested": {
#     "a": 1,
#     "b": 2
#   },
#   "zebra": 3
# }

# Compact / minified — use separators=(',', ':') to strip spaces
print(json.dumps(data, separators=(',', ':')))
# {"zebra":3,"apple":1,"mango":2,"nested":{"b":2,"a":1}}

The default separators value is (', ', ': ') — note the trailing space after each comma and colon. Switching to (',', ':') removes those spaces, shrinking a typical JSON payload by roughly 15–20%. The table below illustrates the size difference for the example above.

CallOutput styleApprox. size
json.dumps(data)Compact with spaces56 bytes
json.dumps(data, separators=(',',':'))Minified, no spaces46 bytes (−18%)
json.dumps(data, indent=2)Pretty (2-space indent)88 bytes
json.dumps(data, indent=4)Pretty (4-space indent)104 bytes

Custom serializers: datetime, Decimal, and custom classes

Python's json module only knows how to serialize 7 native types: dict, list, tuple, str, int, float, bool, and None. For everything else — datetime, Decimal, dataclasses, or any custom class — you must provide a conversion strategy via the default parameter or by subclassing json.JSONEncoder. The quickest approach is default=str, which calls str() on any non-serializable object. For more control, write a custom function or encoder.

import json
from datetime import datetime, date
from decimal import Decimal
from dataclasses import dataclass, asdict

# ── 1. Quickest fix: default=str ──────────────────────────────────────────
data = {"created_at": datetime(2026, 5, 11, 9, 0, 0), "price": Decimal("19.99")}
print(json.dumps(data, default=str))
# {"created_at": "2026-05-11 09:00:00", "price": "19.99"}


# ── 2. Lambda for ISO 8601 datetime ──────────────────────────────────────
def datetime_default(obj):
    if isinstance(obj, (datetime, date)):
        return obj.isoformat()
    if isinstance(obj, Decimal):
        return float(obj)
    raise TypeError(f"Object of type {type(obj).__name__} is not JSON serializable")

print(json.dumps(data, default=datetime_default))
# {"created_at": "2026-05-11T09:00:00", "price": 19.99}


# ── 3. Custom JSONEncoder subclass ────────────────────────────────────────
class AppEncoder(json.JSONEncoder):
    def default(self, obj):
        if isinstance(obj, (datetime, date)):
            return obj.isoformat()
        if isinstance(obj, Decimal):
            return str(obj)          # keep full precision as string
        if isinstance(obj, set):
            return sorted(obj)       # sets → sorted list
        if hasattr(obj, '__dict__'):
            return obj.__dict__      # generic fallback for custom classes
        return super().default(obj)  # raises TypeError for truly unsupported types

data2 = {
    "ts": datetime(2026, 5, 11),
    "price": Decimal("9.99"),
    "tags": {"python", "json"},
}
print(json.dumps(data2, cls=AppEncoder, indent=2))


# ── 4. Dataclass serialization ────────────────────────────────────────────
@dataclass
class Product:
    id: int
    name: str
    price: Decimal

product = Product(id=1, name="Widget", price=Decimal("4.99"))

# Option A: dataclasses.asdict() + custom default
print(json.dumps(asdict(product), default=str))
# {"id": 1, "name": "Widget", "price": "4.99"}

# Option B: __dict__ directly (works for non-nested dataclasses)
print(json.dumps(product.__dict__, default=str))

When you need Decimal to round-trip without precision loss, serialize it as a str (not float). Floating-point representation can silently alter values like Decimal("0.1"). If your consumer supports it, you can also output raw numeric strings by overriding the encoder's encode() method — but for most use cases, a string is the safest choice. For further reading, see parse JSON in Python with json.loads() to understand how these serialized strings are decoded on the other end.

json.dumps() vs json.dump() — string vs file

The difference between the two functions is where the output goes. json.dumps() (dumps = "dump to string") returns a str you can use anywhere in memory. json.dump() (dump = "dump to file") writes directly to an open file object and returns None. Both functions accept identical optional parameters — indent, sort_keys, default, separators, and the rest. Choosing between them is purely a question of where you need the output. Learn how to write JSON to a file with json.dump() for the full file-writing workflow.

import json

data = {"id": 1, "name": "Alice", "active": True}

# ── json.dumps() — returns a str ──────────────────────────────────────────
json_str = json.dumps(data, indent=2)
print(type(json_str))  # <class 'str'>
print(json_str)
# {
#   "id": 1,
#   "name": "Alice",
#   "active": true
# }

# Use cases for json.dumps():
# 1. Build an HTTP API response body
response_body = json.dumps({"status": "ok", "data": data})

# 2. Log structured data
import logging
logging.info("User updated: %s", json.dumps(data))

# 3. Store as a string in a database column
db_value = json.dumps(data)

# 4. Cache JSON in Redis
# redis_client.set("user:1", json.dumps(data))


# ── json.dump() — writes to a file object, returns None ──────────────────
with open("output.json", "w", encoding="utf-8") as f:
    json.dump(data, f, indent=2, ensure_ascii=False)
# returns None — output goes directly to the file

# Equivalent using json.dumps() — less efficient for large data
with open("output.json", "w", encoding="utf-8") as f:
    f.write(json.dumps(data, indent=2, ensure_ascii=False))

For large objects, json.dump() is more memory-efficient because it streams output to the file without building the entire string in memory first. For small to medium payloads (under a few MB), the difference is negligible and json.dumps() is more flexible since you can inspect, log, or transform the string before writing it anywhere.

Common errors and edge cases

Most json.dumps() failures fall into four categories: non-serializable types, non-string keys, circular references, and Unicode handling. Understanding each saves significant debugging time. The flatten nested JSON in Python guide covers additional patterns for reshaping complex structures before serialization.

import json
from datetime import datetime
from decimal import Decimal

# ── 1. TypeError: Object of type X is not JSON serializable ──────────────
data = {"ts": datetime.now()}
try:
    json.dumps(data)
except TypeError as e:
    print(e)  # Object of type datetime is not JSON serializable

# Fix: use default=str or a custom encoder
print(json.dumps(data, default=str))

# Other common non-serializable types and their fixes:
fixes = {
    "decimal":     json.dumps({"v": Decimal("3.14")}, default=float),
    "set":         json.dumps({"v": {1, 2, 3}}, default=list),
    "bytes":       json.dumps({"v": b"hello"}, default=lambda o: o.decode()),
    # numpy arrays: json.dumps({"v": np_array}, default=lambda o: o.tolist())
}
print(fixes)


# ── 2. Non-string keys ────────────────────────────────────────────────────
bad = {1: "one", 2: "two"}
try:
    json.dumps(bad)
except TypeError as e:
    print(e)  # keys must be str, int, float, bool or None, not int
              # (actually int keys ARE allowed but produce string keys in JSON)

# Actually Python's json module converts int/float/bool/None keys to strings:
print(json.dumps({1: "one", True: "yes", None: "null-key"}))
# {"1": "one", "true": "yes", "null": "null-key"}

# For truly unsupported keys (e.g. tuple keys), use skipkeys=True:
print(json.dumps({(1, 2): "tuple key", "normal": "ok"}, skipkeys=True))
# {"normal": "ok"}  — the tuple key is silently skipped


# ── 3. Circular references ────────────────────────────────────────────────
circular = {}
circular["self"] = circular
try:
    json.dumps(circular)
except ValueError as e:
    print(e)  # Circular reference detected

# Disable the check only when you are 100% sure there are no circular refs:
# json.dumps(data, check_circular=False)  — will recurse forever if there is one


# ── 4. ensure_ascii=False for Unicode output ──────────────────────────────
text = {"city": "Tōkyō", "greeting": "こんにちは"}

# Default: non-ASCII escaped
print(json.dumps(text))
# {"city": "T\u014dky\u014d", "greeting": "\u3053\u3093\u306b\u3061\u306f"}

# ensure_ascii=False: output actual UTF-8 characters
print(json.dumps(text, ensure_ascii=False))
# {"city": "Tōkyō", "greeting": "こんにちは"}

A subtle edge case: allow_nan=True (the default) lets you serialize float('nan'), float('inf'), and float('-inf') as the JavaScript literals NaN, Infinity, and -Infinity. These are not valid JSON per RFC 8259 — most strict parsers will reject them. Set allow_nan=False if you need strict JSON compliance and want json.dumps() to raise ValueError for these values instead. Use the JSON formatter tool to verify your output is valid JSON before sending it to an external API.

Frequently asked questions

What is the difference between json.dumps() and json.dump()?

json.dumps() returns the JSON as a Python str object — the "s" stands for string. json.dump() writes directly to a file object (or any object with a write() method) and returns None. Use json.dumps() when you need the JSON string in memory (for API responses, logging, or caching), and json.dump() when writing directly to a file. Both accept the same optional parameters: indent, sort_keys, default, separators, and others.

How do I serialize a datetime object to JSON in Python?

datetime is not JSON-serializable by default. The quickest fix is json.dumps(data, default=str), which converts any non-serializable object to its string representation (e.g., datetime(2026, 5, 11) "2026-05-11 00:00:00"). For ISO 8601 format, use default=lambda o: o.isoformat() if isinstance(o, datetime) else str(o). For full control over multiple types, subclass json.JSONEncoder and override its default() method.

How do I pretty print a Python dictionary as JSON?

Pass indent=2 (or indent=4): json.dumps(data, indent=2). Python uses the indent value as the number of spaces per indentation level. Add sort_keys=True to sort keys alphabetically, which makes the output consistent and easier to diff. You can also pass a string like indent='\t' for tab-based indentation — Python's json module accepts any string as the indent value, not just integers.

Why does json.dumps() raise TypeError: Object of type X is not JSON serializable?

Python's json module can only serialize 7 types natively: dict, list, tuple, str, int/float, bool, and None. Common culprits include datetime (use .isoformat()), Decimal (convert to float or str), set (convert to list), bytes (decode to str), numpy arrays (use .tolist()), and custom class instances (use __dict__). Fix all of these with a default function or a custom JSONEncoder subclass.

How do I produce the most compact JSON string with json.dumps()?

Pass separators=(',', ':'): json.dumps(data, separators=(',', ':')). By default, json.dumps() uses (', ', ': ') (with spaces after each separator). Removing those spaces reduces the output size by roughly 15–20% for typical objects — useful for network transfer or embedding JSON in URLs. This is the most compact encoding the standard json module can produce without a third-party library.

Does json.dumps() handle Unicode characters?

By default, json.dumps() escapes all non-ASCII characters as \uXXXX sequences (ensure_ascii=True). To output UTF-8 characters directly, pass ensure_ascii=False: `json.dumps({"city": "Tōkyō"}, ensure_ascii=False)` '{"city": "Tōkyō"}'. Note that ensure_ascii=False produces a Python str containing actual Unicode characters — safe for HTTP responses with Content-Type: application/json; charset=utf-8, but be careful when embedding in ASCII-only contexts.

Validate your JSON output

Paste the output of json.dumps() into Jsonic to confirm it is valid JSON before sending it to an API or saving to a file.

Open JSON Formatter