Skip to content

Give user registered types priority when encoding / decoding JSON#2188

Open
soceanainn wants to merge 8 commits intocelery:mainfrom
soceanainn:seamus/give-custom-json-encoders-priority
Open

Give user registered types priority when encoding / decoding JSON#2188
soceanainn wants to merge 8 commits intocelery:mainfrom
soceanainn:seamus/give-custom-json-encoders-priority

Conversation

@soceanainn
Copy link
Copy Markdown

@soceanainn soceanainn commented Nov 7, 2024

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 calling register_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:

  1. Override implementation of superclass with a subclass aware encoder / decoder implementation, or
  2. Access the 'protected' _encoders dictionary 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

@codecov
Copy link
Copy Markdown

codecov Bot commented Nov 7, 2024

Codecov Report

❌ Patch coverage is 95.65217% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 82.46%. Comparing base (33d4eba) to head (ccbc8df).
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
kombu/utils/json.py 95.65% 0 Missing and 1 partial ⚠️
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.
📢 Have feedback on the report? Share it here.

@soceanainn soceanainn force-pushed the seamus/give-custom-json-encoders-priority branch from 52b2f86 to 247cf3e Compare November 7, 2024 14:45
Copy link
Copy Markdown
Member

@auvipy auvipy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please fix the lint errors as well

@soceanainn soceanainn force-pushed the seamus/give-custom-json-encoders-priority branch 2 times, most recently from e9cfe80 to 21430db Compare November 18, 2024 21:41
@soceanainn soceanainn requested a review from auvipy November 18, 2024 21:42
Copy link
Copy Markdown
Member

@Nusnus Nusnus left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@Nusnus Nusnus force-pushed the seamus/give-custom-json-encoders-priority branch from 21430db to b3d4bb7 Compare December 1, 2024 23:28
@Nusnus Nusnus force-pushed the seamus/give-custom-json-encoders-priority branch from b3d4bb7 to 7a20cbd Compare December 26, 2024 22:42
@auvipy
Copy link
Copy Markdown
Member

auvipy commented Dec 28, 2024

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.

technically the change is a breaking change

@auvipy
Copy link
Copy Markdown
Member

auvipy commented Dec 29, 2024

we have to release in in a new major version like 5.6/5.7

@auvipy auvipy added this to the 5.7.0 milestone May 12, 2025
@auvipy auvipy requested a review from Copilot February 12, 2026 07:33
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 ValueError raised for unknown __type__ currently includes the builtin type object, not the marker from the payload. This makes the error misleading; use o["__type__"] (and possibly include available markers) instead of type.
        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.

Comment thread kombu/utils/json.py
Comment on lines 149 to +154
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(
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_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.

Copilot uses AI. Check for mistakes.
Comment thread kombu/utils/json.py
Comment on lines 25 to +31
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))
)

Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@soceanainn please cross check this and other suggestions

Comment thread t/unit/utils/test_json.py Outdated
Copy link
Copy Markdown
Member

@auvipy auvipy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you please revisit this? some review comments need your attention

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread t/unit/utils/test_json.py
Comment on lines +104 to +107
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)
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment thread kombu/utils/json.py
Comment on lines 25 to 34
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()
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment thread kombu/utils/json.py
Comment on lines 149 to +162
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(
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_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.

Copilot uses AI. Check for mistakes.
Comment thread t/unit/utils/test_json.py
Comment on lines +98 to +110
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
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
@celery celery deleted a comment from thedrow Apr 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants