Quick verdict: Use json.loads(s) to convert a Python string to JSON. The result is a dict, list, or scalar depending on the JSON type. Wrap in try/except for json.JSONDecodeError. The five errors that catch 99% of beginners: single quotes, trailing commas, NaN/Infinity, JS-style comments, and bytes-vs-string confusion. For higher throughput than the standard library, use orjson.
This guide covers the basics, the common errors and how to handle them, schema validation with pydantic and jsonschema, and 8 copy-paste examples for the patterns that matter.
import json
s = '{"name": "Alex", "age": 28, "skills": ["Python", "Playwright"]}'
data = json.loads(s)
print(type(data)) # <class 'dict'>
print(data["name"]) # Alex
print(data["skills"][0]) # Python
Type mapping from JSON to Python:
| JSON | Python |
|---|---|
| object {} | dict |
| array [] | list |
| string | str |
| number (int) | int |
| number (float) | float |
| true / false | True / False |
| null | None |
# Fails
json.loads("{'a': 1}")
# json.JSONDecodeError: Expecting property name enclosed in double quotes
# Workaround if you can't fix the source — use ast.literal_eval
import ast
ast.literal_eval("{'a': 1}") # {'a': 1}
RFC 8259 requires double quotes. Single-quoted "JSON" is actually a Python literal — different format.
# Fails
json.loads('{"a": 1,}')
# json.JSONDecodeError: Expecting property name
# Real fix: clean the source. Hack: regex
import re
clean = re.sub(r',s*([]}])', r'', '{"a": 1,}')
json.loads(clean)
# By default these are accepted (non-standard but tolerated)
json.loads('NaN') # nan
json.loads('Infinity') # inf
# Reject them strictly
def reject_constant(c):
raise ValueError(f"Invalid constant: {c}")
json.loads('NaN', parse_constant=reject_constant)
# Fails — JSON has no comments
json.loads('{"a": 1 // ignore me
}')
# If you actually need JSON-with-comments, use json5 library
# pip install json5
import json5
json5.loads('{"a": 1 // comment
}') # {'a': 1}
# Both work in Python 3.6+
json.loads('{"a": 1}') # str
json.loads(b'{"a": 1}') # bytes — UTF-8/16/32 auto-detected
# But other encodings need explicit decode
data = '{"name": "café"}'.encode('latin-1')
json.loads(data.decode('latin-1'))
pip install jsonschema
from jsonschema import validate
schema = {
"type": "object",
"properties": {
"name": {"type": "string"},
"age": {"type": "integer", "minimum": 0},
},
"required": ["name"],
}
data = json.loads('{"name": "Alex", "age": 28}')
validate(instance=data, schema=schema) # raises ValidationError if invalid
pip install pydantic
from pydantic import BaseModel
class User(BaseModel):
name: str
age: int
user = User.model_validate_json('{"name": "Alex", "age": 28}')
print(user.name) # Alex (typed)
| Library | Relative parse speed | Drop-in API? |
|---|---|---|
| json (stdlib) | 1× (baseline) | N/A |
| ujson | ~3-5× | Yes |
| orjson | ~5-10× | Mostly (returns bytes by default) |
import orjson
data = orjson.loads('{"a": 1}') # str OR bytes
# orjson.dumps returns bytes (faster); use .decode() if you need str
# 1. Simple parse
data = json.loads('{"a": 1, "b": [2, 3]}')
# 2. With error handling
try:
data = json.loads(payload)
except json.JSONDecodeError as e:
print(f"Bad JSON at line {e.lineno} col {e.colno}: {e.msg}")
# 3. Bytes input
data = json.loads(b'{"a": 1}')
# 4. Nested JSON-in-JSON (escaped)
outer = json.loads('{"data": "{"a": 1}"}')
inner = json.loads(outer["data"])
# 5. JSONL (line-delimited)
records = [json.loads(line) for line in text.splitlines() if line.strip()]
# 6. With custom hook (datetime parsing)
from datetime import datetime
def hook(d):
for k, v in d.items():
if isinstance(v, str) and "T" in v and v.endswith("Z"):
try: d[k] = datetime.fromisoformat(v.replace("Z", "+00:00"))
except ValueError: pass
return d
data = json.loads(payload, object_hook=hook)
# 7. Streaming large file (ijson)
import ijson
with open("big.json") as f:
for record in ijson.items(f, "records.item"):
process(record)
# 8. Pretty-print after parsing
print(json.dumps(json.loads(payload), indent=2, sort_keys=True))