diff --git a/extendable_fastapi/__init__.py b/extendable_fastapi/__init__.py
index 08121b91b..28a255de7 100644
--- a/extendable_fastapi/__init__.py
+++ b/extendable_fastapi/__init__.py
@@ -1 +1,2 @@
from . import fastapi_dispatcher
+from .schemas import StrictExtendableBaseModel
diff --git a/extendable_fastapi/__manifest__.py b/extendable_fastapi/__manifest__.py
index 04421f9d7..374394ada 100644
--- a/extendable_fastapi/__manifest__.py
+++ b/extendable_fastapi/__manifest__.py
@@ -15,7 +15,7 @@
"demo": [],
"external_dependencies": {
"python": [
- "extendable-pydantic>=1.0.0",
+ "extendable-pydantic>=1.1.0",
],
},
"installable": True,
diff --git a/extendable_fastapi/schemas.py b/extendable_fastapi/schemas.py
new file mode 100644
index 000000000..797f065a0
--- /dev/null
+++ b/extendable_fastapi/schemas.py
@@ -0,0 +1,29 @@
+from extendable_pydantic import ExtendableBaseModel
+
+
+class StrictExtendableBaseModel(
+ ExtendableBaseModel,
+ revalidate_instances="always",
+ validate_assignment=True,
+ extra="forbid",
+ strict=True,
+):
+ """
+ An ExtendableBaseModel with strict validation.
+
+ By default, Pydantic does not revalidate instances during validation, nor
+ when the data is changed. Validation only occurs when the model is created.
+ This is not suitable for a REST API, where the data is changed after the
+ model is created or the model is created from a partial set of data and
+ then updated with more data. This class enforce strict validation by
+ forcing the revalidation of instances when the method `model_validate` is
+ called and by ensuring that the values assignment is validated.
+
+ The following parameters are added:
+ * revalidate_instances="always": model instances are revalidated during validation
+ (default is "never")
+ * validate_assignment=True: revalidate the model when the data is changed (default is False)
+ * extra="forbid": Forbid any extra attributes (default is "ignore")
+ * strict=True: raise an error if a value's type does not match the field's type
+ annotation (default is False; Pydantic attempts to coerce values to the correct type)
+ """
diff --git a/extendable_fastapi/tests/__init__.py b/extendable_fastapi/tests/__init__.py
index e69de29bb..11499bebb 100644
--- a/extendable_fastapi/tests/__init__.py
+++ b/extendable_fastapi/tests/__init__.py
@@ -0,0 +1 @@
+from . import test_strict_extendable_base_model
diff --git a/extendable_fastapi/tests/test_strict_extendable_base_model.py b/extendable_fastapi/tests/test_strict_extendable_base_model.py
new file mode 100644
index 000000000..650982f43
--- /dev/null
+++ b/extendable_fastapi/tests/test_strict_extendable_base_model.py
@@ -0,0 +1,68 @@
+import warnings
+from datetime import date
+
+from extendable_pydantic import ExtendableBaseModel
+
+from pydantic import ValidationError
+
+from ..schemas import StrictExtendableBaseModel
+from .common import FastAPITransactionCase
+
+
+class TestStrictExtendableBaseModel(FastAPITransactionCase):
+ class Model(ExtendableBaseModel):
+ x: int
+ d: date | None
+
+ class StrictModel(StrictExtendableBaseModel):
+ x: int
+ d: date | None
+
+ def test_Model_revalidate_instance_never(self):
+ # Missing required fields but no re-validation
+ m = self.Model.model_construct()
+ self.assertEqual(m.model_validate(m).model_dump(), {})
+
+ def test_StrictModel_revalidate_instance_always(self):
+ # Missing required fields and always revalidate
+ m = self.StrictModel.model_construct()
+ with self.assertRaises(ValidationError):
+ m.model_validate(m)
+
+ def test_Model_validate_assignment_false(self):
+ # Wrong assignment but no re-validation at assignment
+ m = self.Model(x=1, d=None)
+ m.x = "TEST"
+ with warnings.catch_warnings():
+ # Disable 'Expected `int` but got `str`' warning
+ warnings.simplefilter("ignore")
+ self.assertEqual(m.model_dump(), {"x": "TEST", "d": None})
+
+ def test_StrictModel_validate_assignment_true(self):
+ # Wrong assignment and validation at assignment
+ m = self.StrictModel.model_construct()
+ m.x = 1 # Validate only this field -> OK even if m.d is not set
+ with self.assertRaises(ValidationError):
+ m.x = "TEST"
+
+ def test_Model_extra_ignored(self):
+ # Ignore extra fields
+ m = self.Model(x=1, z=3, d=None)
+ self.assertEqual(m.model_dump(), {"x": 1, "d": None})
+
+ def test_StrictModel_extra_forbidden(self):
+ # Forbid extra fields
+ with self.assertRaises(ValidationError):
+ self.StrictModel(x=1, z=3, d=None)
+
+ def test_Model_strict_false(self):
+ # Coerce str->date is allowed
+ m = self.Model(x=1, d=None)
+ m.d = "2023-01-01"
+ self.assertTrue(m.model_validate(m))
+
+ def test_StrictModel_strict_true(self):
+ # Coerce str->date is forbidden
+ m = self.StrictModel(x=1, d=None)
+ with self.assertRaises(ValidationError):
+ m.d = "2023-01-01"
diff --git a/fastapi/README.rst b/fastapi/README.rst
index 1d7f693d3..e9c1c4122 100644
--- a/fastapi/README.rst
+++ b/fastapi/README.rst
@@ -23,7 +23,7 @@ Odoo FastAPI
:target: https://runbot.odoo-community.org/runbot/271/16.0
:alt: Try me on Runbot
-|badge1| |badge2| |badge3| |badge4| |badge5|
+|badge1| |badge2| |badge3| |badge4| |badge5|
This addon provides the basis to smoothly integrate the `FastAPI`_
framework into Odoo.
@@ -862,14 +862,14 @@ complete transparent. This addon is called
`odoo-addon-extendable-fastapi `_.
When you want to allow other addons to extend a pydantic model, you must
-first define the model as an extendable model by using a dedicated metaclass
+first define the model as an extendable model by using the dedicated
+class from `extendable_pydantic`.
.. code-block:: python
- from pydantic import BaseModel
- from extendable_pydantic import ExtendableModelMeta
+ from extendable_pydantic import ExtendableBaseModel
- class Partner(BaseModel, metaclass=ExtendableModelMeta):
+ class Partner(ExtendableBaseModel):
name = 0.1
model_config = ConfigDict(from_attributes=True)
@@ -890,6 +890,11 @@ pydantic.
"""Return the location"""
return Partner.model_validate(partner)
+Some of the Pydantic config parameters are not set by default but are very useful when
+using `odoo-addon-fastapi`, hence the `ExtendableBaseModel` model was extended
+to `StrictExtendableBaseModel` in **'odoo-addon-extendable-fastapi'**
+to force revalidation of the instances at each change, and forbidding the
+extra values.
If you need to add a new field into the model **'Partner'**, you can extend it
in your new addon by defining a new model that inherits from the model **'Partner'**.
@@ -899,7 +904,7 @@ in your new addon by defining a new model that inherits from the model **'Partne
from typing import Optional
from odoo.addons.fastapi.models.fastapi_endpoint_demo import Partner
- class PartnerExtended(Partner, extends=Partner):
+ class PartnerExtended(Partner, extends=True):
email: Optional[str]
If your new addon is installed in a database, a call to the route handler
@@ -1503,7 +1508,7 @@ promote its widespread use.
Current `maintainer `__:
-|maintainer-lmignon|
+|maintainer-lmignon|
This module is part of the `OCA/rest-framework `_ project on GitHub.
diff --git a/requirements.txt b/requirements.txt
index dbae38c38..87fd73dbe 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -6,6 +6,7 @@ cerberus
contextvars
extendable-pydantic
extendable-pydantic>=1.0.0
+extendable-pydantic>=1.1.0
extendable>=0.0.4
fastapi
graphene