Serialize Datetime and Decimal Values to JSON
Serialize datetime and Decimal values to JSON with an explicit representation and reject unsupported types.
Example check: Worked and guided code samples are checked against CPython 3.13. Practice uses Pyodide 0.29.5 (Python 3.13) in your browser. How we check lessons
Video lesson: Serialize Datetime and Decimal Values to JSON
The preview is stored on this site. YouTube loads only when you press Play. Watch on YouTube
Jump to a chapter
Read the video transcript
Select a timestamp to watch that moment on YouTube.
- 00:00
A report contains a Decimal amount and a timezone-aware datetime. We want to send it as JSON to another service. Before running anything, predict whether plain json.dumps can represent both values. JSON has strings and numbers, but it has no Decimal or datetime tag. A serializer must choose a representation the receiver can understand. For money, a decimal string preserves 12.30 exactly, including the trailing zero. For time, ISO 8601 gives an explicit timestamp and offset. Today we will make that policy visible, test unsupported values, and show what
- 00:40
decoding does with the resulting strings. The JSON format has strings, numbers, booleans, null, arrays, and objects, but no native datetime or Decimal type. Calling json.dumps on a dictionary containing either Python object raises TypeError. That is useful feedback: a serializer needs a decision about what representation another program can understand. A common contract writes a datetime as an ISO 8601 string and a Decimal as a decimal string. Strings preserve an exact monetary value such as 12.30, including its trailing zero.
- 01:21
They do not automatically restore Python types when read back. The first run raises TypeError on Decimal. That is preferable to silently changing its meaning. A common quick fix passes default=str, which appears to work, but now an accidental set can also become a string. The second run demonstrates that failure of validation: JSON output exists even though the schema never allowed a set. Ask whether a future consumer could reconstruct the intended value from that string. It cannot. The callback should recognize only the Python types named in our contract and raise for all other unsupported objects.
- 02:02
We will build that callback inside encode_report. The tempting default=str shortcut makes almost any unsupported object look serializable. It can hide a set or custom object that your API never intended to accept. Write a narrow default function instead: recognize datetime, recognize Decimal, and raise TypeError for anything else. Pass it as the default argument to json.dumps. The callback runs only for objects the standard encoder cannot handle. Ordinary strings, integers, lists, and dictionaries continue through the normal encoder. Use sort_keys=True only when deterministic display is useful, such as a test or
- 02:46
video; key ordering is not a substitute for a data contract. The explicit encoder checks datetime and Decimal with isinstance. A datetime becomes its isoformat string, including the UTC offset when it is aware. A Decimal becomes str(value) so 12.30 remains the characters 12.30 instead of a binary floating-point approximation. json.dumps handles ordinary JSON- compatible values itself, so the callback does not need cases for strings or integers. We set sort_keys=True to make the demonstration and tests deterministic.
- 03:27
That option is about key display order; the conversion policy comes from the callback and the contract written for each field. Decoding needs a separate choice. json.loads returns the ISO timestamp and decimal amount as strings because that is what was written into JSON. A consumer may call datetime.fromisoformat and Decimal on known fields after validating them, or use a carefully scoped object_hook. Never assume an arbitrary string that resembles a date should be converted throughout a document. That could change a legitimate product code. This lesson keeps the serialized wire shape explicit and demonstrates a field-
- 04:08
level round trip, including the fact that Decimal('12.30') stays exact when converted from its string. The corrected output is an ordinary JSON object with two strings. This means a consumer using json.loads sees strings, not Python datetime and Decimal instances. That is expected. A receiver should know which fields carry these representations and validate them before calling datetime.fromisoformat or Decimal. Automatically converting every date- looking string in a document could alter a product identifier or a note. The task keeps the encoder narrow and asks you to preserve nested ordinary values.
- 04:52
It also checks that a set raises TypeError instead of being silently accepted. Timezones matter in serialized datetimes. A naive datetime has no offset, so isoformat does not tell a remote consumer which instant it represents. The prepared task uses an aware UTC datetime to avoid that ambiguity. The encoder does not mutate the original dictionary or objects. It converts only when json.dumps encounters an unsupported value, including one nested in a list or dictionary. A broad catch-all callback can mask genuine bugs, so the final branch raises TypeError.
- 05:32
On the decoding side, parse only documented fields and handle invalid text as an input error. A JSON text cannot preserve arbitrary Python identity, custom classes, or all numeric semantics on its own. Design the wire contract before relying on a convenient printout. The sample's exact amount string and timezone offset are assertions about that contract, not merely formatting choices. The last run reconstructs a known amount field from its JSON string and prints its Decimal value. Then it passes a set to the encoder and observes a clear failure. Think about the boundary: storing a decimal amount as a JSON string is a
- 06:16
deliberate API agreement, not a workaround that magically extends JSON types. If your receiver wants a JSON number instead, discuss precision and conversion rules explicitly. Complete the checked exercise, answer the quiz, and explain why the two parties must share a schema for the special fields to round-trip without ambiguity. For the checked task, implement encode_report(record). Accept a dictionary containing ordinary JSON data plus datetime or Decimal values, return a JSON string, and use a custom encoder callback. Raise TypeError for an unsupported set rather than converting it to a string.
- 06:58
The prepared example prints a JSON object with an ISO timestamp and exact amount. Test a timezone-aware datetime, a nested list of Decimal values, and unsupported input. A real API should document which fields carry these strings; here the browser task makes the conversion policy visible in code.
Understand the concept
The JSON format has strings, numbers, booleans, null, arrays, and objects, but no native datetime or Decimal type. Calling json.dumps on a dictionary containing either Python object raises TypeError. That is useful feedback: a serializer needs a decision about what representation another program can understand. A common contract writes a datetime as an ISO 8601 string and a Decimal as a decimal string. Strings preserve an exact monetary value such as 12.30, including its trailing zero. They do not automatically restore Python types when read back.
The tempting default=str shortcut makes almost any unsupported object look serializable. It can hide a set or custom object that your API never intended to accept. Write a narrow default function instead: recognize datetime, recognize Decimal, and raise TypeError for anything else. Pass it as the default argument to json.dumps. The callback runs only for objects the standard encoder cannot handle. Ordinary strings, integers, lists, and dictionaries continue through the normal encoder. Use sort_keys=True only when deterministic display is useful, such as a test or video; key ordering is not a substitute for a data contract.
Decoding needs a separate choice. json.loads returns the ISO timestamp and decimal amount as strings because that is what was written into JSON. A consumer may call datetime.fromisoformat and Decimal on known fields after validating them, or use a carefully scoped object_hook. Never assume an arbitrary string that resembles a date should be converted throughout a document. That could change a legitimate product code. This lesson keeps the serialized wire shape explicit and demonstrates a field-level round trip, including the fact that Decimal('12.30') stays exact when converted from its string.
For the checked task, implement encode_report(record). Accept a dictionary containing ordinary JSON data plus datetime or Decimal values, return a JSON string, and use a custom encoder callback. Raise TypeError for an unsupported set rather than converting it to a string. The prepared example prints a JSON object with an ISO timestamp and exact amount. Test a timezone-aware datetime, a nested list of Decimal values, and unsupported input. A real API should document which fields carry these strings; here the browser task makes the conversion policy visible in code.
- JSON lacks native datetime and Decimal
- json.dumps(default=...) handles unsupported Python values
- ISO timestamps and decimal strings are explicit wire representations
- Decoding those strings requires field-aware validation
See it step by step
Read the code, predict the output, then compare it with the result.
01. See the unsupported-value error
import json
from decimal import Decimal
try:
json.dumps({'amount': Decimal('12.30')})
except TypeError as exc:
print(type(exc).__name__)
TypeErrorPlain JSON encoding rejects Decimal rather than inventing a representation.
02. Encode a known datetime field
import json
from datetime import datetime, timezone
value = {'at': datetime(2026, 10, 26, tzinfo=timezone.utc)}
print(json.dumps(value, default=lambda obj: obj.isoformat()))
{"at": "2026-10-26T00:00:00+00:00"}The callback turns a datetime into a documented ISO string.
03. Restore a known decimal field
import json
from decimal import Decimal
record = json.loads('{"amount": "12.30"}')
print(Decimal(record['amount']))
print(type(record['amount']).__name__)
12.30
strA field-aware consumer can reconstruct Decimal while the raw JSON value remains a string.
A closer look
Follow the reasoning, inspect each result, then try the suggested changes in the console below.
Identify unsupported Python values
The JSON format has strings, numbers, booleans, null, arrays, and objects, but no native datetime or Decimal type. Calling json.dumps on a dictionary containing either Python object raises TypeError. That is useful feedback: a serializer needs a decision about what representation another program can understand. A common contract writes a datetime as an ISO 8601 string and a Decimal as a decimal string. Strings preserve an exact monetary value such as 12.30, including its trailing zero. They do not automatically restore Python types when read back.
Predict which Python objects the standard encoder handles. The TypeError is a useful boundary: it tells you a new wire representation must be chosen before data leaves your process.
import json
from decimal import Decimal
try:
json.dumps({'amount': Decimal('12.30')})
except TypeError as exc:
print(type(exc).__name__)
TypeError- Run plain json.dumps with Decimal.
- Read the TypeError as a missing policy.
Use a narrow default callback
The tempting default=str shortcut makes almost any unsupported object look serializable. It can hide a set or custom object that your API never intended to accept. Write a narrow default function instead: recognize datetime, recognize Decimal, and raise TypeError for anything else. Pass it as the default argument to json.dumps. The callback runs only for objects the standard encoder cannot handle. Ordinary strings, integers, lists, and dictionaries continue through the normal encoder. Use sort_keys=True only when deterministic display is useful, such as a test or video; key ordering is not a substitute for a data contract.
The default callback receives only values the normal encoder cannot serialize. Raise TypeError at the end so an accidental set or custom class does not become an undocumented string.
import json
from datetime import datetime, timezone
value = {'at': datetime(2026, 10, 26, tzinfo=timezone.utc)}
print(json.dumps(value, default=lambda obj: obj.isoformat()))
{"at": "2026-10-26T00:00:00+00:00"}- Encode datetime and Decimal explicitly.
- Reject every other unsupported type.
Decode known fields deliberately
Decoding needs a separate choice. json.loads returns the ISO timestamp and decimal amount as strings because that is what was written into JSON. A consumer may call datetime.fromisoformat and Decimal on known fields after validating them, or use a carefully scoped object_hook. Never assume an arbitrary string that resembles a date should be converted throughout a document. That could change a legitimate product code. This lesson keeps the serialized wire shape explicit and demonstrates a field-level round trip, including the fact that Decimal('12.30') stays exact when converted from its string.
json.loads cannot infer that every ISO-looking string is a datetime. Rebuild a Decimal or datetime only in fields whose schema promises that type, and validate malformed strings at that boundary.
import json
from decimal import Decimal
record = json.loads('{"amount": "12.30"}')
print(Decimal(record['amount']))
print(type(record['amount']).__name__)
12.30
str- Read the JSON string back.
- Convert only the known amount field to Decimal.
Try it in Python
Edit the example and run it. Python starts in your browser the first time you click Run.
Python console
Ready to runNeed input()? Add one value per line
Your output appears here.
Need a hint?
Define a default(value) callback. Use isinstance for datetime and Decimal, return isoformat() or str(value), otherwise raise TypeError. Pass it to json.dumps with sort_keys=True.
Complete the task and select Check task to verify your code.
Quick quiz
Three questions. You can change your answers and try again.
Typical mistakes
Everyone meets these errors. See what causes them and how to fix them.
Passing datetime directly to json.dumps
json.dumps({'at': datetime.now()})json.dumps(data, default=encode_special)What happens: TypeError reports an unsupported datetime object.
Define a representation for values JSON lacks.
Using default=str for everything
json.dumps(data, default=str)if isinstance(value, Decimal): return str(value)
raise TypeError('unsupported type')What happens: An accidental set becomes a string instead of failing validation.
A narrow callback preserves the contract.