json.load() and json.dumps() cover the basic case in one line each, but production JSON handling needs to deal with types JSON doesn't natively support, malformed input from external sources, and files too large to load entirely into memory. This lesson covers all three.
1Custom Serialization: Handling Types JSON Doesn't Natively Support
JSON's type system is deliberately minimal ā strings, numbers, booleans, null, arrays, and objects ā which means any Python type outside that set (datetime, Decimal, a custom class instance, a set) has no automatic, unambiguous JSON representation, and json.dumps() raises TypeError the moment it encounters one rather than guessing at a conversion that might be wrong for your specific use case.
The default parameter is the sanctioned extension point for this: a function you provide that json.dumps() calls specifically when it hits an object it can't serialize natively, and whose return value is used in its place (recursively re-serialized if needed). json_default checking isinstance(obj, datetime) and returning obj.isoformat() converts any datetime into a standard, unambiguous, ISO 8601 string representation ā a convention widely understood by other tools and languages that will eventually deserialize this same data.
The default function should always end with a raise TypeError(...) for any type it doesn't explicitly handle (rather than silently returning something like str(obj) for everything), so that genuinely unexpected, unhandled types still fail loudly with a clear error rather than being serialized into some plausible-but-wrong representation that might cause quiet data-quality problems downstream.
import json
from datetime import datetime
data = {"created_at": datetime.now()}
json.dumps(data) # TypeError: Object of type datetime is not JSON serializabledatetime ā ISO 8601 string, cleanly
2JSONDecodeError: Handling Malformed Input From External Sources
Any JSON your code parses that didn't originate from your own json.dumps() call ā a webhook payload, a file uploaded by a user, a response from a third-party API ā should be treated as untrusted input that might not actually be valid JSON at all. json.loads() raises json.JSONDecodeError (a ValueError subclass) when parsing fails, and ā following the same precise-exception-catching discipline the Advanced Error Handling section established ā catching that specific exception type, rather than a bare except:, ensures you only handle genuine JSON parsing failures without accidentally swallowing unrelated bugs elsewhere in the same function.
JSONDecodeError carries genuinely useful structured attributes beyond its message: .lineno and .colno pinpoint exactly where in the input the parser gave up, and .msg describes what specifically went wrong (an unexpected character, an unterminated string, a trailing comma) ā directly analogous to the structured exception attributes covered in the Custom Exceptions lesson, letting calling code (or a log line) report precisely where malformed input failed rather than just 'JSON parsing failed somewhere.'
This matters practically for building resilient systems: a webhook handler receiving occasionally malformed payloads from a third party should log the specific parse failure (with the useful lineno/colno detail) and return an appropriate error response, rather than letting an unhandled JSONDecodeError crash the request handler entirely ā exactly the graceful, informative failure handling this platform's error-handling section builds toward generally, applied specifically to JSON parsing.
import json
from datetime import datetime
def json_default(obj):
if isinstance(obj, datetime):
return obj.isoformat()
raise TypeError(f"Type {type(obj)} is not JSON serializable")
data = {"created_at": datetime.now()}
print(json.dumps(data, default=json_default)) # {"created_at": "2026-08-10T14:30:00"}Structured, actionable detail about exactly where parsing failed
3Loading Large JSON Files Without Exhausting Memory
json.load(f) reads the *entire* file into memory and parses it as one complete operation, returning a single, fully-materialized Python object ā perfectly fine for files of a reasonable size, but a genuine problem for a JSON file large enough that loading it entirely could exhaust available memory, since there's no way to get a partial result or process it incrementally with the standard library's json module alone.
For genuinely large JSON files, the common real-world pattern is JSON Lines (also called NDJSON) ā a file format where each line is an independent, complete JSON object, rather than one giant JSON array spanning the whole file ā which can be processed lazily, one line (one record) at a time, using a generator exactly like the File Processing streaming techniques covered later in this section: for line in f: record = json.loads(line), never holding more than one record in memory at once.
For a file that's genuinely one enormous JSON array or object rather than JSON Lines, and can't be restructured, a streaming JSON parser like the third-party ijson library becomes necessary ā it parses incrementally, yielding pieces of the structure as they're encountered rather than requiring the whole document to be read first. Recognizing which situation you're in ā 'a reasonably-sized JSON file, json.load() is fine' versus 'a JSON Lines file, iterate lazily' versus 'one enormous JSON document, need a streaming parser' ā is the practical judgment call that separates production-ready JSON handling from code that works fine in testing and fails unexpectedly against real production data volumes.
import json
def safe_load(raw: str) -> dict | None:
try:
return json.loads(raw)
except json.JSONDecodeError as e:
print(f"Invalid JSON at line {e.lineno}, column {e.colno}: {e.msg}")
return NoneConstant memory, regardless of total file size
4Step-by-Step Breakdown
json.dumps() works perfectly until you try to serialize a datetime, and then it just raises TypeError. Let's handle that, and the other real gotchas.
json.dumps() raises TypeError on types it doesn't know how to serialize -- datetime being the most common real-world case.
A custom 'default' function tells json.dumps() how to handle types it doesn't natively understand -- fixing the error cleanly.
Checkpoint: What does passing default=json_default to json.dumps() accomplish?
- āIt lets you define how to convert a type json.dumps() does not natively understand into something serializable
- āIt makes JSON serialization run measurably faster
json.JSONDecodeError is a SPECIFIC exception -- catch it specifically to handle malformed JSON from external sources gracefully.
Checkpoint: Why catch json.JSONDecodeError specifically, instead of a bare except: around json.loads()?
- āIt catches only genuine JSON parsing failures (with useful lineno/colno/msg details) without accidentally hiding unrelated bugs
- āThere's no real difference -- both accomplish the same thing
JSON handles hierarchical data cleanly; YAML is the next format, used heavily for human-edited configuration.
Handle Real Malformed JSON. Finish safe_parse_json(): JSONDecodeError lets you handle bad data gracefully.
Level Up š
Advanced cheat sheets, SEO tricks, and interview prep for this topic.
Browser Support
Fully supported (via server-side Python execution).
Fully supported (via server-side Python execution).
Fully supported (via server-side Python execution).
Fully supported (via server-side Python execution).
Best Practices
Provide a default= function for json.dumps() covering every non-standard type your data might contain
This avoids TypeError crashes on common types like datetime, and gives you explicit control over the serialized representation rather than an ad-hoc workaround at each call site.
Catch json.JSONDecodeError specifically when parsing JSON from any external or untrusted source
It provides genuinely useful lineno/colno/msg detail for diagnosing malformed input, and avoids the risk of a bare except: silently swallowing unrelated bugs.
Frequent Bugs
Calling json.dumps() on data containing a datetime (or other non-JSON-native type) without a default= handler, causing an unhandled TypeError in production the first time real data includes that type.
Provide a default= function to json.dumps() that explicitly converts any non-standard types your data might contain (datetime, Decimal, custom objects) into a JSON-serializable representation.
Real-World Examples
Safely Parsing a Third-Party Webhook Payload
A webhook endpoint receives JSON payloads from a third-party service, and occasionally receives malformed or truncated payloads due to network issues on the sender's side.
import json
from flask import request
def handle_webhook():
try:
payload = json.loads(request.data)
except json.JSONDecodeError as e:
logger.warning(f"Malformed webhook payload: {e.msg} at line {e.lineno}")
return {"error": "Invalid JSON payload"}, 400
process_webhook(payload)
return {"status": "ok"}, 200