Skip to content

Commit 98b32e9

Browse files
committed
Final fix
Pydantic v2 copies the class-level attribute into the core schema as the field default. Because ``setattr(new_class, field_name, ExpressionProxy)`` runs *after* ``super().__new__()``, the core schema already contains the parent class's ExpressionProxy as the default for ``pk`` (and any other inherited field). We must rebuild the schema so that Pydantic reads the newly-set class attribute instead of the stale inherited one. Additionally, for ``pk`` specifically, we need to clear the ``ExpressionProxy`` from the ``FieldInfo`` default so that ``model_validate`` and ``model_validate_json`` don't try to validate it as a string.
1 parent a11d8c2 commit 98b32e9

3 files changed

Lines changed: 40 additions & 3 deletions

File tree

‎aredis_om/model/model.py‎

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1974,8 +1974,21 @@ def __new__(cls, name, bases, attrs, **kwargs): # noqa C901
19741974
setattr(new_class, score_attr, None)
19751975
new_class.__annotations__[score_attr] = Union[float, None]
19761976

1977-
if getattr(new_class._meta, "embedded", False):
1978-
new_class.model_rebuild(force=True)
1977+
# Pydantic v2 copies the class-level attribute into the core schema as
1978+
# the field default. Because ``setattr(new_class, field_name, ExpressionProxy)``
1979+
# runs *after* ``super().__new__()``, the core schema already contains the
1980+
# parent class's ExpressionProxy as the default for ``pk`` (and any other
1981+
# inherited field). We must rebuild the schema so that Pydantic reads the
1982+
# newly-set class attribute instead of the stale inherited one.
1983+
#
1984+
# Additionally, for ``pk`` specifically, we need to clear the
1985+
# ``ExpressionProxy`` from the ``FieldInfo`` default so that
1986+
# ``model_validate`` and ``model_validate_json`` don't try to validate
1987+
# it as a string.
1988+
pk_field = new_class.model_fields.get("pk")
1989+
if pk_field is not None and getattr(pk_field, "default", None) is not None:
1990+
pk_field.default = None
1991+
new_class.model_rebuild(force=True)
19791992

19801993
# If this is an embedded model, we don't want to allow primary keys at all,
19811994
if getattr(new_class._meta, "embedded", False):

‎pyproject.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "pyredis-om"
3-
version = "0.6.4"
3+
version = "0.6.5"
44
description = "A drop-in replacement for `redis-om`, built out of frustration."
55
authors = [
66
{ name = "Redis OSS", email = "oss@redis.com" },

‎tests/test_pydantic_compat.py‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,30 @@ class RedisUser(JsonModel):
8080
assert user_dict["operations"]["likes"][0]["pk"] == "alice:bob"
8181

8282

83+
def test_json_model_validate_missing_pk():
84+
"""model_validate on JsonModel must not crash when pk is omitted."""
85+
86+
class User(JsonModel):
87+
name: str
88+
89+
user = User.model_validate({"name": "Ada"})
90+
assert user.name == "Ada"
91+
assert user.pk is not None
92+
assert isinstance(user.pk, str)
93+
94+
95+
def test_json_model_validate_json_missing_pk():
96+
"""model_validate_json on JsonModel must not crash when pk is omitted."""
97+
98+
class User(JsonModel):
99+
name: str
100+
101+
user = User.model_validate_json('{"name": "Ada"}')
102+
assert user.name == "Ada"
103+
assert user.pk is not None
104+
assert isinstance(user.pk, str)
105+
106+
83107
def test_field_validator_on_hashmodel():
84108
class TaggedItem(HashModel):
85109
name: str

0 commit comments

Comments
 (0)