#34539: `get_prep_value` no longer called for JSONField
-------------------------------------+-------------------------------------
Reporter: Julie Rymer | Owner: nobody
Type: Bug | Status: new
Component: Database layer | Version: 4.2
(models, ORM) |
Severity: Release blocker | Resolution:
Keywords: | Triage Stage: Accepted
Has patch: 0 | Needs documentation: 0
Needs tests: 0 | Patch needs improvement: 0
Easy pickings: 0 | UI/UX: 0
-------------------------------------+-------------------------------------
Comment (by Julie Rymer):
My usecase is the following:
I have a db json field that I use to keep data in the following format
`[{id: str, label: str, length: Decimal, height: Decimal, width: Decimal,
weight: Decimal}]`
For easier manipulation and validation, I have a dataclass containing
those properties. I created a custom JSONField to convert to/from a list
of that dataclass to a simple python dict to be inserted in db.
From the [https://docs.djangoproject.com/en/4.2/howto/custom-model-fields
/#converting-values-to-python-objects doc]
> If your custom Field class deals with data structures that are more
complex than strings, dates, integers, or floats, then you may need to
override from_db_value() and to_python().
And so I overrode `from_db_value()` and `to_python()`
Further along the [https://docs.djangoproject.com/en/4.2/howto/custom-
model-fields/#converting-python-objects-to-query-values doc]
> Since using a database requires conversion in both ways, if you override
from_db_value() you also have to override get_prep_value() to convert
Python objects back to query values.
And so I overrode `get_prep_value()`. But `get_prep_value()` is never
called, alas. This behaviour is not consistent from what is expected.
I know that this doc is about `Field.get_prep_value()` and not
`JSONField.get_prep_value()`, but unless I missed something, there is no
specific documentation for `JSONField.get_prep_value()`, nor forewarning
in `Field.get_prep_value()`, so the obvious guess is that this doc apply
to `JSONField.get_prep_value()` as well as any other Field class.
I think the question to solve is
- do we want to add a warning that the documented rules for customisation
does not apply to some Field classes and to document the missing rules, or
- should we harmonize instead all Field classes that don't follow the
rules yet and make their behaviour consistent with current documentation.
I am more in favour of the second option since it does seems to be less of
a change effort and more practical for the end-user.
You can find my custom Field implementation here:
{{{
from __future__ import annotations
import decimal
import json
from dataclasses import dataclass, fields
from decimal import Decimal
from django.core import exceptions
from django.db import models
from django.utils.translation import gettext_lazy as _
@dataclass(repr=True, eq=True, unsafe_hash=True)
class ProductPackage:
id: str
length: Decimal # in cm
height: Decimal # in cm
width: Decimal # in cm
weight: Decimal # in kg
label: str = ""
@staticmethod
def from_dict(package_json: dict) -> ProductPackage:
return ProductPackage(
id=package_json.get("id"),
length=Decimal(package_json["length"]),
height=Decimal(package_json["height"]),
width=Decimal(package_json["width"]),
weight=Decimal(package_json["weight"]),
label=package_json.get("label", ""),
)
def to_dict(self) -> dict:
return dict((field.name, getattr(self, field.name)) for field in
fields(self))
@classmethod
def from_list(cls, packages_json: list[dict]) -> list:
return list(
cls.from_dict(package_json) for package_json in (packages_json
or [])
)
def __copy__(self):
return self.copy()
def copy(self):
return self.__class__(**self.to_dict())
class PackageJSONField(models.JSONField):
"""
ProductPackage class will make package dimension more practical to use
and this class also handle Decimal so they can be
serialized/deserialized from JSON
"""
description = "A JSON field to store a list of ProductPackage"
default_error_messages = {
**models.JSONField.default_error_messages,
"invalid_packages": _("Value must be valid list of packages."),
"invalid_decimal": _("Package dimensions must be valid decimal
strings."),
}
def from_db_value(self, value, expression, connection) ->
list[ProductPackage]:
packages_json = super().from_db_value(value, expression,
connection)
return ProductPackage.from_list(packages_json)
def to_python(self, value):
if value is None:
return value
if isinstance(value, ProductPackage):
return value
try:
if isinstance(value, list):
return ProductPackage.from_list(value)
return json.loads(value, cls=self.decoder)
except json.JSONDecodeError:
raise exceptions.ValidationError(
self.error_messages["invalid"],
code="invalid",
params={"value": value},
)
except TypeError:
raise exceptions.ValidationError(
self.error_messages["invalid_packages"],
code="invalid_packages",
params={"value": value},
)
except decimal.InvalidOperation:
raise exceptions.ValidationError(
self.error_messages["invalid_decimal"],
code="invalid_decimal",
params={"value": value},
)
def get_prep_value(self, value: list[ProductPackage | dict]):
if value is None:
return value
if any(isinstance(package, dict) for package in value):
value = ProductPackage.from_list(value)
value = list(
{
"id": package.id,
"label": package.label,
"length": str(package.length),
"height": str(package.height),
"width": str(package.width),
"weight": str(package.weight),
}
for package in value
)
return super().get_prep_value(value)
def get_db_prep_value(self, value, connection, prepared=False):
# the override I ended up doing for the current issue
if isinstance(value, models.expressions.Value) and isinstance(
value.output_field, models.JSONField
):
value = value.value
elif hasattr(value, "as_sql"):
return value
return connection.ops.adapt_json_value(self.get_prep_value(value),
self.encoder)
}}}
--
Ticket URL: <https://code.djangoproject.com/ticket/34539#comment:9>
Django <https://code.djangoproject.com/>
The Web framework for perfectionists with deadlines.
--
You received this message because you are subscribed to the Google Groups
"Django updates" group.
To unsubscribe from this group and stop receiving emails from it, send an email
to [email protected].
To view this discussion on the web visit
https://groups.google.com/d/msgid/django-updates/01070187e67a30d2-9d95a9e7-eeb8-48c1-b409-8f971f8bdb75-000000%40eu-central-1.amazonses.com.