Turning raw dicts into typed objects at the boundary keeps the rest of the code clean.
json.loads accepts hooks that run while parsing:
- object_hook=fn is called with every decoded JSON object (as a dict), innermost first. Whatever it returns replaces that dict. Use it to turn tagged dicts into objects, e.g.
{"__type__": "point", "x": 1, "y": 2}into aPoint. - object_pairs_hook=fn receives the object's contents as a list of
(key, value)pairs instead of a dict. It sees duplicate keys, which a dict would silently collapse (the last one wins), so it's the way to reject them. If both hooks are given, this one takes priority. - parse_float=fn is called with the string of every JSON float.
parse_float=Decimalkeeps exact values like"19.90"for money. - parse_int=fn does the same for integers.
- parse_constant handles
NaN,Infinityand-Infinity; raise there to reject them.
Hooks run on every nested object, so keep them fast and make them return the dict unchanged when it isn't the kind they convert.
For anything beyond a couple of types, converting after decoding is often clearer: parse to dicts, then construct your objects with explicit code or a validation library.
Example
import json
from decimal import Decimal
def no_duplicates(pairs):
keys = [k for k, _ in pairs]
dupes = {k for k in keys if keys.count(k) > 1}
if dupes:
raise ValueError(f"duplicate keys: {sorted(dupes)}")
return dict(pairs)
text = '{"price": 19.90, "qty": 3}'
print(json.loads(text)) # {'price': 19.9, 'qty': 3}
print(json.loads(text, parse_float=Decimal)) # {'price': Decimal('19.90'), 'qty': 3}
print(json.loads('{"a": 1, "a": 2}')) # {'a': 2}
try:
json.loads('{"a": 1, "a": 2}', object_pairs_hook=no_duplicates)
except ValueError as e:
print(e) # duplicate keys: ['a']
point = lambda d: (d["x"], d["y"]) if d.keys() == {"x", "y"} else d
print(json.loads('{"p": {"x": 1, "y": 2}}', object_hook=point)) # {'p': (1, 2)}Remember
object_hooktransforms every decoded object, innermost firstobject_pairs_hooksees raw key/value pairs, including duplicatesparse_float=Decimalkeeps exact decimal values
Watch out for
- Forgetting that hooks run for every nested object, so returning None for 'other' dicts wipes out data.