Skip to main content

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): Returns True if 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:

TypeTag KeySerialization Method
dict diHandled specially to avoid collisions (see below).
tuple tConverted to a JSON list.
bytes bBase64 encoded string.
Markup mThe HTML string representation.
UUID uThe hex string representation.
datetime dAn 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).