diff --git a/NEWS.rst b/NEWS.rst
index 08c78c8d765da3f8d20f03a27ff48e20a54c6b38_TkVXUy5yc3Q=..0a3fa9ad94afaf928de060d3cb15e5ef96988d91_TkVXUy5yc3Q= 100644
--- a/NEWS.rst
+++ b/NEWS.rst
@@ -1,6 +1,18 @@
 Changelog
 =========
 
+18.0.4.0.0
+----------
+
+In Model converter, also validate messages in ``message_to_odoo``.
+
+Model converter argument changed, `__type__` is now the last argument and is optional. It is expected not to be used
+anymore.
+Added possible_datatypes property and odoo_datatype on Converter. Defaults to empty set and None.
+On Model, it can be set.
+
+Replace generic exception.
+
 18.0.3.1.0
 ----------
 
diff --git a/__manifest__.py b/__manifest__.py
index 08c78c8d765da3f8d20f03a27ff48e20a54c6b38_X19tYW5pZmVzdF9fLnB5..0a3fa9ad94afaf928de060d3cb15e5ef96988d91_X19tYW5pZmVzdF9fLnB5 100644
--- a/__manifest__.py
+++ b/__manifest__.py
@@ -1,7 +1,7 @@
 ##############################################################################
 #
 #    Converter Odoo module
-#    Copyright © 2020-2024 XCG Consulting <https://xcg-consulting.fr>
+#    Copyright © 2020-2025 XCG Consulting <https://xcg-consulting.fr>
 #
 #    This program is free software: you can redistribute it and/or modify
 #    it under the terms of the GNU Affero General Public License as
@@ -21,7 +21,7 @@
     "name": "Converter",
     "license": "AGPL-3",
     "summary": "Convert odoo records to/from plain data structures.",
-    "version": "18.0.3.1.0",
+    "version": "18.0.4.0.0",
     "category": "Hidden",
     "author": "XCG Consulting",
     "website": "https://orbeet.io/",
diff --git a/base.py b/base.py
index 08c78c8d765da3f8d20f03a27ff48e20a54c6b38_YmFzZS5weQ==..0a3fa9ad94afaf928de060d3cb15e5ef96988d91_YmFzZS5weQ== 100644
--- a/base.py
+++ b/base.py
@@ -43,9 +43,9 @@
 from odoo import api, models  # type: ignore[import-untyped]
 
 from .exception import InternalError
-from .validate import Validator
+from .validate import Validation, Validator
 
 logger = logging.getLogger(__name__)
 
 
 class SkipType:
@@ -47,9 +47,20 @@
 
 logger = logging.getLogger(__name__)
 
 
 class SkipType:
-    pass
+    def get__type__(self) -> set[str]:
+        # Avoid conditions isinstance(converter, SkipType)
+        return set()
+
+    @property
+    def possible_datatypes(self) -> set[str]:
+        # Avoid conditions isinstance(converter, SkipType)
+        return set()
+
+    def odoo_datatype(self, instance: models.BaseModel) -> str | None:
+        # Avoid conditions isinstance(converter, SkipType)
+        return None
 
 
 Skip = SkipType()
@@ -90,8 +101,13 @@
     return ctx
 
 
+class NotAnInstanceGetterException(Exception):
+    def __init__(self):
+        super().__init__("Not an instance getter")
+
+
 class Converter:
     """Base converter class.
     It does not actually convert anything.
     """
 
@@ -93,8 +109,11 @@
 class Converter:
     """Base converter class.
     It does not actually convert anything.
     """
 
+    def __init__(self):
+        self._validation: Validation = Validation.SKIP
+
     def odoo_to_message(self, instance: models.BaseModel, ctx: Context = None) -> Any:
         """From an instance, this method returns a matching value for the
         message field.
@@ -134,7 +153,7 @@
         self, odoo_env: api.Environment, message_data
     ) -> models.BaseModel | NewinstanceType | None:
         """Return an instance of a model. Check is_instance_getter before calling"""
-        raise Exception("Not an instance getter")
+        raise NotAnInstanceGetterException()
 
     def get__type__(self) -> set[str]:
         """Indicate if this converter is associated to several __type__.
@@ -166,7 +185,7 @@
                 )
 
     @property
-    def validation(self) -> str:
+    def validation(self) -> Validation:
         return self._get_validation()
 
     @validation.setter
@@ -170,6 +189,6 @@
         return self._get_validation()
 
     @validation.setter
-    def validation(self, value: str) -> None:
+    def validation(self, value: Validation) -> None:
         self._set_validation(value)
 
@@ -174,5 +193,5 @@
         self._set_validation(value)
 
-    def _get_validation(self) -> str:
+    def _get_validation(self) -> Validation:
         return self._validation
 
@@ -177,7 +196,7 @@
         return self._validation
 
-    def _set_validation(self, value: str) -> None:
+    def _set_validation(self, value: Validation) -> None:
         """Define if validation should be done"""
         assert value is not None
         self._validation = value
 
@@ -180,7 +199,16 @@
         """Define if validation should be done"""
         assert value is not None
         self._validation = value
 
+    @property
+    def possible_datatypes(self) -> set[str]:
+        """Possible values for datatype."""
+        # A set, as for get___type__, to allow switch to handle different messages.
+        return set()
+
+    def odoo_datatype(self, instance: models.BaseModel) -> str | None:
+        return None
+
 
 class PostHookConverter(Converter, metaclass=ABCMeta):
     @abstractmethod
@@ -191,6 +219,6 @@
 class Readonly(Converter):
     def __init__(self, conv: Converter):
         super().__init__()
-        self.conv = conv
+        self._conv = conv
 
     def odoo_to_message(self, instance: models.BaseModel, ctx: Context = None) -> Any:
@@ -195,6 +223,9 @@
 
     def odoo_to_message(self, instance: models.BaseModel, ctx: Context = None) -> Any:
-        return self.conv.odoo_to_message(instance, ctx)
+        return self._conv.odoo_to_message(instance, ctx)
+
+    def odoo_datatype(self, instance: models.BaseModel) -> str | None:
+        return self._conv.odoo_datatype(instance)
 
 
 class Writeonly(Converter):
@@ -225,6 +256,10 @@
     ) -> models.BaseModel | NewinstanceType | None:
         return self._conv.get_instance(odoo_env, message_data)
 
+    @property
+    def possible_datatypes(self) -> set[str]:
+        return self._conv.possible_datatypes
+
 
 class Computed(Converter):
     def __init__(self, from_odoo: Callable[[models.BaseModel, Context], Any]):
diff --git a/model.py b/model.py
index 08c78c8d765da3f8d20f03a27ff48e20a54c6b38_bW9kZWwucHk=..0a3fa9ad94afaf928de060d3cb15e5ef96988d91_bW9kZWwucHk= 100644
--- a/model.py
+++ b/model.py
@@ -21,7 +21,7 @@
 import traceback
 from collections import OrderedDict
 from collections.abc import Iterable, Mapping
-from typing import Any
+from typing import Any, Final
 
 import fastjsonschema  # type: ignore[import-untyped]
 from odoo import _, api, models  # type: ignore[import-untyped]
@@ -42,8 +42,17 @@
 _logger = logging.getLogger(__name__)
 
 
+class IncorrectTypeException(Exception):
+    """__type__ is in the message is not the same as the expected value"""
+
+
+class MissingRequiredValidatorException(Exception):
+    def __init__(self):
+        super().__init__("Strict validation without validator")
+
+
 class Model(PostHookConverter):
     """A converter that takes a dict of key, used when a message has values"""
 
     def __init__(
         self,
@@ -45,9 +54,8 @@
 class Model(PostHookConverter):
     """A converter that takes a dict of key, used when a message has values"""
 
     def __init__(
         self,
-        __type__: str | None,
         converters: Mapping[str, Converter],
         json_schema: str | None = None,
         # The validator is usually not given at this point but is common
@@ -56,4 +64,6 @@
         merge_with: Iterable[Converter] | None = None,
         validation: Validation = Validation.SKIP,
         context: ContextBuilder | None = None,
+        datatype: str | None = None,
+        __type__: str | None = None,
     ):
@@ -59,4 +69,7 @@
     ):
+        """
+        :param datatype: datatype to use. Usually used with None __type__.
+        """
         super().__init__()
         self._type: str | None = __type__
         self._converters: Mapping[str, Converter] = converters
@@ -68,6 +81,7 @@
         self.context: ContextBuilder | None = context
         self.validator = validator
         self.validation = validation
+        self._datatype: Final[str | None] = datatype
 
         for key, converter in converters.items():
             if self._get_instance is None and converter.is_instance_getter:
@@ -120,7 +134,7 @@
                     if self.validation == Validation.STRICT:
                         raise
             elif self.validation == Validation.STRICT:
-                raise Exception("Strict validation without validator")
+                raise MissingRequiredValidatorException()
 
         return message_data
 
@@ -134,4 +148,15 @@
     ) -> dict | SkipType:
         values: dict[str, Any] = OrderedDict()
 
+        if self.validation != Validation.SKIP and self._jsonschema is not None:
+            if self.validator:
+                try:
+                    self.validator.validate(self._jsonschema, message_value)
+                except (NotInitialized, fastjsonschema.JsonSchemaException):
+                    _logger.warning("Validation failed", exc_info=True)
+                    if self.validation == Validation.STRICT:
+                        raise
+            elif self.validation == Validation.STRICT:
+                raise MissingRequiredValidatorException()
+
         if self._type is not None and message_value["__type__"] != self._type:
@@ -137,5 +162,5 @@
         if self._type is not None and message_value["__type__"] != self._type:
-            raise Exception(
+            raise IncorrectTypeException(
                 "Expected __type__ {}, found {}".format(
                     self._type, message_value["__type__"]
                 )
@@ -191,3 +216,13 @@
 
     def get__type__(self) -> set[str]:
         return set() if self._type is None else {self._type}
+
+    @property
+    def possible_datatypes(self) -> set[str]:
+        result = set()
+        if self._datatype is not None:
+            result.add(self._datatype)
+        return result
+
+    def odoo_datatype(self, instance: models.BaseModel) -> str | None:
+        return self._datatype
diff --git a/pyproject.toml b/pyproject.toml
index 08c78c8d765da3f8d20f03a27ff48e20a54c6b38_cHlwcm9qZWN0LnRvbWw=..0a3fa9ad94afaf928de060d3cb15e5ef96988d91_cHlwcm9qZWN0LnRvbWw= 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -3,7 +3,7 @@
 description = "Odoo addon to convert records to and from plain data structures"
 dynamic = ["version"]
 readme = "README.rst"
-requires-python = "~=3.11"
+requires-python = ">=3.11"
 license = { file = "LICENSE", name = "GNU Affero General Public License v3" }
 keywords = ["odoo"]
 authors = [{ name = "XCG Consulting" }]
diff --git a/switch.py b/switch.py
index 08c78c8d765da3f8d20f03a27ff48e20a54c6b38_c3dpdGNoLnB5..0a3fa9ad94afaf928de060d3cb15e5ef96988d91_c3dpdGNoLnB5 100644
--- a/switch.py
+++ b/switch.py
@@ -1,7 +1,7 @@
 ##############################################################################
 #
 #    Converter Odoo module
-#    Copyright © 2020, 2024 XCG Consulting <https://xcg-consulting.fr>
+#    Copyright © 2020, 2024, 2025 XCG Consulting <https://xcg-consulting.fr>
 #
 #    This program is free software: you can redistribute it and/or modify
 #    it under the terms of the GNU Affero General Public License as
@@ -36,7 +36,7 @@
 
     .. code-block:: python
 
-        AURION_REFERENTIAL: Switch(
+        Switch(
             [
               (
                   lambda record: record.is_xxx,
@@ -58,7 +58,7 @@
             ]
         ],
         validator: Validator | None = None,
-        validation: str = Validation.SKIP,
+        validation: Validation = Validation.SKIP,
         context: ContextBuilder | None = None,
     ):
         """
@@ -103,7 +103,7 @@
                     value_present=value_present,
                 )
 
-        return {}
+        return Skip
 
     @property
     def is_instance_getter(self) -> bool:
@@ -132,7 +132,7 @@
             if not isinstance(converter, SkipType):
                 converter.validator = value
 
-    def _set_validation(self, value: str) -> None:
+    def _set_validation(self, value: Validation) -> None:
         # also set validation on any converters in our switch
         super()._set_validation(value)
         for _out_cond, _in_cond, converter in self._converters:
@@ -142,6 +142,5 @@
     def get__type__(self) -> set[str]:
         types = set()
         for _out_cond, _in_cond, converter in self._converters:
-            if not isinstance(converter, SkipType):
-                types.update(converter.get__type__())
+            types.update(converter.get__type__())
         return types
@@ -147,1 +146,14 @@
         return types
+
+    @property
+    def possible_datatypes(self) -> set[str]:
+        result = super().possible_datatypes
+        for _out_cond, _in_cond, converter in self._converters:
+            result.update(converter.possible_datatypes)
+        return result
+
+    def odoo_datatype(self, instance: models.BaseModel) -> str | None:
+        for out_cond, _in_cond, converter in self._converters:
+            if out_cond is None or out_cond(instance):
+                return converter.odoo_datatype(instance)
+        return super().odoo_datatype(instance)
diff --git a/tests/test_base.py b/tests/test_base.py
index 08c78c8d765da3f8d20f03a27ff48e20a54c6b38_dGVzdHMvdGVzdF9iYXNlLnB5..0a3fa9ad94afaf928de060d3cb15e5ef96988d91_dGVzdHMvdGVzdF9iYXNlLnB5 100644
--- a/tests/test_base.py
+++ b/tests/test_base.py
@@ -60,7 +60,6 @@
             Field("name"),
         )
         converter = Model(
-            "res.users",
             {
                 "user_creator_name": rel,
             },
@@ -64,6 +63,7 @@
             {
                 "user_creator_name": rel,
             },
+            __type__="res.users",
         )
         with self.assertRaises(UserError) as e:
             converter.odoo_to_message(self.active_user)
@@ -84,7 +84,6 @@
 
     def test_convert(self):
         converter = Model(
-            None,
             {
                 "active": Field("active"),
                 "ref": Xref("base"),
diff --git a/tests/test_converters.py b/tests/test_converters.py
index 08c78c8d765da3f8d20f03a27ff48e20a54c6b38_dGVzdHMvdGVzdF9jb252ZXJ0ZXJzLnB5..0a3fa9ad94afaf928de060d3cb15e5ef96988d91_dGVzdHMvdGVzdF9jb252ZXJ0ZXJzLnB5 100644
--- a/tests/test_converters.py
+++ b/tests/test_converters.py
@@ -99,7 +99,6 @@
         """
         return Model(
             # TODO add a schema to test validation
-            "test-type",
             {
                 "id": Xref(),
                 "country_code": RelationToOne(
@@ -108,4 +107,5 @@
                 "name": Field("name"),
                 "partner_id": RelationToOne("partner_id", "res.partner", Xref()),
             },
+            __type__="test-type",
         )
diff --git a/tests/test_relation.py b/tests/test_relation.py
index 08c78c8d765da3f8d20f03a27ff48e20a54c6b38_dGVzdHMvdGVzdF9yZWxhdGlvbi5weQ==..0a3fa9ad94afaf928de060d3cb15e5ef96988d91_dGVzdHMvdGVzdF9yZWxhdGlvbi5weQ== 100644
--- a/tests/test_relation.py
+++ b/tests/test_relation.py
@@ -75,9 +75,8 @@
 
         # This converter wraps a user and adds info from its related partner.
         converter = Model(
-            None,
             {
                 "partner": RelationToOne(
                     "partner_id",
                     "res.partner",
                     Model(
@@ -79,11 +78,10 @@
             {
                 "partner": RelationToOne(
                     "partner_id",
                     "res.partner",
                     Model(
-                        "partner",
                         {
                             "color": Field("color"),
                             "name": Field("name"),
                             "xref": Xref("test"),
                         },
@@ -85,8 +83,9 @@
                         {
                             "color": Field("color"),
                             "name": Field("name"),
                             "xref": Xref("test"),
                         },
+                        __type__="partner",
                     ),
                 ),
                 "xref": Xref("base"),
@@ -128,9 +127,8 @@
 
         # This converter wraps a user and adds info from its related partner.
         converter = Model(
-            None,
             {
                 "users": RelationToMany(
                     "user_ids",
                     "res.users",
                     Model(
@@ -132,10 +130,9 @@
             {
                 "users": RelationToMany(
                     "user_ids",
                     "res.users",
                     Model(
-                        "user",
                         {
                             "email": Field("email"),
                             "xref": Xref("base"),
                         },
@@ -138,7 +135,8 @@
                         {
                             "email": Field("email"),
                             "xref": Xref("base"),
                         },
+                        __type__="user",
                     ),
                 ),
                 "xref": Xref("base"),
@@ -167,3 +165,46 @@
         # Check the partner's users
         self.assertTrue(partner.user_ids)
         self.assertEqual(len(partner.user_ids), 2)
+
+    def test_many2many_to_odoo_no___type__(self):
+        """Ensure multiple sub-objects linked from the main one gets updated
+        when Odoo receives a message.
+        """
+
+        # This converter wraps a user and adds info from its related partner.
+        converter = Model(
+            {
+                "users": RelationToMany(
+                    "user_ids",
+                    "res.users",
+                    Model(
+                        {
+                            "email": Field("email"),
+                            "xref": Xref("base"),
+                        },
+                    ),
+                ),
+                "xref": Xref("base"),
+            },
+        )
+
+        partner = self.env.ref("base.main_partner")
+        self.assertFalse(partner.user_ids)
+
+        # Run our message reception.
+        message: dict[str, Any] = {
+            "users": [
+                {
+                    "xref": "user_admin",
+                },
+                {
+                    "xref": "user_demo",
+                },
+            ],
+            "xref": "main_partner",
+        }
+        message_to_odoo(self.env, message, self.env["res.partner"], converter)
+
+        # Check the partner's users
+        self.assertTrue(partner.user_ids)
+        self.assertEqual(len(partner.user_ids), 2)
diff --git a/tests/test_switch.py b/tests/test_switch.py
index 08c78c8d765da3f8d20f03a27ff48e20a54c6b38_dGVzdHMvdGVzdF9zd2l0Y2gucHk=..0a3fa9ad94afaf928de060d3cb15e5ef96988d91_dGVzdHMvdGVzdF9zd2l0Y2gucHk= 100644
--- a/tests/test_switch.py
+++ b/tests/test_switch.py
@@ -38,5 +38,4 @@
                     lambda e: e.active,
                     falser,
                     Model(
-                        "__activebanks__",
                         {"name": Field("name"), "active": Field("active")},
@@ -42,7 +41,8 @@
                         {"name": Field("name"), "active": Field("active")},
+                        __type__="__activebanks__",
                     ),
                 ),
                 (
                     None,
                     falser,
                     Model(
@@ -43,8 +43,7 @@
                     ),
                 ),
                 (
                     None,
                     falser,
                     Model(
-                        "__inactivebanks__",
                         {"name": Field("name"), "active": Field("active")},
@@ -50,4 +49,5 @@
                         {"name": Field("name"), "active": Field("active")},
+                        __type__="__inactivebanks__",
                     ),
                 ),
             ]