Tagged JSON Serialization
The TaggedJSONSerializer system in Flask provides a mechanism for lossless serialization of Python objects into a JSON-compatible format. While standard JSON is limited to basic types like strings, numbers, and lists, Flask sessions often require storing more complex types such as datetime objects, UUIDs, or bytes.
This system is primarily used by the SecureCookieSessionInterface in src/flask/sessions.py to ensure that session data can be safely stored in a browser cookie and perfectly reconstructed on the next request.
The Tagging Mechanism
The core idea behind the system is "tagging": representing a non-standard type as a single-key JSON object where the key indicates the type. For example, a UUID object is transformed into a dictionary like {" u": "..."}.
The TaggedJSONSerializer class (found in src/flask/json/tag.py) manages a collection of JSONTag objects. When serializing a value, it iterates through these tags in a specific order, using the first one that claims the value.
The JSONTag Base Class
Every supported type is defined by a subclass of JSONTag. A tag implementation must define:
key: A short string (usually starting with a space to avoid common key collisions) used to identify the type in JSON.check(value): ReturnsTrueif this tag can handle the given Python object.to_json(value): Converts the Python object into a JSON-serializable format (e.g., a string or a list).to_python(value): Reverses the process, converting the JSON-serializable format back into the original Python type.
For example, the TagUUID implementation handles uuid.UUID objects:
class TagUUID(JSONTag):
__slots__ = ()
key = " u"
def check(self, value: t.Any) -> bool:
return isinstance(value, UUID)
def to_json(self, value: t.Any) -> t.Any:
return value.hex
def to_python(self, value: t.Any) -> t.Any:
return UUID(value)
Default Supported Types
The TaggedJSONSerializer includes several built-in tags by default:
| Type | Tag Key | Serialization Method |
|---|---|---|
dict | di | Handled specially to avoid collisions (see below). |
tuple | t | Converted to a JSON list. |
bytes | b | Base64 encoded string. |
Markup | m | The HTML string representation. |
UUID | u | The hex string representation. |
datetime | d | An HTTP-formatted date string (via werkzeug.http.http_date). |
Handling Key Collisions
A potential issue with this tagging system is ambiguity: what if a user-provided dictionary happens to have a single key that matches a registered tag, such as {" u": "some-string"}?
To solve this, the system includes TagDict. If a dictionary has exactly one key and that key matches a registered tag, TagDict "escapes" it by appending __ to the key during serialization.
# In src/flask/json/tag.py
class TagDict(JSONTag):
key = " di"
def check(self, value: t.Any) -> bool:
return (
isinstance(value, dict)
and len(value) == 1
and next(iter(value)) in self.serializer.tags
)
def to_json(self, value: t.Any) -> t.Any:
key = next(iter(value))
return {f"{key}__": self.serializer.tag(value[key])}
When deserializing, the untag method detects the di tag and removes the __ suffix, restoring the original dictionary.
Extensibility and Registration Order
The order in which tags are checked is critical. For instance, OrderedDict is a subclass of dict. If the standard dict tag is checked first, it will claim the OrderedDict and lose the ordering information.
To extend the serializer, you can register custom tags using the register method. The index parameter allows you to insert a tag at a specific position in the check order.
from flask.json.tag import JSONTag
from collections import OrderedDict
class TagOrderedDict(JSONTag):
key = ' od'
def check(self, value):
return isinstance(value, OrderedDict)
def to_json(self, value):
return [[k, self.serializer.tag(v)] for k, v in value.items()]
def to_python(self, value):
return OrderedDict(value)
# Register at index 0 so it is checked before the standard dict tag
app.session_interface.serializer.register(TagOrderedDict, index=0)
Integration with Sessions
In src/flask/sessions.py, Flask instantiates a global session_json_serializer and assigns it to the SecureCookieSessionInterface.
session_json_serializer = TaggedJSONSerializer()
class SecureCookieSessionInterface(SessionInterface):
serializer = session_json_serializer
# ...
When save_session is called, this serializer is passed to itsdangerous.URLSafeTimedSerializer. The TaggedJSONSerializer handles the transformation of complex types into a JSON string, which itsdangerous then signs and encodes into the final session cookie. This layered approach ensures that Flask sessions are both secure (signed) and expressive (supporting complex Python types).