untag without `object_hook`
diff --git a/CHANGES.rst b/CHANGES.rst
index 9f79995b46..27c04f7f3e 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -5,6 +5,9 @@ Unreleased
 
 -   Correct type for ``path`` argument to ``send_file``. :issue:`5230`
 -   Fix a typo in an error message for the ``flask run --key`` option. :pr:`5344`
+-   Session data is untagged without relying on the built-in ``json.loads``
+    ``object_hook``. This allows other JSON providers that don't implement that.
+    :issue:`5381`
 
 
 Version 3.0.0
diff --git a/src/flask/json/tag.py b/src/flask/json/tag.py
index 91cc4412d6..069739f264 100644
--- a/src/flask/json/tag.py
+++ b/src/flask/json/tag.py
@@ -305,10 +305,22 @@ def untag(self, value: dict[str, t.Any]) -> t.Any:
 
         return self.tags[key].to_python(value[key])
 
+    def _untag_scan(self, value: t.Any) -> t.Any:
+        if isinstance(value, dict):
+            # untag each item recursively
+            value = {k: self._untag_scan(v) for k, v in value.items()}
+            # untag the dict itself
+            value = self.untag(value)
+        elif isinstance(value, list):
+            # untag each item recursively
+            value = [self._untag_scan(item) for item in value]
+
+        return value
+
     def dumps(self, value: t.Any) -> str:
         """Tag the value and dump it to a compact JSON string."""
         return dumps(self.tag(value), separators=(",", ":"))
 
     def loads(self, value: str) -> t.Any:
         """Load data from a JSON string and deserialized any tagged objects."""
-        return loads(value, object_hook=self.untag)
+        return self._untag_scan(loads(value))

Load session JSON without using `object_hook`, then recursively untag the data.

fixes #5381