Skip to content

Commit 6b4c652

Browse files
committed
Fix json validation of complex types in strict models
1 parent 6b56235 commit 6b4c652

File tree

3 files changed

+113
-14
lines changed

3 files changed

+113
-14
lines changed

sqlmodel/_compat.py

Lines changed: 57 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -283,14 +283,12 @@ def sqlmodel_table_construct(
283283
# End SQLModel override
284284
return self_instance
285285

286-
def sqlmodel_validate(
286+
def _sqlmodel_validate(
287287
cls: Type[_TSQLModel],
288288
obj: Any,
289289
*,
290-
strict: Union[bool, None] = None,
291-
from_attributes: Union[bool, None] = None,
292-
context: Union[Dict[str, Any], None] = None,
293290
update: Union[Dict[str, Any], None] = None,
291+
validator: Callable[[Any, _TSQLModel], None],
294292
) -> _TSQLModel:
295293
if not is_table_model_class(cls):
296294
new_obj: _TSQLModel = cls.__new__(cls)
@@ -308,13 +306,7 @@ def sqlmodel_validate(
308306
use_obj = {**obj, **update}
309307
elif update:
310308
use_obj = ObjectWithUpdateWrapper(obj=obj, update=update)
311-
cls.__pydantic_validator__.validate_python(
312-
use_obj,
313-
strict=strict,
314-
from_attributes=from_attributes,
315-
context=context,
316-
self_instance=new_obj,
317-
)
309+
validator(use_obj, new_obj)
318310
# Capture fields set to restore it later
319311
fields_set = new_obj.__pydantic_fields_set__.copy()
320312
if not is_table_model_class(cls):
@@ -335,6 +327,46 @@ def sqlmodel_validate(
335327
setattr(new_obj, key, value)
336328
return new_obj
337329

330+
def sqlmodel_validate_python(
331+
cls: Type[_TSQLModel],
332+
obj: Any,
333+
*,
334+
strict: Union[bool, None] = None,
335+
from_attributes: Union[bool, None] = None,
336+
context: Union[Dict[str, Any], None] = None,
337+
update: Union[Dict[str, Any], None] = None,
338+
) -> _TSQLModel:
339+
def validate(use_obj: Any, new_obj: _TSQLModel) -> None:
340+
cls.__pydantic_validator__.validate_python(
341+
use_obj,
342+
strict=strict,
343+
from_attributes=from_attributes,
344+
context=context,
345+
self_instance=new_obj,
346+
)
347+
348+
return _sqlmodel_validate(cls, obj, update=update, validator=validate)
349+
350+
def sqlmodel_validate_json(
351+
cls: Type[_TSQLModel],
352+
json_data: Union[str, bytes, bytearray],
353+
*,
354+
strict: Union[bool, None] = None,
355+
context: Union[Dict[str, Any], None] = None,
356+
update: Union[Dict[str, Any], None] = None,
357+
) -> _TSQLModel:
358+
def validate(use_obj: Any, new_obj: _TSQLModel) -> None:
359+
cls.__pydantic_validator__.validate_json(
360+
use_obj,
361+
strict=strict,
362+
context=context,
363+
self_instance=new_obj,
364+
)
365+
366+
return _sqlmodel_validate(
367+
cls=cls, obj=json_data, update=update, validator=validate
368+
)
369+
338370
def sqlmodel_init(*, self: "SQLModel", data: Dict[str, Any]) -> None:
339371
old_dict = self.__dict__.copy()
340372
if not is_table_model_class(self.__class__):
@@ -496,7 +528,7 @@ def _calculate_keys(
496528

497529
return keys
498530

499-
def sqlmodel_validate(
531+
def sqlmodel_validate_python(
500532
cls: Type[_TSQLModel],
501533
obj: Any,
502534
*,
@@ -542,6 +574,19 @@ def sqlmodel_validate(
542574
m._init_private_attributes() # type: ignore[attr-defined] # noqa
543575
return m
544576

577+
def sqlmodel_validate_json(
578+
cls: Type[_TSQLModel],
579+
json_data: Union[str, bytes, bytearray],
580+
*,
581+
strict: Union[bool, None] = None,
582+
context: Union[Dict[str, Any], None] = None,
583+
update: Union[Dict[str, Any], None] = None,
584+
) -> _TSQLModel:
585+
# We're not doing any real json validation for pydantic v1.
586+
return sqlmodel_validate_python(
587+
cls=cls, obj=json_data, strict=strict, context=context, update=update
588+
)
589+
545590
def sqlmodel_init(*, self: "SQLModel", data: Dict[str, Any]) -> None:
546591
values, fields_set, validation_error = validate_model(self.__class__, data)
547592
# Only raise errors if not a SQLModel model

sqlmodel/main.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,8 @@
7676
post_init_field_info,
7777
set_config_value,
7878
sqlmodel_init,
79-
sqlmodel_validate,
79+
sqlmodel_validate_json,
80+
sqlmodel_validate_python,
8081
)
8182
from .sql.sqltypes import GUID, AutoString
8283

@@ -749,7 +750,7 @@ def model_validate(
749750
context: Union[Dict[str, Any], None] = None,
750751
update: Union[Dict[str, Any], None] = None,
751752
) -> _TSQLModel:
752-
return sqlmodel_validate(
753+
return sqlmodel_validate_python(
753754
cls=cls,
754755
obj=obj,
755756
strict=strict,
@@ -758,6 +759,23 @@ def model_validate(
758759
update=update,
759760
)
760761

762+
@classmethod
763+
def model_validate_json(
764+
cls: Type[_TSQLModel],
765+
json_data: Union[str, bytes, bytearray],
766+
*,
767+
strict: Union[bool, None] = None,
768+
context: Union[Dict[str, Any], None] = None,
769+
update: Union[Dict[str, Any], None] = None,
770+
) -> _TSQLModel:
771+
return sqlmodel_validate_json(
772+
cls=cls,
773+
json_data=json_data,
774+
strict=strict,
775+
context=context,
776+
update=update,
777+
)
778+
761779
def model_dump(
762780
self,
763781
*,

tests/test_validation.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1+
import json
2+
from datetime import date
13
from typing import Optional
4+
from uuid import UUID
25

36
import pytest
47
from pydantic.error_wrappers import ValidationError
@@ -63,3 +66,36 @@ def reject_none(cls, v):
6366

6467
with pytest.raises(ValidationError):
6568
Hero.model_validate({"name": None, "age": 25})
69+
70+
71+
@needs_pydanticv2
72+
def test_validation_strict_mode(clear_sqlmodel):
73+
"""Test validation of fields in strict mode from python and json."""
74+
75+
class Hero(SQLModel):
76+
id: Optional[int] = None
77+
birth_date: Optional[date] = None
78+
uuid: Optional[UUID] = None
79+
80+
model_config = {"strict": True}
81+
82+
date_obj = date(1970, 1, 1)
83+
date_str = date_obj.isoformat()
84+
uuid_obj = UUID("0ffef15c-c04f-4e61-b586-904ffe76c9b1")
85+
uuid_str = str(uuid_obj)
86+
87+
Hero.model_validate({"id": 1, "birth_date": date_obj, "uuid": uuid_obj})
88+
# Check that python validation requires strict types
89+
with pytest.raises(ValidationError):
90+
Hero.model_validate({"id": "1"})
91+
with pytest.raises(ValidationError):
92+
Hero.model_validate({"birth_date": date_str})
93+
with pytest.raises(ValidationError):
94+
Hero.model_validate({"uuid": uuid_str})
95+
96+
# Check that json is a bit more lax, but still refuses to "cast" values when not necessary
97+
Hero.model_validate_json(
98+
json.dumps({"id": 1, "birth_date": date_str, "uuid": uuid_str})
99+
)
100+
with pytest.raises(ValidationError):
101+
Hero.model_validate_json(json.dumps({"id": "1"}))

0 commit comments

Comments
 (0)