Give user registered types priority when encoding / decoding JSON#2188
Give user registered types priority when encoding / decoding JSON#2188soceanainn wants to merge 8 commits intocelery:mainfrom
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #2188 +/- ##
=======================================
Coverage 82.45% 82.46%
=======================================
Files 79 79
Lines 10150 10161 +11
Branches 1167 1171 +4
=======================================
+ Hits 8369 8379 +10
Misses 1579 1579
- Partials 202 203 +1 ☔ View full report in Codecov by Sentry. |
52b2f86 to
247cf3e
Compare
auvipy
left a comment
There was a problem hiding this comment.
please fix the lint errors as well
e9cfe80 to
21430db
Compare
Nusnus
left a comment
There was a problem hiding this comment.
I briefly reviewed the PR and it looks nice but I wonder if anything might break due to the split, even though it makes sense.
21430db to
b3d4bb7
Compare
b3d4bb7 to
7a20cbd
Compare
technically the change is a breaking change |
|
we have to release in in a new major version like 5.6/5.7 |
There was a problem hiding this comment.
Pull request overview
This PR changes Kombu’s JSON type registration mechanism so user-registered encoders/decoders are evaluated before Kombu’s built-in default type handlers, improving behavior when users register subclasses of Kombu’s default types.
Changes:
- Split JSON type registries into user (
_encoders/_decoders) vs default (_default_encoders/_default_decoders) registries. - Update encoding/decoding to consult user registries first, then fall back to defaults.
- Add a unit test intended to validate user-registered type priority.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
kombu/utils/json.py |
Introduces separate user/default registries and updates encoder/decoder lookup order to prefer user registrations. |
t/unit/utils/test_json.py |
Adds a test case intended to validate priority behavior for user-registered types. |
Comments suppressed due to low confidence (1)
kombu/utils/json.py:79
- The
ValueErrorraised for unknown__type__currently includes the builtintypeobject, not the marker from the payload. This makes the error misleading; useo["__type__"](and possibly include available markers) instead oftype.
decoder = _decoders.get(o["__type__"]) or _default_decoders.get(o["__type__"])
if decoder:
return decoder(o["__value__"])
else:
raise ValueError("Unsupported type", type, o)
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| def _register_default_types(): | ||
| # NOTE: datetime should be registered before date, | ||
| # because datetime is also instance of date. | ||
| register_type(datetime, "datetime", datetime.isoformat, | ||
| datetime.fromisoformat) | ||
| register_type( | ||
| _register_type(datetime, "datetime", datetime.isoformat, | ||
| datetime.fromisoformat) | ||
| _register_type( |
There was a problem hiding this comment.
_register_default_types() is used as a reset mechanism in tests, but after splitting defaults into _default_encoders/_default_decoders it no longer clears user-registered _encoders/_decoders. This means a test that calls register_type (e.g. overriding uuid.UUID) can leak that override into subsequent tests. Consider clearing _encoders/_decoders (and reinitializing _default_*) at the start of _register_default_types, or adding a dedicated reset helper for tests.
| def default(self, o): | ||
| for t, (marker, encoder) in _encoders.items(): | ||
| if isinstance(o, t): | ||
| return ( | ||
| encoder(o) if marker is None else _as(marker, encoder(o)) | ||
| ) | ||
|
|
There was a problem hiding this comment.
This change gives priority to user-registered types within JSONEncoder.default, but stdlib json does not call default() for JSON primitives (e.g. str/int/float/bool/None) or their subclasses. If the intent is to solve #1895's SafeString (a str subclass) example, this implementation likely won’t affect that case; it would require intercepting encoding before the primitive fast-path (e.g. overriding iterencode/preprocessing).
There was a problem hiding this comment.
@soceanainn please cross check this and other suggestions
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
auvipy
left a comment
There was a problem hiding this comment.
can you please revisit this? some review comments need your attention
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| serialized_str = dumps(original) | ||
| # Ensure our custom marker is used instead of the default Decimal handler | ||
| assert '"mydecimal"' in serialized_str | ||
| loaded_value = loads(serialized_str) |
There was a problem hiding this comment.
assert '"mydecimal"' in serialized_str is a brittle check (it can match unrelated string content). It’s more robust to parse the JSON and assert on the __type__ field for md, which also makes the intent clearer.
| def default(self, o): | ||
| for t, (marker, encoder) in _encoders.items(): | ||
| if isinstance(o, t): | ||
| return ( | ||
| encoder(o) if marker is None else _as(marker, encoder(o)) | ||
| ) | ||
|
|
||
| reducer = getattr(o, "__json__", None) | ||
| if reducer is not None: | ||
| return reducer() |
There was a problem hiding this comment.
JSONEncoder.default now applies user-registered encoders before checking __json__. This changes existing precedence: objects that implement __json__ and also match a registered type will no longer use their __json__ reducer. Consider keeping the __json__ check first (as before) and only prioritizing user encoders over default encoders.
| def _register_default_types(): | ||
| # NOTE: datetime should be registered before date, | ||
| # because datetime is also instance of date. | ||
| register_type(datetime, "datetime", datetime.isoformat, | ||
| datetime.fromisoformat) | ||
| register_type( | ||
| _register_type(datetime, "datetime", datetime.isoformat, | ||
| datetime.fromisoformat) | ||
| _register_type( | ||
| date, | ||
| "date", | ||
| lambda o: o.isoformat(), | ||
| lambda o: datetime.fromisoformat(o).date(), | ||
| lambda o: datetime.fromisoformat(o).date() | ||
| ) | ||
| register_type(time, "time", lambda o: o.isoformat(), time.fromisoformat) | ||
| register_type(Decimal, "decimal", str, Decimal) | ||
| register_type( | ||
| _register_type(time, "time", lambda o: o.isoformat(), time.fromisoformat) | ||
| _register_type(Decimal, "decimal", str, Decimal) | ||
| _register_type( |
There was a problem hiding this comment.
_register_default_types() no longer resets user overrides registered via register_type(). In the test suite, the autouse fixture calls _register_default_types() to reset state between tests, but user-registered encoders/decoders will now persist and can make tests order-dependent (e.g. after overriding the UUID handler). Consider clearing _encoders/_decoders as part of the test reset, or adding an explicit reset helper that restores a clean default state.
| def test_register_type_takes_priority(self): | ||
| class MyDecimal(Decimal): | ||
| pass | ||
|
|
||
| register_type(MyDecimal, "mydecimal", str, MyDecimal) | ||
| original = {'md': MyDecimal('3314132.13363235235324234123213213214134')} | ||
| serialized_str = dumps(original) | ||
| # Ensure our custom marker is used instead of the default Decimal handler | ||
| assert '"mydecimal"' in serialized_str | ||
| loaded_value = loads(serialized_str) | ||
| # Ensure the decoded value is of the registered subclass, not just equal | ||
| assert isinstance(loaded_value['md'], MyDecimal) | ||
| assert original == loaded_value |
There was a problem hiding this comment.
This test registers a new custom type via register_type(), which mutates module-level global registries. With the new split between user/default registries, the existing _register_default_types() fixture no longer clears user registrations, so this (and other register_type tests) can leak state into subsequent tests and make the suite order-dependent. Consider resetting the user registries in the autouse fixture (or adding teardown in this test) to keep tests isolated.
User registered types should take priority over default types in Kombu.
Although users can currently override the encoder/decoder for a Kombu registered type by calling
register_type(for example callingregister_type(Decimal, ...)in their own code), this doesn't work well when it comes to subclassing. If users currently need to pass subclasses of any Celery registered types (datetime, date, time, Decimal or UUID) they would be forced to either:_encodersdictionary in the Kombu json module, pop the value for the superclass, add their subclass, and then re-add the superclass.By separating user registered types and Kombu registered types into separate dictionaries, we can always give priority to user registered types instead, which simplifies this process for users (although technically it is a breaking change from existing behaviour).
Solves #1895