diff --git a/base_rest_demo/__manifest__.py b/base_rest_demo/__manifest__.py index e51c6bbb1..13eeeb838 100644 --- a/base_rest_demo/__manifest__.py +++ b/base_rest_demo/__manifest__.py @@ -20,7 +20,7 @@ "pydantic", ], "external_dependencies": { - "python": ["jsondiff", "extendable-pydantic", "marshmallow", "pydantic"] + "python": ["jsondiff", "extendable-pydantic>=1.0.0", "marshmallow", "pydantic>=2.0.0"] }, "installable": True, } diff --git a/base_rest_pydantic/__manifest__.py b/base_rest_pydantic/__manifest__.py index ea83d15c9..5ad986429 100644 --- a/base_rest_pydantic/__manifest__.py +++ b/base_rest_pydantic/__manifest__.py @@ -13,7 +13,7 @@ "installable": True, "external_dependencies": { "python": [ - "pydantic", + "pydantic>=2.0.0", ] }, } diff --git a/base_rest_pydantic/restapi.py b/base_rest_pydantic/restapi.py index 1966ea938..25cdd4909 100644 --- a/base_rest_pydantic/restapi.py +++ b/base_rest_pydantic/restapi.py @@ -1,23 +1,12 @@ # Copyright 2021 ACSONE SA/NV # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). - +import json from odoo import _ from odoo.exceptions import UserError from odoo.addons.base_rest import restapi from pydantic import BaseModel, ValidationError -from pydantic.version import VERSION as PYDANTIC_VERSION - -PYDANTIC_V2 = PYDANTIC_VERSION.startswith("2.") - -if PYDANTIC_V2: - - def validate_model(cls, data): - return cls.model_validate_json(data) - -else: - from pydantic import validate_model def replace_ref_in_schema(item, original_schema): @@ -55,15 +44,20 @@ def from_params(self, service, params): def to_response(self, service, result): # do we really need to validate the instance???? - json_dict = result.dict() - if PYDANTIC_V2: - orm_mode = result.model_config.get("from_attributes", None) - else: - orm_mode = result.__config__.orm_mode - to_validate = json_dict if orm_mode else result.dict(by_alias=True) - *_, validation_error = validate_model(self._model_cls, to_validate) - if validation_error: - raise SystemError(_("Invalid Response %s") % validation_error) + json_dict = result.model_dump() + orm_mode = result.model_config.get("from_attributes", None) + to_validate = json_dict if orm_mode else result.model_dump(by_alias=True) + # Ensure that to_validate is under json format + try: + json.loads(to_validate) + to_validate_jsonified = to_validate + except TypeError: + to_validate_jsonified = json.dumps(to_validate) + + try: + self._model_cls.model_validate_json(to_validate_jsonified) + except ValidationError as validation_error: + raise SystemError(_("Invalid Response")) from validation_error return json_dict def to_openapi_query_parameters(self, servic, spec): diff --git a/base_rest_pydantic/tests/test_from_params.py b/base_rest_pydantic/tests/test_from_params.py index b376dde72..54e4074e9 100644 --- a/base_rest_pydantic/tests/test_from_params.py +++ b/base_rest_pydantic/tests/test_from_params.py @@ -16,7 +16,7 @@ def setUp(self): super(TestPydantic, self).setUp() class Model1(BaseModel): - name: str = None + name: str description: str = None self.Model1: BaseModel = Model1 @@ -33,7 +33,10 @@ def _from_params(self, pydantic_cls: Type[BaseModel], params: dict, **kwargs): return restapi_pydantic.from_params(mock_service, params) def test_from_params(self): - params = {"name": "Instance Name", "description": "Instance Description"} + params = { + "name": "Instance Name", + "description": "Instance Description", + } instance = self._from_params(self.Model1, params) self.assertEqual(instance.name, params["name"]) self.assertEqual(instance.description, params["description"]) @@ -45,14 +48,20 @@ def test_from_params_missing_optional_field(self): self.assertIsNone(instance.description) def test_from_params_missing_required_field(self): - msg = r"value_error.missing" + msg = r"Field required" with self.assertRaisesRegex(UserError, msg): self._from_params(self.Model1, {"description": "Instance Description"}) def test_from_params_pydantic_model_list(self): params = [ - {"name": "Instance Name", "description": "Instance Description"}, - {"name": "Instance Name 2", "description": "Instance Description 2"}, + { + "name": "Instance Name", + "description": "Instance Description", + }, + { + "name": "Instance Name 2", + "description": "Instance Description 2", + }, ] instances = self._from_params(self.Model1, params) self.assertEqual(len(instances), 2) diff --git a/base_rest_pydantic/tests/test_response.py b/base_rest_pydantic/tests/test_response.py index fd8e7fd12..b1c5c40f8 100644 --- a/base_rest_pydantic/tests/test_response.py +++ b/base_rest_pydantic/tests/test_response.py @@ -31,11 +31,20 @@ class Model1(BaseModel): res = self._to_response(instance) self.assertEqual(res["name"], instance.name) + def test_to_response_validation_failed(self): + class Model1(BaseModel): + name: str = None + + instance = Model1(named="Instance 1") + msg = r"Invalid Response" + with self.assertRaisesRegex(SystemError, msg): + self._to_response(instance) + def test_to_response_list(self): class Model1(BaseModel): name: str = None - instances = (Model1(name="Instance 1"), Model1(name="Instance 2")) + instances = [Model1(name="Instance 1"), Model1(name="Instance 2")] res = self._to_response_list(instances) self.assertEqual(len(res), 2) self.assertSetEqual({r["name"] for r in res}, {"Instance 1", "Instance 2"})