Tagged JSON Serialization
Standard JSON serialization is lossy for many Python types. If you store a uuid.UUID or a datetime.datetime in a Flask session, a standard JSON serializer would either fail or convert them to strings, losing the original type information when the session is loaded back. Flask solves this with a tagged serialization system that compactly represents these types in JSON strings.
Core Architecture
The system is built around two main classes in flask.json.tag:
JSONTag: The base class for defining how a specific Python type is converted to and from a JSON-compatible format.TaggedJSONSerializer: The coordinator that manages a collection of tags and handles the recursive scanning of data structures (like nested dicts and lists) to apply those tags.
Flask uses this system by default in its SecureCookieSessionInterface to ensure that session data remains rich and type-accurate across requests.
Built-in Supported Types
Flask includes several built-in tags for common types that standard JSON cannot handle natively. Each tag uses a specific key (usually starting with a space to avoid collisions) to identify the type in the resulting JSON object.
| Python Type | Tag Key | Internal Representation |
|---|---|---|
tuple | t | A JSON list of tagged items. |
bytes | b | A base64-encoded string. |
Markup | m | The HTML string from __html__. |
UUID | u | The hex string of the UUID. |
datetime | d | An HTTP-formatted date string. |
How Tagging Works
When you call dumps() on a TaggedJSONSerializer, it iterates through its registered tags in order. For each value, it calls tag.check(value). If a match is found, it wraps the JSON-compatible version of that value in a dictionary keyed by the tag's identifier.
from flask.json.tag import TaggedJSONSerializer
from uuid import uuid4
serializer = TaggedJSONSerializer()
u = uuid4()
print(serializer.dumps(u))
# Output: {" u":"..."} (where ... is the hex UUID)
Internally, TaggedJSONSerializer.tag handles the transformation:
def tag(self, value: t.Any) -> t.Any:
for tag in self.order:
if tag.check(value):
return tag.tag(value)
return value
The tag() method on the JSONTag class (defined in src/flask/json/tag.py) creates the dictionary structure:
def tag(self, value: t.Any) -> dict[str, t.Any]:
return {self.key: self.to_json(value)}
Handling Key Collisions
Because the serializer uses dictionaries with specific keys (like {" u": ...}) to represent types, a conflict occurs if your data already contains a 1-item dictionary with that exact key. For example, if you try to store {" u": "some string"} in the session, the serializer might mistake it for a tagged UUID during deserialization.
Flask handles this using TagDict. If a dictionary has exactly one key and that key matches a registered tag, TagDict appends __ to the key to "escape" it.
data = {" u": "not a uuid"}
serialized = serializer.dumps(data)
# Result: {" di":{" u__": "not a uuid"}}
When loads() is called, the untag method recognizes the di key and the __ suffix, stripping them to restore the original dictionary.
Implementing Custom Tags
You can extend the serializer to support additional types by subclassing JSONTag and registering it with the serializer. This is useful for types like OrderedDict or custom domain objects.
When registering a tag, the order is critical. More specific types must be checked before more general types. For example, an OrderedDict is an instance of dict, so it must be registered at the beginning of the list so it is caught before the standard PassDict tag.
from collections import OrderedDict
from flask.json.tag import JSONTag
class TagOrderedDict(JSONTag):
__slots__ = ('serializer',)
key = ' od'
def check(self, value):
return isinstance(value, OrderedDict)
def to_json(self, value):
# Recursively tag values within the OrderedDict
return [[k, self.serializer.tag(v)] for k, v in value.items()]
def to_python(self, value):
return OrderedDict(value)
# Register at the front (index 0) so it's checked before the standard 'dict' tag
serializer = TaggedJSONSerializer()
serializer.register(TagOrderedDict, index=0)
In a standard Flask application, you can access the session serializer through the app's session interface:
app.session_interface.serializer.register(TagOrderedDict, index=0)
Recursive Processing
The TaggedJSONSerializer does not just tag the top-level object; it performs a recursive scan. The PassDict and PassList tags are internal helpers that do not add a tag key themselves but instead trigger the serializer to descend into the contents of dictionaries and lists.
The _untag_scan method (in src/flask/json/tag.py) performs the inverse during loading:
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
This ensures that a UUID nested deep inside a list of dictionaries is correctly restored to a UUID object upon deserialization.