I prefer two-stage approach.
When serializing:
- Convert Python application objects to JSON compatible values (str, int/float, list, and dict).
- Serialize it to JSON.
When deserializing:
- Deserialize JSON to JSON compatible values.
- Convert it to application objects.
This is what pydantic does.
This approach is symmetric. User can chose converter without changing serializer. This is nice separation of concerns.
Of course, this approach have drawbacks. Temporary JSON-compat objects consumes some RAM (although most int, float, and strings are shared between application objects and JSON-compat object).
To reduce RAM usage, SAX-like parser and incremental writer will be needed. But it is not good at usability.
I think two-stage approach works fine for 99% cases. And I don’t think Python stdlib should have hard-to-use writer/parser only for 1% use cases.