Skip to content

Commit

Permalink
Merge pull request #597 from gpetretto/devel
Browse files Browse the repository at this point in the history
improve error handling in pydantic validation
  • Loading branch information
shyuep committed Jan 29, 2024
2 parents a88cd0c + a43a19c commit cb1ac52
Show file tree
Hide file tree
Showing 2 changed files with 58 additions and 6 deletions.
18 changes: 13 additions & 5 deletions monty/json.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import json
import os
import pathlib
import traceback
import types
from collections import OrderedDict, defaultdict
from enum import Enum
Expand Down Expand Up @@ -257,12 +258,19 @@ def _validate_monty(cls, __input_value):
if isinstance(__input_value, cls):
return __input_value
if isinstance(__input_value, dict):
new_obj = MontyDecoder().process_decoded(__input_value)
if isinstance(new_obj, cls):
# Do not allow generic exceptions to be raised during deserialization
# since pydantic may handle them incorrectly.
try:
new_obj = MontyDecoder().process_decoded(__input_value)
if isinstance(new_obj, cls):
return new_obj
new_obj = cls(**__input_value)
return new_obj

new_obj = cls(**__input_value)
return new_obj
except Exception:
raise ValueError(
f"Error while deserializing {cls.__name__} "
f"object: {traceback.format_exc()}"
)

raise ValueError(
f"Must provide {cls.__name__}, the as_dict form, or the proper"
Expand Down
46 changes: 45 additions & 1 deletion tests/test_json.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,16 @@ def __eq__(self, other):
)


class LimitedMSONClass(MSONable):
"""An MSONable class that only accepts a limited number of options"""

def __init__(self, a):
self.a = a

def __eq__(self, other):
return self.a == other.a


class GoodNestedMSONClass(MSONable):
def __init__(self, a_list, b_dict, c_list_dict_list, **kwargs):
assert isinstance(a_list, list)
Expand Down Expand Up @@ -668,16 +678,22 @@ def test_redirect_settings_file(self):
}

def test_pydantic_integrations(self):
from pydantic import BaseModel
from pydantic import BaseModel, ValidationError

global ModelWithMSONable # allow model to be deserialized in test
global LimitedMSONClass

class ModelWithMSONable(BaseModel):
a: GoodMSONClass

test_object = ModelWithMSONable(a=GoodMSONClass(1, 1, 1))
test_dict_object = ModelWithMSONable(a=test_object.a.as_dict())
assert test_dict_object.a.a == test_object.a.a
dict_no_class = test_object.a.as_dict()
dict_no_class.pop("@class")
dict_no_class.pop("@module")
test_dict_object = ModelWithMSONable(a=dict_no_class)
assert test_dict_object.a.a == test_object.a.a

assert test_object.model_json_schema() == {
"title": "ModelWithMSONable",
Expand Down Expand Up @@ -751,6 +767,34 @@ class ModelWithDict(BaseModel):
assert isinstance(obj.a["x"], GoodMSONClass)
assert obj.a["x"].b == 1

# check that if an MSONable object raises an exception during
# the model validation it is properly handled by pydantic
global ModelWithUnion # allow model to be deserialized in test
global ModelWithLimited # allow model to be deserialized in test

class ModelWithLimited(BaseModel):
a: LimitedMSONClass

class ModelWithUnion(BaseModel):
a: LimitedMSONClass | dict

limited_dict = jsanitize(ModelWithLimited(a=LimitedMSONClass(1)), strict=True)
assert ModelWithLimited.model_validate(limited_dict)
limited_dict["a"]["b"] = 2
with pytest.raises(ValidationError):
ModelWithLimited.model_validate(limited_dict)

limited_union_dict = jsanitize(
ModelWithUnion(a=LimitedMSONClass(1)), strict=True
)
validated_model = ModelWithUnion.model_validate(limited_union_dict)
assert isinstance(validated_model, ModelWithUnion)
assert isinstance(validated_model.a, LimitedMSONClass)
limited_union_dict["a"]["b"] = 2
validated_model = ModelWithUnion.model_validate(limited_union_dict)
assert isinstance(validated_model, ModelWithUnion)
assert isinstance(validated_model.a, dict)

def test_dataclass(self):
c = Coordinates([Point(1, 2), Point(3, 4)])
d = c.as_dict()
Expand Down

0 comments on commit cb1ac52

Please sign in to comment.