Skip to content

Commit

Permalink
Fix json validation of complex types in strict models
Browse files Browse the repository at this point in the history
  • Loading branch information
JacobHayes committed Feb 25, 2024
1 parent 6b56235 commit 6b4c652
Show file tree
Hide file tree
Showing 3 changed files with 113 additions and 14 deletions.
69 changes: 57 additions & 12 deletions sqlmodel/_compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -283,14 +283,12 @@ def sqlmodel_table_construct(
# End SQLModel override
return self_instance

def sqlmodel_validate(
def _sqlmodel_validate(
cls: Type[_TSQLModel],
obj: Any,
*,
strict: Union[bool, None] = None,
from_attributes: Union[bool, None] = None,
context: Union[Dict[str, Any], None] = None,
update: Union[Dict[str, Any], None] = None,
validator: Callable[[Any, _TSQLModel], None],
) -> _TSQLModel:
if not is_table_model_class(cls):
new_obj: _TSQLModel = cls.__new__(cls)
Expand All @@ -308,13 +306,7 @@ def sqlmodel_validate(
use_obj = {**obj, **update}
elif update:
use_obj = ObjectWithUpdateWrapper(obj=obj, update=update)
cls.__pydantic_validator__.validate_python(
use_obj,
strict=strict,
from_attributes=from_attributes,
context=context,
self_instance=new_obj,
)
validator(use_obj, new_obj)
# Capture fields set to restore it later
fields_set = new_obj.__pydantic_fields_set__.copy()
if not is_table_model_class(cls):
Expand All @@ -335,6 +327,46 @@ def sqlmodel_validate(
setattr(new_obj, key, value)
return new_obj

def sqlmodel_validate_python(
cls: Type[_TSQLModel],
obj: Any,
*,
strict: Union[bool, None] = None,
from_attributes: Union[bool, None] = None,
context: Union[Dict[str, Any], None] = None,
update: Union[Dict[str, Any], None] = None,
) -> _TSQLModel:
def validate(use_obj: Any, new_obj: _TSQLModel) -> None:
cls.__pydantic_validator__.validate_python(
use_obj,
strict=strict,
from_attributes=from_attributes,
context=context,
self_instance=new_obj,
)

return _sqlmodel_validate(cls, obj, update=update, validator=validate)

def sqlmodel_validate_json(
cls: Type[_TSQLModel],
json_data: Union[str, bytes, bytearray],
*,
strict: Union[bool, None] = None,
context: Union[Dict[str, Any], None] = None,
update: Union[Dict[str, Any], None] = None,
) -> _TSQLModel:
def validate(use_obj: Any, new_obj: _TSQLModel) -> None:
cls.__pydantic_validator__.validate_json(
use_obj,
strict=strict,
context=context,
self_instance=new_obj,
)

return _sqlmodel_validate(
cls=cls, obj=json_data, update=update, validator=validate
)

def sqlmodel_init(*, self: "SQLModel", data: Dict[str, Any]) -> None:
old_dict = self.__dict__.copy()
if not is_table_model_class(self.__class__):
Expand Down Expand Up @@ -496,7 +528,7 @@ def _calculate_keys(

return keys

def sqlmodel_validate(
def sqlmodel_validate_python(
cls: Type[_TSQLModel],
obj: Any,
*,
Expand Down Expand Up @@ -542,6 +574,19 @@ def sqlmodel_validate(
m._init_private_attributes() # type: ignore[attr-defined] # noqa
return m

def sqlmodel_validate_json(
cls: Type[_TSQLModel],
json_data: Union[str, bytes, bytearray],
*,
strict: Union[bool, None] = None,
context: Union[Dict[str, Any], None] = None,
update: Union[Dict[str, Any], None] = None,
) -> _TSQLModel:
# We're not doing any real json validation for pydantic v1.
return sqlmodel_validate_python(
cls=cls, obj=json_data, strict=strict, context=context, update=update
)

def sqlmodel_init(*, self: "SQLModel", data: Dict[str, Any]) -> None:
values, fields_set, validation_error = validate_model(self.__class__, data)
# Only raise errors if not a SQLModel model
Expand Down
22 changes: 20 additions & 2 deletions sqlmodel/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,8 @@
post_init_field_info,
set_config_value,
sqlmodel_init,
sqlmodel_validate,
sqlmodel_validate_json,
sqlmodel_validate_python,
)
from .sql.sqltypes import GUID, AutoString

Expand Down Expand Up @@ -749,7 +750,7 @@ def model_validate(
context: Union[Dict[str, Any], None] = None,
update: Union[Dict[str, Any], None] = None,
) -> _TSQLModel:
return sqlmodel_validate(
return sqlmodel_validate_python(
cls=cls,
obj=obj,
strict=strict,
Expand All @@ -758,6 +759,23 @@ def model_validate(
update=update,
)

@classmethod
def model_validate_json(
cls: Type[_TSQLModel],
json_data: Union[str, bytes, bytearray],
*,
strict: Union[bool, None] = None,
context: Union[Dict[str, Any], None] = None,
update: Union[Dict[str, Any], None] = None,
) -> _TSQLModel:
return sqlmodel_validate_json(
cls=cls,
json_data=json_data,
strict=strict,
context=context,
update=update,
)

def model_dump(
self,
*,
Expand Down
36 changes: 36 additions & 0 deletions tests/test_validation.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import json
from datetime import date
from typing import Optional
from uuid import UUID

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

with pytest.raises(ValidationError):
Hero.model_validate({"name": None, "age": 25})


@needs_pydanticv2
def test_validation_strict_mode(clear_sqlmodel):
"""Test validation of fields in strict mode from python and json."""

class Hero(SQLModel):
id: Optional[int] = None
birth_date: Optional[date] = None
uuid: Optional[UUID] = None

model_config = {"strict": True}

date_obj = date(1970, 1, 1)
date_str = date_obj.isoformat()
uuid_obj = UUID("0ffef15c-c04f-4e61-b586-904ffe76c9b1")
uuid_str = str(uuid_obj)

Hero.model_validate({"id": 1, "birth_date": date_obj, "uuid": uuid_obj})
# Check that python validation requires strict types
with pytest.raises(ValidationError):
Hero.model_validate({"id": "1"})
with pytest.raises(ValidationError):
Hero.model_validate({"birth_date": date_str})
with pytest.raises(ValidationError):
Hero.model_validate({"uuid": uuid_str})

# Check that json is a bit more lax, but still refuses to "cast" values when not necessary
Hero.model_validate_json(
json.dumps({"id": 1, "birth_date": date_str, "uuid": uuid_str})
)
with pytest.raises(ValidationError):
Hero.model_validate_json(json.dumps({"id": "1"}))

0 comments on commit 6b4c652

Please sign in to comment.