diff --git a/__init__.py b/__init__.py
index f88452a152ee41ae12f247f0a25d3823cb13e089_X19pbml0X18ucHk=..2a40d04b025ac4fc4985feda4621981fcd9c2563_X19pbml0X18ucHk= 100644
--- a/__init__.py
+++ b/__init__.py
@@ -43,9 +43,7 @@
 from .relation import RelationToMany, RelationToManyMap, RelationToOne, relation
 from .switch import Switch
 from .validate import (
-    VALIDATION_SKIP,
-    VALIDATION_SOFT,
-    VALIDATION_STRICT,
+    Validation,
     NotInitialized,
     Validator,
 )
diff --git a/__manifest__.py b/__manifest__.py
index f88452a152ee41ae12f247f0a25d3823cb13e089_X19tYW5pZmVzdF9fLnB5..2a40d04b025ac4fc4985feda4621981fcd9c2563_X19tYW5pZmVzdF9fLnB5 100644
--- a/__manifest__.py
+++ b/__manifest__.py
@@ -17,7 +17,6 @@
 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
 #
 ##############################################################################
-
 {
     "name": "Converter",
     "license": "AGPL-3",
@@ -26,8 +25,7 @@
     "category": "Hidden",
     "author": "XCG Consulting",
     "website": "https://orbeet.io/",
-    "depends": ["base", "mail"],
-    "data": [],
+    "depends": ["mail"],
     "installable": True,
     "external_dependencies": {"python": ["fastjsonschema"]},
 }
diff --git a/base.py b/base.py
index f88452a152ee41ae12f247f0a25d3823cb13e089_YmFzZS5weQ==..2a40d04b025ac4fc4985feda4621981fcd9c2563_YmFzZS5weQ== 100644
--- a/base.py
+++ b/base.py
@@ -36,6 +36,7 @@
 
 import inspect
 import logging
+from abc import ABCMeta, abstractmethod
 from collections.abc import Callable, Mapping
 from typing import Any
 
@@ -39,7 +40,7 @@
 from collections.abc import Callable, Mapping
 from typing import Any
 
-from odoo import api, models
+from odoo import api, models  # type: ignore[import-untyped]
 
 from .exception import InternalError
 from .validate import Validator
@@ -60,7 +61,8 @@
 
 Newinstance = NewinstanceType()
 
-ContextBuilder = Callable[[models.Model, Mapping | None], Mapping | None]
+Context = Mapping | None
+ContextBuilder = Callable[[models.BaseModel, Context], Context]
 
 PHASE_PRECREATE = "precreate"
 PHASE_POSTCREATE = "postcreate"
@@ -71,6 +73,6 @@
 
 
 def build_context(
-    instance: models.Model | None,
-    ctx: Mapping | None,
+    instance: models.BaseModel | None,
+    ctx: Context,
     extend: ContextBuilder | None,
@@ -76,5 +78,5 @@
     extend: ContextBuilder | None,
-) -> dict | None:
+) -> Context:
     if instance is None:
         return ctx
     if extend:
@@ -82,7 +84,9 @@
             ctx = {}
         else:
             ctx = dict(ctx)
-        ctx.update(extend(instance))
+        extended = extend(instance, None)
+        if extended is not None:
+            ctx.update(extended)
     return ctx
 
 
@@ -91,9 +95,7 @@
     It does not actually convert anything.
     """
 
-    def odoo_to_message(
-        self, instance: models.Model, ctx: Mapping | None = None
-    ) -> Any:
+    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.
         :param instance: an instance of an Odoo model
@@ -107,7 +109,7 @@
         odoo_env: api.Environment,
         phase: str,
         message_value: Any,
-        instance: models.Model,
+        instance: models.BaseModel,
         value_present: bool = True,
     ) -> dict:
         """From a message, returns a dict.
@@ -123,7 +125,7 @@
         """
         return {}
 
-    @classmethod
-    def is_instance_getter(cls) -> bool:
+    @property
+    def is_instance_getter(self) -> bool:
         return False
 
@@ -128,5 +130,12 @@
         return False
 
+    # XXX should that be moved to a different class, like PostHookConverter
+    def get_instance(
+        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")
+
     def get__type__(self) -> set[str]:
         """Indicate if this converter is associated to several __type__.
         If so, it will be called with incoming messages associated to them.
@@ -136,7 +145,7 @@
     @property
     def validator(self) -> Validator | None:
         """A validator to use for validation of created messages"""
-        return self._validator
+        return self._get_validator()
 
     @validator.setter
     def validator(self, value: Validator | None) -> None:
@@ -140,6 +149,12 @@
 
     @validator.setter
     def validator(self, value: Validator | None) -> None:
+        self._set_validator(value)
+
+    def _get_validator(self) -> Validator | None:
+        return self._validator
+
+    def _set_validator(self, value: Validator | None) -> None:
         if value is None:
             self._validator = None
         else:
@@ -152,7 +167,7 @@
 
     @property
     def validation(self) -> str:
-        return self._validation
+        return self._get_validation()
 
     @validation.setter
     def validation(self, value: str) -> None:
@@ -156,8 +171,14 @@
 
     @validation.setter
     def validation(self, value: str) -> None:
+        self._set_validation(value)
+
+    def _get_validation(self) -> str:
+        return self._validation
+
+    def _set_validation(self, value: str) -> None:
         """Define if validation should be done"""
         assert value is not None
         self._validation = value
 
 
@@ -159,10 +180,16 @@
         """Define if validation should be done"""
         assert value is not None
         self._validation = value
 
 
+class PostHookConverter(Converter, metaclass=ABCMeta):
+    @abstractmethod
+    def post_hook(self, instance: models.BaseModel, message_data):
+        """Post hook"""
+
+
 class Readonly(Converter):
     def __init__(self, conv):
         super().__init__()
         self.conv = conv
 
@@ -164,10 +191,10 @@
 class Readonly(Converter):
     def __init__(self, conv):
         super().__init__()
         self.conv = conv
 
-    def odoo_to_message(self, instance: models.Model, ctx: dict | None = None) -> Any:
+    def odoo_to_message(self, instance: models.BaseModel, ctx: Context = None) -> Any:
         return self.conv.odoo_to_message(instance, ctx)
 
 
 class Computed(Converter):
@@ -170,8 +197,8 @@
         return self.conv.odoo_to_message(instance, ctx)
 
 
 class Computed(Converter):
-    def __init__(self, from_odoo: Callable[[models.Model, Mapping | None], Any]):
+    def __init__(self, from_odoo: Callable[[models.BaseModel, Context], Any]):
         self.from_odoo = from_odoo
 
         sig = inspect.signature(from_odoo)
@@ -182,7 +209,5 @@
                 " got {self.from_odoo_arg_count}",
             )
 
-    def odoo_to_message(
-        self, instance: models.Model, ctx: Mapping | None = None
-    ) -> Any:
+    def odoo_to_message(self, instance: models.BaseModel, ctx: Context = None) -> Any:
         if self.from_odoo_arg_count == 1:
@@ -188,5 +213,5 @@
         if self.from_odoo_arg_count == 1:
-            return self.from_odoo(instance)
+            return self.from_odoo(instance, None)
         return self.from_odoo(instance, ctx)
 
 
@@ -194,9 +219,7 @@
     def __init__(self, value):
         self.value = value
 
-    def odoo_to_message(
-        self, instance: models.Model, ctx: Mapping | None = None
-    ) -> Any:
+    def odoo_to_message(self, instance: models.BaseModel, ctx: Context = None) -> Any:
         return self.value
 
 
@@ -206,7 +229,7 @@
     model_name: str,
     converter: Converter,
     operation: str | None = None,
-) -> models.Model:
+) -> models.BaseModel:
     """
 
     :param odoo_env: an Odoo environment
@@ -218,6 +241,7 @@
        :py:meth:odoo.addons.Converter.get_instance
     :return:
     """
+    instance: NewinstanceType | models.BaseModel
     if operation == OPERATION_CREATION:
         instance = Newinstance
     else:
diff --git a/doc/autotodo.py b/doc/autotodo.py
index f88452a152ee41ae12f247f0a25d3823cb13e089_ZG9jL2F1dG90b2RvLnB5..2a40d04b025ac4fc4985feda4621981fcd9c2563_ZG9jL2F1dG90b2RvLnB5 100755
--- a/doc/autotodo.py
+++ b/doc/autotodo.py
@@ -22,7 +22,7 @@
 import os
 import os.path
 import sys
-from collections.abc import Mapping
+from collections.abc import MutableMapping
 
 
 def main():
@@ -31,13 +31,13 @@
         sys.exit(1)
 
     folder = sys.argv[1]
-    exts = sys.argv[2].split(",")
-    tags = sys.argv[3].split(",")
-    todolist = {tag: [] for tag in tags}
-    path_file_length: Mapping[str, int] = {}
+    exts: list[str] = sys.argv[2].split(",")
+    tags: list[str] = sys.argv[3].split(",")
+    todolist: dict[str, list[tuple[str, int, str]]] = {tag: [] for tag in tags}
+    path_file_length: MutableMapping[str, int] = {}
 
     for root, _dirs, files in os.walk(folder):
         scan_folder((exts, tags, todolist, path_file_length), root, files)
     create_autotodo(folder, todolist, path_file_length)
 
 
@@ -38,10 +38,10 @@
 
     for root, _dirs, files in os.walk(folder):
         scan_folder((exts, tags, todolist, path_file_length), root, files)
     create_autotodo(folder, todolist, path_file_length)
 
 
-def write_info(f, infos, folder, path_file_length: Mapping[str, int]):
+def write_info(f, infos, folder, path_file_length: MutableMapping[str, int]):
     # Check sphinx version for lineno-start support
 
     import sphinx
@@ -78,10 +78,10 @@
         f.write("\n")
 
 
-def create_autotodo(folder, todolist, path_file_length: Mapping[str, int]):
+def create_autotodo(folder, todolist, path_file_length: MutableMapping[str, int]):
     with open("autotodo", "w+") as f:
         for tag, info in list(todolist.items()):
             f.write("{}\n{}\n\n".format(tag, "=" * len(tag)))
             write_info(f, info, folder, path_file_length)
 
 
@@ -82,10 +82,19 @@
     with open("autotodo", "w+") as f:
         for tag, info in list(todolist.items()):
             f.write("{}\n{}\n\n".format(tag, "=" * len(tag)))
             write_info(f, info, folder, path_file_length)
 
 
-def scan_folder(data_tuple, dirname, names):
+def scan_folder(
+    data_tuple: tuple[
+        list[str],
+        list[str],
+        dict[str, list[tuple[str, int, str]]],
+        MutableMapping[str, int],
+    ],
+    dirname: str,
+    names: list[str],
+):
     (exts, tags, res, path_file_length) = data_tuple
     for name in names:
         (root, ext) = os.path.splitext(name)
@@ -98,7 +107,9 @@
                     res[tag].extend(info)
 
 
-def scan_file(filename, tags) -> tuple[dict[str, list[tuple[str, int, str]]], int]:
+def scan_file(
+    filename: str, tags: list[str]
+) -> tuple[dict[str, list[tuple[str, int, str]]], int]:
     res: dict[str, list[tuple[str, int, str]]] = {tag: [] for tag in tags}
     line_num: int = 0
     with open(filename) as f:
diff --git a/doc/conf.py b/doc/conf.py
index f88452a152ee41ae12f247f0a25d3823cb13e089_ZG9jL2NvbmYucHk=..2a40d04b025ac4fc4985feda4621981fcd9c2563_ZG9jL2NvbmYucHk= 100644
--- a/doc/conf.py
+++ b/doc/conf.py
@@ -26,7 +26,6 @@
 extensions = [
     "sphinx.ext.autodoc",
     "sphinx.ext.doctest",
-    "sphinx.ext.intersphinx",
     "sphinx.ext.todo",
     "sphinx.ext.coverage",
     "sphinx.ext.graphviz",
@@ -104,8 +103,6 @@
 
 # -- Options for LaTeX output ---------------------------------------------
 
-latex_elements = {}
-
 # Grouping the document tree into LaTeX files. List of tuples
 # (source start file, target name, title,
 #  author, documentclass [howto, manual, or own class]).
diff --git a/field.py b/field.py
index f88452a152ee41ae12f247f0a25d3823cb13e089_ZmllbGQucHk=..2a40d04b025ac4fc4985feda4621981fcd9c2563_ZmllbGQucHk= 100644
--- a/field.py
+++ b/field.py
@@ -20,6 +20,6 @@
 
 import datetime
 from collections.abc import Callable
-from typing import Any
+from typing import Any, Literal
 
 import pytz
@@ -24,4 +24,4 @@
 
 import pytz
-from odoo import api, models
+from odoo import api, models  # type: ignore[import-untyped]
 
@@ -27,5 +27,5 @@
 
-from .base import PHASE_POSTCREATE, Converter, Newinstance, Skip
+from .base import PHASE_POSTCREATE, Context, Converter, Newinstance, Skip
 
 
 class Field(Converter):
@@ -61,7 +61,7 @@
         self.default = default
         self.send_empty = send_empty or required_blank_value is not None
         self.required_blank_value = required_blank_value
-        self._blank_value = None
+        self._blank_value: Literal[False, "", 0] | list | float | None = None
         self._message_formatter = message_formatter
         self._odoo_formatter = odoo_formatter
 
@@ -87,7 +87,7 @@
 
         return self._blank_value
 
-    def odoo_to_message(self, instance: models.Model, ctx: dict | None = None) -> Any:
+    def odoo_to_message(self, instance: models.BaseModel, ctx: Context = None) -> Any:
         value = False
         # could be empty due to chaining converter on a many2one without value
         # for example
@@ -120,7 +120,7 @@
         odoo_env: api.Environment,
         phase: str,
         message_value: Any,
-        instance: models.Model,
+        instance: models.BaseModel,
         value_present: bool = True,
     ) -> dict:
         if phase == PHASE_POSTCREATE:
@@ -161,7 +161,7 @@
         super().__init__(field_name, default, send_empty, required_blank_value)
         self._language = language
 
-    def _lazy_dicts(self, instance: models.Model):
+    def _lazy_dicts(self, instance: models.BaseModel):
         if not hasattr(self, "_lazy_dict_odoo_to_message"):
             description_selection = (
                 instance.with_context(lang=self._language)
@@ -173,7 +173,7 @@
                 (b, a) for a, b in description_selection
             )
 
-    def odoo_to_message(self, instance: models.Model, ctx: dict | None = None) -> Any:
+    def odoo_to_message(self, instance: models.BaseModel, ctx: Context = None) -> Any:
         value = super().odoo_to_message(instance, ctx)
         if value:
             self._lazy_dicts(instance)
@@ -185,7 +185,7 @@
         odoo_env: api.Environment,
         phase: str,
         message_value: Any,
-        instance: models.Model,
+        instance: models.BaseModel,
         value_present: bool = True,
     ) -> dict:
         message = super().message_to_odoo(
diff --git a/keyfield.py b/keyfield.py
index f88452a152ee41ae12f247f0a25d3823cb13e089_a2V5ZmllbGQucHk=..2a40d04b025ac4fc4985feda4621981fcd9c2563_a2V5ZmllbGQucHk= 100644
--- a/keyfield.py
+++ b/keyfield.py
@@ -20,5 +20,5 @@
 
 from typing import Any
 
-from odoo import models
+from odoo import api, models  # type: ignore[import-untyped]
 
@@ -24,5 +24,5 @@
 
-from .base import Converter
+from .base import Context, Converter, NewinstanceType
 
 
 class KeyField(Converter):
@@ -39,6 +39,6 @@
         self.model_name = model_name
         self.lookup_limit = 1 if limit_lookup else None
 
-    def odoo_to_message(self, instance: models.Model, ctx: dict | None = None) -> Any:
+    def odoo_to_message(self, instance: models.BaseModel, ctx: Context = None) -> Any:
         return getattr(instance, self.field_name)
 
@@ -43,6 +43,8 @@
         return getattr(instance, self.field_name)
 
-    def get_instance(self, odoo_env, message_data):
+    def get_instance(
+        self, odoo_env: api.Environment, message_data
+    ) -> models.BaseModel | NewinstanceType | None:
         instance = odoo_env[self.model_name].search(
             [(self.field_name, "=", message_data)], limit=self.lookup_limit
         )
@@ -50,6 +52,6 @@
             instance.ensure_one()
         return instance
 
-    @classmethod
-    def is_instance_getter(cls) -> bool:
+    @property
+    def is_instance_getter(self) -> bool:
         return True
diff --git a/list.py b/list.py
index f88452a152ee41ae12f247f0a25d3823cb13e089_bGlzdC5weQ==..2a40d04b025ac4fc4985feda4621981fcd9c2563_bGlzdC5weQ== 100644
--- a/list.py
+++ b/list.py
@@ -20,5 +20,5 @@
 
 from typing import Any
 
-from odoo import models
+from odoo import models  # type: ignore[import-untyped]
 
@@ -24,5 +24,5 @@
 
-from .base import ContextBuilder, Converter, Skip, build_context
+from .base import Context, ContextBuilder, Converter, Skip, build_context
 
 
 class List(Converter):
@@ -37,7 +37,7 @@
         self._converters = converters
         self.context = context
 
-    def odoo_to_message(self, instance: models.Model, ctx: dict | None = None) -> Any:
+    def odoo_to_message(self, instance: models.BaseModel, ctx: Context = None) -> Any:
         ctx = build_context(instance, ctx, self.context)
 
         message_data = []
@@ -54,7 +54,7 @@
         odoo_env,
         phase: str,
         message_value,
-        instance: models.Model,
+        instance: models.BaseModel,
         value_present: bool = True,
     ) -> dict:
         return {}
diff --git a/mail_template.py b/mail_template.py
index f88452a152ee41ae12f247f0a25d3823cb13e089_bWFpbF90ZW1wbGF0ZS5weQ==..2a40d04b025ac4fc4985feda4621981fcd9c2563_bWFpbF90ZW1wbGF0ZS5weQ== 100644
--- a/mail_template.py
+++ b/mail_template.py
@@ -1,7 +1,7 @@
 ##############################################################################
 #
 #    Converter Odoo module
-#    Copyright © 2020 XCG Consulting <https://xcg-consulting.fr>
+#    Copyright © 2020, 2024 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
@@ -17,7 +17,6 @@
 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
 #
 ##############################################################################
-
 import ast
 from typing import Any
 
@@ -21,5 +20,5 @@
 import ast
 from typing import Any
 
-from odoo import models
+from odoo import models  # type: ignore[import-untyped]
 
@@ -25,4 +24,4 @@
 
-from . import base
+from .base import Context, Converter
 
 
@@ -27,6 +26,6 @@
 
 
-class MailTemplate(base.Converter):
+class MailTemplate(Converter):
     """This converter wraps ``mail.template::_render_template``.
     Multiple records are allowed but ``mail.template::_render_template`` still
     runs once per record; to accomodate, we provide ``ctx["records"]``.
@@ -36,7 +35,7 @@
         self.template = template
         self.post_eval = post_eval
 
-    def odoo_to_message(self, records: models.Model, ctx: dict | None = None) -> Any:
+    def odoo_to_message(self, records: models.BaseModel, ctx: Context = None) -> Any:
         value = (
             records.env["mail.template"]
             .with_context(records=records, safe=True)
diff --git a/model.py b/model.py
index f88452a152ee41ae12f247f0a25d3823cb13e089_bW9kZWwucHk=..2a40d04b025ac4fc4985feda4621981fcd9c2563_bW9kZWwucHk= 100644
--- a/model.py
+++ b/model.py
@@ -1,7 +1,7 @@
 ##############################################################################
 #
 #    Converter Odoo module
-#    Copyright © 2020 XCG Consulting <https://xcg-consulting.fr>
+#    Copyright © 2020, 2024 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
@@ -17,10 +17,9 @@
 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
 #
 ##############################################################################
-
 import logging
 import traceback
 from collections import OrderedDict
 from collections.abc import Iterable, Mapping
 from typing import Any
 
@@ -21,10 +20,11 @@
 import logging
 import traceback
 from collections import OrderedDict
 from collections.abc import Iterable, Mapping
 from typing import Any
 
-from odoo import _, api, models
-from odoo.exceptions import UserError
+import fastjsonschema  # type: ignore[import-untyped]
+from odoo import _, api, models  # type: ignore[import-untyped]
+from odoo.exceptions import UserError  # type: ignore[import-untyped]
 
 from .base import (
@@ -29,6 +29,7 @@
 
 from .base import (
+    Context,
     ContextBuilder,
     Converter,
     Newinstance,
     NewinstanceType,
@@ -31,7 +32,8 @@
     ContextBuilder,
     Converter,
     Newinstance,
     NewinstanceType,
+    PostHookConverter,
     Skip,
     build_context,
 )
@@ -35,8 +37,8 @@
     Skip,
     build_context,
 )
-from .validate import VALIDATION_SKIP, VALIDATION_STRICT, Validator
+from .validate import NotInitialized, Validation, Validator
 
 _logger = logging.getLogger(__name__)
 
 
@@ -39,9 +41,9 @@
 
 _logger = logging.getLogger(__name__)
 
 
-class Model(Converter):
+class Model(PostHookConverter):
     """A converter that takes a dict of key, used when a message has values"""
 
     def __init__(
         self,
@@ -44,11 +46,11 @@
     """A converter that takes a dict of key, used when a message has values"""
 
     def __init__(
         self,
-        __type__: str,
+        __type__: str | None,
         converters: Mapping[str, Converter],
         json_schema: str | None = None,
         # The validator is usually not given at this point but is common
         # throughout a project. That’s why it is a property
         validator: Validator | None = None,
         merge_with: Iterable[Converter] | None = None,
@@ -49,10 +51,10 @@
         converters: Mapping[str, Converter],
         json_schema: str | None = None,
         # The validator is usually not given at this point but is common
         # throughout a project. That’s why it is a property
         validator: Validator | None = None,
         merge_with: Iterable[Converter] | None = None,
-        validation: str = VALIDATION_SKIP,
+        validation: Validation = Validation.SKIP,
         context: ContextBuilder | None = None,
     ):
         super().__init__()
@@ -56,5 +58,5 @@
         context: ContextBuilder | None = None,
     ):
         super().__init__()
-        self._type: str = __type__
+        self._type: str | None = __type__
         self._converters: Mapping[str, Converter] = converters
@@ -60,3 +62,3 @@
         self._converters: Mapping[str, Converter] = converters
-        self._post_hooks_converters_key: list[str] = []
+        self._post_hooks_converters: dict[str, PostHookConverter] = {}
         self._jsonschema: str | None = json_schema
@@ -62,5 +64,5 @@
         self._jsonschema: str | None = json_schema
-        self._get_instance: Converter = None
+        self._get_instance: str | None = None
         """First converter whose `is_instance_getter` is true if any"""
         self.merge_with: Iterable[Converter] | None = merge_with
         self.context: ContextBuilder | None = context
@@ -64,7 +66,7 @@
         """First converter whose `is_instance_getter` is true if any"""
         self.merge_with: Iterable[Converter] | None = merge_with
         self.context: ContextBuilder | None = context
-        self.validator: Validator | None = validator
+        self.validator = validator
         self.validation = validation
 
         for key, converter in converters.items():
@@ -68,5 +70,5 @@
         self.validation = validation
 
         for key, converter in converters.items():
-            if self._get_instance is None and converter.is_instance_getter():
+            if self._get_instance is None and converter.is_instance_getter:
                 self._get_instance = key
@@ -72,4 +74,4 @@
                 self._get_instance = key
-            if hasattr(converter, "post_hook"):
-                self._post_hooks_converters_key.append(key)
+            if isinstance(converter, PostHookConverter):
+                self._post_hooks_converters[key] = converter
 
@@ -75,5 +77,5 @@
 
-    def odoo_to_message(self, instance: models.Model, ctx: dict | None = None) -> Any:
+    def odoo_to_message(self, instance: models.BaseModel, ctx: Context = None) -> Any:
         ctx = build_context(instance, ctx, self.context)
 
         message_data = {}
@@ -77,7 +79,7 @@
         ctx = build_context(instance, ctx, self.context)
 
         message_data = {}
-        if self._type:
+        if self._type is not None:
             message_data["__type__"] = self._type
 
         errors = []
@@ -109,13 +111,16 @@
                     continue
                 message_data.update(value)
 
-        if self.validation != VALIDATION_SKIP and self._jsonschema is not None:
-            try:
-                self.validator.validate(self._jsonschema, message_data)
-            except Exception as exception:
-                _logger.warning("Validation failed", exc_info=1)
-                if self.validation == VALIDATION_STRICT:
-                    raise exception
+        if self.validation != Validation.SKIP and self._jsonschema is not None:
+            if self.validator:
+                try:
+                    self.validator.validate(self._jsonschema, message_data)
+                except (NotInitialized, fastjsonschema.JsonSchemaException):
+                    _logger.warning("Validation failed", exc_info=True)
+                    if self.validation == Validation.STRICT:
+                        raise
+            elif self.validation == Validation.STRICT:
+                raise Exception("Strict validation without validator")
 
         return message_data
 
@@ -124,8 +129,8 @@
         odoo_env: api.Environment,
         phase: str,
         message_value: Any,
-        instance: models.Model,
+        instance: models.BaseModel,
         value_present: bool = True,
     ) -> dict:
         values = OrderedDict()
 
@@ -128,8 +133,8 @@
         value_present: bool = True,
     ) -> dict:
         values = OrderedDict()
 
-        if self._type and message_value["__type__"] != self._type:
+        if self._type is not None and message_value["__type__"] != self._type:
             raise Exception(
                 "Expected __type__ {}, found {}".format(
                     self._type, message_value["__type__"]
@@ -157,8 +162,9 @@
 
         return values
 
+    @property
     def is_instance_getter(self) -> bool:
         return self._get_instance is not None
 
     def get_instance(
         self, odoo_env: api.Environment, message_data
@@ -160,9 +166,9 @@
     def is_instance_getter(self) -> bool:
         return self._get_instance is not None
 
     def get_instance(
         self, odoo_env: api.Environment, message_data
-    ) -> None | models.Model | NewinstanceType:
+    ) -> models.BaseModel | NewinstanceType | None:
         """:return: an instance linked to the converter, if any"""
         if self._get_instance:
             instance = self._converters[self._get_instance].get_instance(
@@ -173,6 +179,6 @@
             return instance
         return None
 
-    def post_hook(self, instance: models.Model, message_data):
-        for key in self._post_hooks_converters_key:
+    def post_hook(self, instance: models.BaseModel, message_data):
+        for key in self._post_hooks_converters:
             if key in message_data:
@@ -178,8 +184,8 @@
             if key in message_data:
-                self._converters[key].post_hook(instance, message_data[key])
+                self._post_hooks_converters[key].post_hook(instance, message_data[key])
         if self.merge_with:
             for converter in self.merge_with:
                 if hasattr(converter, "post_hook"):
                     converter.post_hook(instance, message_data)
 
     def get__type__(self) -> set[str]:
@@ -180,7 +186,7 @@
         if self.merge_with:
             for converter in self.merge_with:
                 if hasattr(converter, "post_hook"):
                     converter.post_hook(instance, message_data)
 
     def get__type__(self) -> set[str]:
-        return {self._type}
+        return set() if self._type is None else {self._type}
diff --git a/models/ir_model_data.py b/models/ir_model_data.py
index f88452a152ee41ae12f247f0a25d3823cb13e089_bW9kZWxzL2lyX21vZGVsX2RhdGEucHk=..2a40d04b025ac4fc4985feda4621981fcd9c2563_bW9kZWxzL2lyX21vZGVsX2RhdGEucHk= 100644
--- a/models/ir_model_data.py
+++ b/models/ir_model_data.py
@@ -20,9 +20,9 @@
 
 import uuid
 
-from odoo import api, models
+from odoo import api, models  # type: ignore[import-untyped]
 
 # Xrefs are stored within "ir.model.data" with this module name.
 _XREF_IMD_MODULE = "__converter__"
 
 
@@ -24,9 +24,9 @@
 
 # Xrefs are stored within "ir.model.data" with this module name.
 _XREF_IMD_MODULE = "__converter__"
 
 
-class IrModelData(models.Model):
+class IrModelData(models.BaseModel):
     """Add xref tools.
     All done with the super-admin user to bypass security rules.
 
@@ -37,5 +37,5 @@
     _inherit = "ir.model.data"
 
     @api.model
-    def generate_name(self):
+    def generate_name(self) -> str:
         """Generate an xref for odoo record;
@@ -41,7 +41,7 @@
         """Generate an xref for odoo record;
-        It return a UUID from a string of 32 hex digit
+        :return: a UUID from a string of 32 hex digit
         """
 
         return uuid.uuid4().hex
 
     @api.model
@@ -43,10 +43,12 @@
         """
 
         return uuid.uuid4().hex
 
     @api.model
-    def object_to_module_and_name(self, record_set, module=_XREF_IMD_MODULE):
+    def object_to_module_and_name(
+        self, record_set: models.BaseModel, module: str | None = _XREF_IMD_MODULE
+    ) -> tuple[str, str]:
         """Retrieve an xref pointing to the specified Odoo record; create one
         when missing.
         :param module: Name of the module to use. None to use any name, if no
         xmlid exists "" will be used as the module name.
@@ -49,9 +51,7 @@
         """Retrieve an xref pointing to the specified Odoo record; create one
         when missing.
         :param module: Name of the module to use. None to use any name, if no
         xmlid exists "" will be used as the module name.
-        :type module: Optional[str]
-        :rtype: Tuple[str, str]
         :return: tuple module and name
         """
         record_set.ensure_one()
@@ -76,8 +76,10 @@
         return module, name
 
     @api.model
-    def object_to_xmlid(self, record_set, module=_XREF_IMD_MODULE):
+    def object_to_xmlid(
+        self, record_set: models.BaseModel, module: str | None = _XREF_IMD_MODULE
+    ) -> str:
         """Retrieve an xref pointing to the specified Odoo record; create one
         when missing.
         :param module: Name of the module to use. None to use any name, if no
         xmlid exists "" will be used as the module name.
@@ -80,9 +82,7 @@
         """Retrieve an xref pointing to the specified Odoo record; create one
         when missing.
         :param module: Name of the module to use. None to use any name, if no
         xmlid exists "" will be used as the module name.
-        :type module: Optional[str]
-        :rtype: xmlid
         """
         return "{0[0]}.{0[1]}".format(
             self.object_to_module_and_name(record_set, module)
@@ -91,6 +91,6 @@
     @api.model
     def set_xmlid(
         self,
-        record_set: models.Model,
+        record_set: models.BaseModel,
         name: str,
         module: str = _XREF_IMD_MODULE,
@@ -95,6 +95,6 @@
         name: str,
         module: str = _XREF_IMD_MODULE,
-        only_when_missing: str = False,
+        only_when_missing: bool = False,
     ):
         """Save an external reference to the specified Odoo record.
         :param module: Name of the module to use.
diff --git a/py.typed b/py.typed
new file mode 100644
diff --git a/pyproject.toml b/pyproject.toml
index f88452a152ee41ae12f247f0a25d3823cb13e089_cHlwcm9qZWN0LnRvbWw=..2a40d04b025ac4fc4985feda4621981fcd9c2563_cHlwcm9qZWN0LnRvbWw= 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -19,6 +19,7 @@
 [project.optional-dependencies]
 doc = ["sphinx"]
 test = []
+typing = ["types-pytz"]
 
 [project.urls]
 repository = "https://orus.io/xcg/odoo-modules/converter"
@@ -42,6 +43,7 @@
 
 [tool.hatch.build.targets.wheel]
 include = [
+  "py.typed",
   "*.csv",
   "/i18n/",
   "/static/",
@@ -58,24 +60,6 @@
 [tool.hatch.version]
 source = "vcs"
 
-[tool.isort]
-section-order = [
-  "future",
-  "standard-library",
-  "third-party",
-  "odoo",
-  "odoo-addons",
-  "first-party",
-  "local-folder"
-]
-
-[tool.isort.sections]
-"odoo" = ["odoo"]
-"odoo-addons" = ["odoo.addons"]
-
-[tool.ruff.lint.mccabe]
-max-complexity = 16
-
 [tool.ruff]
 target-version = "py310"
 
@@ -92,3 +76,21 @@
 [tool.ruff.lint.per-file-ignores]
 "__init__.py" = ["F401", "I001"] # ignore unused and unsorted imports in __init__.py
 "__manifest__.py" = ["B018"] # useless expression
+
+[tool.ruff.lint.mccabe]
+max-complexity = 16
+
+[tool.isort]
+section-order = [
+  "future",
+  "standard-library",
+  "third-party",
+  "odoo",
+  "odoo-addons",
+  "first-party",
+  "local-folder"
+]
+
+[tool.isort.sections]
+"odoo" = ["odoo"]
+"odoo-addons" = ["odoo.addons"]
diff --git a/relation.py b/relation.py
index f88452a152ee41ae12f247f0a25d3823cb13e089_cmVsYXRpb24ucHk=..2a40d04b025ac4fc4985feda4621981fcd9c2563_cmVsYXRpb24ucHk= 100644
--- a/relation.py
+++ b/relation.py
@@ -1,7 +1,7 @@
 ##############################################################################
 #
 #    Converter Odoo module
-#    Copyright © 2020 XCG Consulting <https://xcg-consulting.fr>
+#    Copyright © 2020, 2024 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
@@ -17,8 +17,7 @@
 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
 #
 ##############################################################################
-
 import logging
 from collections.abc import Callable
 from typing import Any
 
@@ -21,6 +20,6 @@
 import logging
 from collections.abc import Callable
 from typing import Any
 
-from odoo import api, models
+from odoo import api, models  # type: ignore[import-untyped]
 
@@ -26,5 +25,12 @@
 
-from .base import ContextBuilder, Converter, Newinstance, Skip, build_context
+from .base import (
+    Context,
+    ContextBuilder,
+    Converter,
+    NewinstanceType,
+    Skip,
+    build_context,
+)
 from .field import Field
 
 _logger = logging.getLogger(__name__)
@@ -34,7 +40,7 @@
     def __init__(
         self,
         field_name: str,
-        model_name: str,
+        model_name: str | None,
         converter: Converter,
         send_empty: bool = True,
         context: ContextBuilder | None = None,
@@ -45,7 +51,7 @@
         self._send_empty = send_empty
         self.context = context
 
-    def odoo_to_message(self, instance: models.Model, ctx: dict | None = None) -> Any:
+    def odoo_to_message(self, instance: models.BaseModel, ctx: Context = None) -> Any:
         ctx = build_context(instance, ctx, self.context)
         # do not use super, otherwise if empty, will convert that
         relation_instance = getattr(instance, self.field_name)
@@ -61,7 +67,7 @@
         odoo_env: api.Environment,
         phase: str,
         message_value: Any,
-        instance: models.Model,
+        instance: models.BaseModel,
         value_present: bool = True,
     ) -> dict:
         if not value_present:
@@ -69,9 +75,11 @@
 
         post_hook = getattr(self.converter, "post_hook", None)
 
-        if self.converter.is_instance_getter():
-            rel_record = self.converter.get_instance(odoo_env, message_value)
+        if self.converter.is_instance_getter:
+            rel_record: models.BaseModel | NewinstanceType | None = (
+                self.converter.get_instance(odoo_env, message_value)
+            )
 
             if rel_record is None:
                 return {self.field_name: None}
 
@@ -74,8 +82,8 @@
 
             if rel_record is None:
                 return {self.field_name: None}
 
-            if rel_record is Newinstance:
+            if isinstance(rel_record, NewinstanceType):
                 rel_record = None
 
             updates = self.converter.message_to_odoo(
@@ -126,8 +134,8 @@
         field_name: str,
         model_name: str | None,
         converter: Converter,
-        sortkey: None | Callable[[models.Model], bool] = None,
-        filtered: None | str | Callable[[models.Model], bool] = None,
+        sortkey: None | Callable[[models.BaseModel], bool] = None,
+        filtered: None | str | Callable[[models.BaseModel], bool] = None,
         context: ContextBuilder | None = None,
         limit: Any | None = None,
     ):
@@ -142,7 +150,7 @@
         self.context = context
         self.limit = limit
 
-    def odoo_to_message(self, instance: models.Model, ctx: dict | None = None) -> Any:
+    def odoo_to_message(self, instance: models.BaseModel, ctx: Context = None) -> Any:
         ctx = build_context(instance, ctx, self.context)
         value = super().odoo_to_message(instance, ctx)
         if value is Skip:
@@ -165,7 +173,7 @@
         odoo_env: api.Environment,
         phase: str,
         message_value: Any,
-        instance: models.Model,
+        instance: models.BaseModel,
         value_present: bool = True,
     ) -> dict:
         # if not present or value is None, do not update the values.
@@ -186,7 +194,7 @@
         model_name: str | None,
         key_converter: Converter,
         value_converter: Converter,
-        filtered: None | str | Callable[[models.Model], bool] = None,
+        filtered: None | str | Callable[[models.BaseModel], bool] = None,
         context: ContextBuilder | None = None,
     ):
         """
@@ -199,7 +207,7 @@
         self.filtered = filtered
         self.context = context
 
-    def odoo_to_message(self, instance: models.Model, ctx: dict | None = None) -> Any:
+    def odoo_to_message(self, instance: models.BaseModel, ctx: Context = None) -> Any:
         ctx = build_context(instance, ctx, self.context)
         value = super().odoo_to_message(instance, ctx)
         if value is Skip:
@@ -223,7 +231,7 @@
         odoo_env: api.Environment,
         phase: str,
         message_value: Any,
-        instance: models.Model,
+        instance: models.BaseModel,
         value_present: bool = True,
     ) -> dict:
         # if not present or value is None, do not update the values.
diff --git a/switch.py b/switch.py
index f88452a152ee41ae12f247f0a25d3823cb13e089_c3dpdGNoLnB5..2a40d04b025ac4fc4985feda4621981fcd9c2563_c3dpdGNoLnB5 100644
--- a/switch.py
+++ b/switch.py
@@ -1,7 +1,7 @@
 ##############################################################################
 #
 #    Converter Odoo module
-#    Copyright © 2020 XCG Consulting <https://xcg-consulting.fr>
+#    Copyright © 2020, 2024 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
@@ -17,7 +17,6 @@
 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
 #
 ##############################################################################
-
-from collections.abc import Callable, Mapping
+from collections.abc import Callable
 from typing import Any
 
@@ -22,4 +21,4 @@
 from typing import Any
 
-from odoo import api, models
+from odoo import api, models  # type: ignore[import-untyped]
 
@@ -25,6 +24,6 @@
 
-from .base import ContextBuilder, Converter, Skip, SkipType
-from .validate import VALIDATION_SKIP, Validator
+from .base import Context, ContextBuilder, Converter, NewinstanceType, Skip, SkipType
+from .validate import Validation, Validator
 
 
 class Switch(Converter):
@@ -53,9 +52,9 @@
         self,
         converters: list[
             tuple[
-                Callable[[models.Model], bool],
+                Callable[[models.BaseModel], bool] | None,
                 Callable[[Any], bool],
                 Converter,
             ]
         ],
         validator: Validator | None = None,
@@ -57,9 +56,9 @@
                 Callable[[Any], bool],
                 Converter,
             ]
         ],
         validator: Validator | None = None,
-        validation: str = VALIDATION_SKIP,
+        validation: str = Validation.SKIP,
         context: ContextBuilder | None = None,
     ):
         """
@@ -74,9 +73,7 @@
         self.validator = validator
         self.validation = validation
 
-    def odoo_to_message(
-        self, instance: models.Model, ctx: Mapping | None = None
-    ) -> Any:
+    def odoo_to_message(self, instance: models.BaseModel, ctx: Context = None) -> Any:
         for out_cond, _in_cond, converter in self._converters:
             if out_cond is None or out_cond(instance):
                 if isinstance(converter, SkipType):
@@ -91,7 +88,7 @@
         odoo_env: api.Environment,
         phase: str,
         message_value: Any,
-        instance: models.Model,
+        instance: models.BaseModel,
         value_present: bool = True,
     ) -> dict:
         for _out_cond, in_cond, converter in self._converters:
@@ -104,5 +101,5 @@
                     value_present=value_present,
                 )
 
-        return Skip
+        return {}
 
@@ -108,3 +105,4 @@
 
+    @property
     def is_instance_getter(self) -> bool:
         for _out_cond, _in_cond, converter in self._converters:
@@ -109,7 +107,7 @@
     def is_instance_getter(self) -> bool:
         for _out_cond, _in_cond, converter in self._converters:
-            if converter.is_instance_getter():
+            if converter.is_instance_getter:
                 return True
 
         return False
 
@@ -112,6 +110,8 @@
                 return True
 
         return False
 
-    def get_instance(self, odoo_env, message_data):
+    def get_instance(
+        self, odoo_env: api.Environment, message_data
+    ) -> models.BaseModel | NewinstanceType | None:
         for _out_cond, in_cond, converter in self._converters:
@@ -117,5 +117,5 @@
         for _out_cond, in_cond, converter in self._converters:
-            if converter.is_instance_getter() and (
+            if converter.is_instance_getter and (
                 in_cond is None or in_cond(message_data)
             ):
                 return converter.get_instance(odoo_env, message_data)
@@ -119,4 +119,5 @@
                 in_cond is None or in_cond(message_data)
             ):
                 return converter.get_instance(odoo_env, message_data)
+        return super().get_instance(odoo_env, message_data)
 
@@ -122,4 +123,3 @@
 
-    @Converter.validator.setter
-    def validator(self, value: Validator | None) -> None:
+    def _set_validator(self, value: Validator | None) -> None:
         # also set validator on any converters in our switch, in case they care
@@ -125,5 +125,5 @@
         # also set validator on any converters in our switch, in case they care
-        Converter.validator.fset(self, value)
+        super()._set_validator(value)
         for _out_cond, _in_cond, converter in self._converters:
             converter.validator = value
 
@@ -127,6 +127,5 @@
         for _out_cond, _in_cond, converter in self._converters:
             converter.validator = value
 
-    @Converter.validation.setter
-    def validation(self, value: str) -> None:
+    def _set_validation(self, value: str) -> None:
         # also set validation on any converters in our switch
@@ -132,5 +131,5 @@
         # also set validation on any converters in our switch
-        Converter.validation.fset(self, value)
+        super()._set_validation(value)
         for _out_cond, _in_cond, converter in self._converters:
             converter.validation = value
 
diff --git a/tests/test_base.py b/tests/test_base.py
index f88452a152ee41ae12f247f0a25d3823cb13e089_dGVzdHMvdGVzdF9iYXNlLnB5..2a40d04b025ac4fc4985feda4621981fcd9c2563_dGVzdHMvdGVzdF9iYXNlLnB5 100644
--- a/tests/test_base.py
+++ b/tests/test_base.py
@@ -1,7 +1,7 @@
 ##############################################################################
 #
 #    Converter Odoo module
-#    Copyright © 2020 XCG Consulting <https://xcg-consulting.fr>
+#    Copyright © 2020, 2024 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
@@ -17,4 +17,6 @@
 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
 #
 ##############################################################################
+from odoo import tests  # type: ignore[import-untyped]
+from odoo.exceptions import UserError  # type: ignore[import-untyped]
 
@@ -20,6 +22,5 @@
 
-from odoo import tests
-from odoo.addons.converter import (
+from .. import (
     Constant,
     Field,
     Model,
@@ -27,7 +28,6 @@
     message_to_odoo,
     relation,
 )
-from odoo.exceptions import UserError
 
 
 class Test(tests.TransactionCase):
@@ -43,8 +43,8 @@
         self.assertEqual("a", converter.odoo_to_message(self.env["res.users"]))
 
     def test_unset_field(self):
-        # this tests when a relationnal substitution
-        # is substitued on a record that has not
+        # this tests when a relational substitution
+        # is substituted on a record that has not
         # the relation set
 
         self.assertTrue(self.active_user.create_uid)
@@ -60,7 +60,7 @@
             Field("name"),
         )
         converter = Model(
-            None,
+            "res.users",
             {
                 "user_creator_name": rel,
             },
diff --git a/tests/test_converters.py b/tests/test_converters.py
index f88452a152ee41ae12f247f0a25d3823cb13e089_dGVzdHMvdGVzdF9jb252ZXJ0ZXJzLnB5..2a40d04b025ac4fc4985feda4621981fcd9c2563_dGVzdHMvdGVzdF9jb252ZXJ0ZXJzLnB5 100644
--- a/tests/test_converters.py
+++ b/tests/test_converters.py
@@ -18,7 +18,7 @@
 #
 ##############################################################################
 
-from odoo import tests
+from odoo import tests  # type: ignore[import-untyped]
 
 from ..base import Skip
 from ..field import Field
diff --git a/tests/test_field.py b/tests/test_field.py
index f88452a152ee41ae12f247f0a25d3823cb13e089_dGVzdHMvdGVzdF9maWVsZC5weQ==..2a40d04b025ac4fc4985feda4621981fcd9c2563_dGVzdHMvdGVzdF9maWVsZC5weQ== 100644
--- a/tests/test_field.py
+++ b/tests/test_field.py
@@ -20,8 +20,9 @@
 
 from typing import Any
 
-from odoo import tests
-from odoo.addons.converter import Field, TranslatedSelection
+from odoo import tests  # type: ignore[import-untyped]
+
+from .. import Field, TranslatedSelection
 
 
 class Test(tests.TransactionCase):
diff --git a/tests/test_ir_model.py b/tests/test_ir_model.py
index f88452a152ee41ae12f247f0a25d3823cb13e089_dGVzdHMvdGVzdF9pcl9tb2RlbC5weQ==..2a40d04b025ac4fc4985feda4621981fcd9c2563_dGVzdHMvdGVzdF9pcl9tb2RlbC5weQ== 100644
--- a/tests/test_ir_model.py
+++ b/tests/test_ir_model.py
@@ -18,7 +18,7 @@
 #
 ##############################################################################
 
-from odoo.tests import TransactionCase, tagged
+from odoo.tests import TransactionCase, tagged  # type: ignore[import-untyped]
 
 
 @tagged("post_install", "-at_install")
diff --git a/tests/test_mail_template.py b/tests/test_mail_template.py
index f88452a152ee41ae12f247f0a25d3823cb13e089_dGVzdHMvdGVzdF9tYWlsX3RlbXBsYXRlLnB5..2a40d04b025ac4fc4985feda4621981fcd9c2563_dGVzdHMvdGVzdF9tYWlsX3RlbXBsYXRlLnB5 100644
--- a/tests/test_mail_template.py
+++ b/tests/test_mail_template.py
@@ -18,8 +18,9 @@
 #
 ##############################################################################
 
-from odoo import tests
-from odoo.addons.converter import MailTemplate
+from odoo import tests  # type: ignore[import-untyped]
+
+from .. import MailTemplate
 
 
 class Test(tests.TransactionCase):
diff --git a/tests/test_relation.py b/tests/test_relation.py
index f88452a152ee41ae12f247f0a25d3823cb13e089_dGVzdHMvdGVzdF9yZWxhdGlvbi5weQ==..2a40d04b025ac4fc4985feda4621981fcd9c2563_dGVzdHMvdGVzdF9yZWxhdGlvbi5weQ== 100644
--- a/tests/test_relation.py
+++ b/tests/test_relation.py
@@ -1,7 +1,7 @@
 ##############################################################################
 #
 #    Converter Odoo module
-#    Copyright © 2021 XCG Consulting <https://xcg-consulting.fr>
+#    Copyright © 2021, 2024 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
@@ -17,4 +17,5 @@
 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
 #
 ##############################################################################
+from typing import Any
 
@@ -20,6 +21,7 @@
 
-from odoo import tests
-from odoo.addons.converter import (
+from odoo import tests  # type: ignore[import-untyped]
+
+from .. import (
     Field,
     Model,
     RelationToOne,
@@ -78,7 +80,7 @@
                     "partner_id",
                     "res.partner",
                     Model(
-                        "",
+                        "partner",
                         {
                             "color": Field("color"),
                             "name": Field("name"),
@@ -94,5 +96,5 @@
         old_partner = user.partner_id
 
         # Run our message reception.
-        message = {
+        message: dict[str, Any] = {
             "partner": {
@@ -98,4 +100,5 @@
             "partner": {
+                "__type__": "partner",
                 "color": 2,
                 "name": "TEST",
                 "xref": "new_partner_converter",
@@ -104,7 +107,7 @@
         }
         message_to_odoo(self.env, message, self.env["res.users"], converter)
 
-        # Ensure a new partner got created and that is has an xref (post hook).
+        # Ensure a new partner got created and that it has an xref (post hook).
         new_partner = self.env.ref("test.new_partner_converter")
         self.assertEqual(user.partner_id, new_partner)
         self.assertNotEqual(new_partner, old_partner)
diff --git a/tests/test_switch.py b/tests/test_switch.py
index f88452a152ee41ae12f247f0a25d3823cb13e089_dGVzdHMvdGVzdF9zd2l0Y2gucHk=..2a40d04b025ac4fc4985feda4621981fcd9c2563_dGVzdHMvdGVzdF9zd2l0Y2gucHk= 100644
--- a/tests/test_switch.py
+++ b/tests/test_switch.py
@@ -18,7 +18,7 @@
 #
 ##############################################################################
 
-from odoo import tests
+from odoo import tests  # type: ignore[import-untyped]
 
 from .. import Field, Model, Switch
 
diff --git a/validate.py b/validate.py
index f88452a152ee41ae12f247f0a25d3823cb13e089_dmFsaWRhdGUucHk=..2a40d04b025ac4fc4985feda4621981fcd9c2563_dmFsaWRhdGUucHk= 100644
--- a/validate.py
+++ b/validate.py
@@ -1,7 +1,7 @@
 ##############################################################################
 #
 #    Converter Odoo module
-#    Copyright © 2020 XCG Consulting <https://xcg-consulting.fr>
+#    Copyright © 2020, 2024 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
@@ -17,6 +17,5 @@
 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
 #
 ##############################################################################
-
 import json
 import os
@@ -21,3 +20,5 @@
 import json
 import os
+from enum import Enum
+from typing import Any, LiteralString
 
@@ -23,4 +24,5 @@
 
-import fastjsonschema
-import odoo.addons
+import fastjsonschema  # type: ignore[import-untyped]
+import odoo.addons  # type: ignore[import-untyped]
+
 
@@ -26,7 +28,10 @@
 
-VALIDATION_SKIP = "skip"
-VALIDATION_SOFT = "soft"
-VALIDATION_STRICT = "strict"
+class Validation(str, Enum):
+    """Type of validation"""
+
+    SKIP = "skip"
+    SOFT = "soft"
+    STRICT = "strict"
 
 
 class NotInitialized(Exception):
@@ -44,8 +49,8 @@
         self.repository = repository
         # exemple "https://annonces-legales.fr/xbus/schemas/v1/{}.schema.json"
         self.default_url_pattern = default_url_pattern
-        self.validators = {}
+        self.validators: dict[LiteralString, Any] = {}
         self.initialized = False
         self.encoding = "UTF-8"
 
     def initialize(self) -> None:
@@ -48,7 +53,8 @@
         self.initialized = False
         self.encoding = "UTF-8"
 
     def initialize(self) -> None:
+        # TODO Not working if module is installed compressed
         repo_module_basepath = os.path.dirname(
             getattr(odoo.addons, self.repository_module_name).__file__
         )
@@ -77,7 +83,7 @@
             )
         self.initialized = True
 
-    def validate(self, schema_id, payload) -> None:
+    def validate(self, schema_id: str, payload) -> None:
         if not self.initialized:
             raise NotInitialized("please call the initialize() method")
 
diff --git a/xref.py b/xref.py
index f88452a152ee41ae12f247f0a25d3823cb13e089_eHJlZi5weQ==..2a40d04b025ac4fc4985feda4621981fcd9c2563_eHJlZi5weQ== 100644
--- a/xref.py
+++ b/xref.py
@@ -20,5 +20,5 @@
 
 from typing import Any
 
-from odoo import models
+from odoo import models  # type: ignore[import-untyped]
 
@@ -24,6 +24,6 @@
 
-from .base import Converter, NewinstanceType
+from .base import Context, NewinstanceType, PostHookConverter
 from .models.ir_model_data import _XREF_IMD_MODULE
 
 
 # TODO dans quel cas ça ne pourrait pas être un instance getter???
@@ -26,9 +26,9 @@
 from .models.ir_model_data import _XREF_IMD_MODULE
 
 
 # TODO dans quel cas ça ne pourrait pas être un instance getter???
-class Xref(Converter):
+class Xref(PostHookConverter):
     """This converter represents an external reference, using the standard xmlid with a
     custom module name.
     """
 
@@ -31,9 +31,11 @@
     """This converter represents an external reference, using the standard xmlid with a
     custom module name.
     """
 
-    def __init__(self, module: str = _XREF_IMD_MODULE, is_instance_getter: bool = True):
+    def __init__(
+        self, module: str | None = _XREF_IMD_MODULE, is_instance_getter: bool = True
+    ):
         super().__init__()
         self._module = module
         self._is_instance_getter = is_instance_getter
 
@@ -36,8 +38,8 @@
         super().__init__()
         self._module = module
         self._is_instance_getter = is_instance_getter
 
-    def odoo_to_message(self, instance: models.Model, ctx: dict | None = None) -> Any:
+    def odoo_to_message(self, instance: models.BaseModel, ctx: Context = None) -> Any:
         if not instance:
             return ""
         return instance.env["ir.model.data"].object_to_xmlid(
@@ -46,8 +48,11 @@
 
     def get_instance(
         self, odoo_env, message_data
-    ) -> None | models.Model | NewinstanceType:
-        return odoo_env.ref(
-            ".".join([self._module, message_data]), raise_if_not_found=False
-        )
+    ) -> None | models.BaseModel | NewinstanceType:
+        if self._is_instance_getter:
+            return odoo_env.ref(
+                ".".join(["" if self._module is None else self._module, message_data]),
+                raise_if_not_found=False,
+            )
+        return None
 
@@ -53,7 +58,7 @@
 
-    def post_hook(self, instance: models.Model, message_data):
+    def post_hook(self, instance: models.BaseModel, message_data):
         # add xmlid to the newly created object
         instance.env["ir.model.data"].set_xmlid(
             instance, message_data, module=self._module, only_when_missing=True
         )
 
@@ -55,7 +60,8 @@
         # add xmlid to the newly created object
         instance.env["ir.model.data"].set_xmlid(
             instance, message_data, module=self._module, only_when_missing=True
         )
 
+    @property
     def is_instance_getter(self) -> bool:
         return self._is_instance_getter