json1-3 Exercise 2: Dataclasses, Decimals and Dates in Both Directions ======================================================================= Run with Python 3.14.0: py ex2.py --- ex2.py --- # Exercise 2: hooks - a dataclass with Decimal and dates, out and back import json, dataclasses, datetime as dt from decimal import Decimal @dataclasses.dataclass class Line: sku: str price: Decimal added: dt.date order = {'id': 1001, 'placed': dt.datetime(2026, 10, 12, 8, 30, tzinfo=dt.timezone.utc), 'lines': [Line('A1', Decimal('19.99'), dt.date(2026, 1, 2)), Line('B2', Decimal('0.10'), dt.date(2026, 3, 4))], 'tags': {'rush', 'gift'}} print('1. plain dumps:') try: json.dumps(order) except TypeError as e: print(' TypeError:', e) def default(o): if isinstance(o, (dt.datetime, dt.date)): return o.isoformat() if isinstance(o, Decimal): return str(o) # a string keeps every digit if dataclasses.is_dataclass(o): return dataclasses.asdict(o) if isinstance(o, (set, frozenset)): return sorted(o) raise TypeError('Object of type ' + type(o).__name__ + ' is not JSON serializable') text = json.dumps(order, default=default, indent=2) print('\n2. dumps with default=:\n' + text) def hook(o): if 'placed' in o: o['placed'] = dt.datetime.fromisoformat(o['placed']) if 'added' in o: o['added'] = dt.date.fromisoformat(o['added']) if 'price' in o: o['price'] = Decimal(o['price']) return o back = json.loads(text, object_hook=hook) print('\n3. loads with object_hook:') print(' placed ', repr(back['placed']), '== original:', back['placed'] == order['placed']) print(' line 0 ', back['lines'][0]) print(' price is Decimal:', isinstance(back['lines'][0]['price'], Decimal), '| tags is now a', type(back['tags']).__name__, '(a set cannot come back by itself)') print('\n4. money arithmetic:') prices_text = '[0.1, 0.2]' print(' default floats :', sum(json.loads(prices_text))) print(' parse_float=Decimal:', sum(json.loads(prices_text, parse_float=Decimal))) print('\n5. duplicate keys:') def no_dupes(pairs): d = {} for k, v in pairs: if k in d: raise ValueError('duplicate key: ' + repr(k)) d[k] = v return d print(' default :', json.loads('{"a":1,"a":2}')) try: json.loads('{"a":1,"a":2}', object_pairs_hook=no_dupes) except ValueError as e: print(' strict hook : ValueError:', e) print(' strict hook on clean input:', json.loads('{"a":1,"b":2}', object_pairs_hook=no_dupes)) Output: 1. plain dumps: TypeError: Object of type datetime is not JSON serializable 2. dumps with default=: { "id": 1001, "placed": "2026-10-12T08:30:00+00:00", "lines": [ { "sku": "A1", "price": "19.99", "added": "2026-01-02" }, { "sku": "B2", "price": "0.10", "added": "2026-03-04" } ], "tags": [ "gift", "rush" ] } 3. loads with object_hook: placed datetime.datetime(2026, 10, 12, 8, 30, tzinfo=datetime.timezone.utc) == original: True line 0 {'sku': 'A1', 'price': Decimal('19.99'), 'added': datetime.date(2026, 1, 2)} price is Decimal: True | tags is now a list (a set cannot come back by itself) 4. money arithmetic: default floats : 0.30000000000000004 parse_float=Decimal: 0.3 5. duplicate keys: default : {'a': 2} strict hook : ValueError: duplicate key: 'a' strict hook on clean input: {'a': 1, 'b': 2} WHY THIS WORKS AS AN ANSWER --------------------------- Step 1 shows the failure the default function must fix. The default function covers every non-JSON type in the order (datetime, date, Decimal, dataclass, set); dataclasses.asdict handles the nesting and default is called again for the Decimal and date inside each line, which is why the output shows prices as strings and dates as ISO text. The set is sorted so the output is the same on every run. Step 3 uses == on the datetime to prove the round trip (a printed value could look right and still differ), and shows the Decimal is a real Decimal. The tags set comes back as a list, which the output states: JSON cannot say "set". Step 4 uses 0.1 + 0.2 because that sum really fails in floating point (0.30000000000000004), unlike my first choice of 19.99 + 0.10 + 0.20, which printed 20.29 and showed nothing. The duplicate-key hook is tested on both a duplicate (rejected) and clean input (accepted). Caveat: the object_hook converts by key name at any depth, so a key called "price" anywhere in the document would be turned into a Decimal.