spyderproxy

Python String to JSON: Parse and Fix Errors

A

Alex R.

|
Published date

Mon May 04 2026

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.

The Basics

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
stringstr
number (int)int
number (float)float
true / falseTrue / False
nullNone

The 5 Common Errors

1. Single quotes

# 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.

2. Trailing commas

# 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)

3. NaN and Infinity

# 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)

4. Comments (not allowed)

# 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}

5. Bytes vs string

# 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'))

Schema Validation

jsonschema (cross-language standard)

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

pydantic (Python-native, faster)

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)

Performance: orjson vs ujson vs json

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

8 Working Examples

# 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))