Opened 23 months ago
Closed 23 months ago
#35071 closed New feature (invalid)
Resolve lazy objects inside dictionaries when saving JSONFields
| Reported by: | Jacob Walls | Owned by: | Jacob Walls |
|---|---|---|---|
| Component: | Database layer (models, ORM) | Version: | dev |
| Severity: | Normal | Keywords: | |
| Cc: | Triage Stage: | Unreviewed | |
| Has patch: | no | Needs documentation: | no |
| Needs tests: | no | Patch needs improvement: | no |
| Easy pickings: | no | UI/UX: | no |
Description
I have a model Notification like this:
class Notification(models.Model):
message = models.TextField(null=True)
context = models.JSONField(null=True)
Here is the lazy object...
>>> from django.utils.translation import gettext_lazy as _
>>> lazy = _("lazy")
... that I can use in a TextField ...
>>> models.Notification(message=lazy).save() >>>
... but not for the JSONField without string-ifying it myself. Suggesting an implementation that resolves these in one of the methods that prepares values for database insertion. Tested with psycopg2 2.9.9.
>>> models.Notification(context={'lazy': lazy}).save()
Traceback (most recent call last):
File "<console>", line 1, in <module>
File "/Users/jwalls/venv311/lib/python3.11/site-packages/django/db/models/base.py", line 814, in save
self.save_base(
File "/Users/jwalls/venv311/lib/python3.11/site-packages/django/db/models/base.py", line 877, in save_base
updated = self._save_table(
^^^^^^^^^^^^^^^^^
File "/Users/jwalls/venv311/lib/python3.11/site-packages/django/db/models/base.py", line 990, in _save_table
updated = self._do_update(
^^^^^^^^^^^^^^^^
File "/Users/jwalls/venv311/lib/python3.11/site-packages/django/db/models/base.py", line 1054, in _do_update
return filtered._update(values) > 0
^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/jwalls/venv311/lib/python3.11/site-packages/django/db/models/query.py", line 1231, in _update
return query.get_compiler(self.db).execute_sql(CURSOR)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/jwalls/venv311/lib/python3.11/site-packages/django/db/models/sql/compiler.py", line 1982, in execute_sql
cursor = super().execute_sql(result_type)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/jwalls/venv311/lib/python3.11/site-packages/django/db/models/sql/compiler.py", line 1560, in execute_sql
cursor.execute(sql, params)
File "/Users/jwalls/venv311/lib/python3.11/site-packages/django/db/backends/utils.py", line 102, in execute
return super().execute(sql, params)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/jwalls/venv311/lib/python3.11/site-packages/django/db/backends/utils.py", line 67, in execute
return self._execute_with_wrappers(
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/jwalls/venv311/lib/python3.11/site-packages/django/db/backends/utils.py", line 80, in _execute_with_wrappers
return executor(sql, params, many, context)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/jwalls/venv311/lib/python3.11/site-packages/django/db/backends/utils.py", line 89, in _execute
return self.cursor.execute(sql, params)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/jwalls/venv311/lib/python3.11/site-packages/psycopg2/_json.py", line 78, in getquoted
s = self.dumps(self.adapted)
^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/jwalls/venv311/lib/python3.11/site-packages/psycopg2/_json.py", line 72, in dumps
return self._dumps(obj)
^^^^^^^^^^^^^^^^
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/json/__init__.py", line 231, in dumps
return _default_encoder.encode(obj)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/json/encoder.py", line 200, in encode
chunks = self.iterencode(o, _one_shot=True)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/json/encoder.py", line 258, in iterencode
return _iterencode(o, 0)
^^^^^^^^^^^^^^^^^
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/json/encoder.py", line 180, in default
raise TypeError(f'Object of type {o.__class__.__name__} '
TypeError: Object of type __proxy__ is not JSON serializable
Change History (13)
comment:1 by , 23 months ago
| Resolution: | → wontfix |
|---|---|
| Status: | new → closed |
comment:2 by , 23 months ago
I was thinking of something like this:
-
django/db/models/fields/json.py
diff --git a/django/db/models/fields/json.py b/django/db/models/fields/json.py index 7b9b2ae6b2..2df4f8ee12 100644
a b 1 from collections.abc import Container 1 2 import json 2 3 3 4 from django import forms 5 from django.conf import settings 4 6 from django.core import checks, exceptions 5 7 from django.db import NotSupportedError, connections, router 6 8 from django.db.models import expressions, lookups … … from django.db.models.lookups import ( 11 13 PostgresOperatorLookup, 12 14 Transform, 13 15 ) 16 from django.utils.functional import Promise 14 17 from django.utils.translation import gettext_lazy as _ 15 18 16 19 from . import Field … … class JSONField(CheckFieldDefaultMixin, Field): 96 99 def get_internal_type(self): 97 100 return "JSONField" 98 101 102 def get_prep_value(self, value): 103 if settings.USE_I18N: 104 match value: 105 case dict(): 106 value = { 107 self.get_prep_value(k): self.get_prep_value(v) 108 for k, v in value.items() 109 } 110 case str() | Promise(): 111 pass 112 case Container(): 113 value = value.__class__(self.get_prep_value(v) for v in value) 114 115 return super().get_prep_value(value) 116 99 117 def get_db_prep_value(self, value, connection, prepared=False): 100 118 if not prepared: 101 119 value = self.get_prep_value(value)
comment:4 by , 23 months ago
Yeah, I hadn't yet looked into whether lazy translation objects get resolved when USE_I18N = False. Looks like they do, so I should remove the guard.
comment:5 by , 23 months ago
| Resolution: | wontfix |
|---|---|
| Status: | closed → new |
| Triage Stage: | Unreviewed → Accepted |
Tentatively accepted.
comment:6 by , 23 months ago
| Owner: | changed from to |
|---|---|
| Status: | new → assigned |
| Version: | 4.2 → dev |
comment:7 by , 23 months ago
| Summary: | Resolve lazy translation objects inside dictionaries when saving JSONFields → Resolve lazy objects inside dictionaries when saving JSONFields |
|---|
comment:8 by , 23 months ago
So I have something sort of ready to go here, but then I noticed that what I should probably be doing is using DjangoJSONEncoder in the encoder arg of JSONField, which solves my use case. Is that enough? Should that be the default?
comment:9 by , 23 months ago
I guess it would still be nice to separate this concern (resolving the promise) from the choice of serializer.
comment:11 by , 23 months ago
I think using the encoder arg of JSONField is the right answer to this issue. The get_prep_value of your suggested patch would add too much overhead in my opinion.
comment:12 by , 23 months ago
I completely agree with Claude here.
As a matter of fact DjangoJSONEncoder already works perfectly for this use case so I don't understand why we should treat promises differently given we already document using it for such cases.
import json from django.core.serializers.json import DjangoJSONEncoder from django.utils.translation import gettext_lazy as _ json.dumps({"key": _("foobar")}) # TypeError: Object of type __proxy__ is not JSON serializable json.dumps({"key": _("foobar")}, cls=DjangoJSONEncoder) >>> '{"key": "foobar"}'
comment:13 by , 23 months ago
| Has patch: | unset |
|---|---|
| Resolution: | → invalid |
| Status: | assigned → closed |
| Triage Stage: | Accepted → Unreviewed |
Makes sense. Docs links are very helpful. Sorry for not using a support channel to check my understanding first!
It's easier to resolve a type for string-based fields, however,
JSONField's values can contain different types of data. I don't think it's worth adding extra guess-like complexity for this. We can reconsider this decision if you provide a PoC.