Getting started#
This tutorial walks you through installing zodb-json-codec, converting Python pickles to JSON and back, and understanding the marker format that preserves type fidelity.
Prerequisites#
Python 3.12 or later
Install zodb-json-codec#
Install the package from PyPI:
pip install zodb-json-codec
Verify the installation:
>>> import zodb_json_codec
Your first encode and decode#
The codec provides two pairs of functions for standalone pickle conversion:
pickle_to_json()/json_to_pickle()– work with JSON stringspickle_to_dict()/dict_to_pickle()– work with Python dicts directly
Let’s start with a simple dictionary.
Pickle to JSON string#
import pickle
import json
import zodb_json_codec
# Create a pickle using protocol 3 (the protocol ZODB uses)
data = {"name": "Alice", "age": 30, "active": True}
pickled = pickle.dumps(data, protocol=3)
# Decode to a JSON string
json_str = zodb_json_codec.pickle_to_json(pickled)
print(json_str)
Output:
{
"active": true,
"age": 30,
"name": "Alice"
}
Simple Python types – strings, integers, floats, booleans, None, lists, and
string-keyed dicts – map directly to their JSON equivalents with no markers.
Pickle to Python dict#
The pickle_to_dict() function returns a Python dict instead of a JSON string.
This is faster when you need to work with the data in Python:
result = zodb_json_codec.pickle_to_dict(pickled)
print(result)
# {'active': true, 'age': 30, 'name': 'Alice'}
Understanding type markers#
Python has types that JSON does not: tuples, bytes, sets, datetimes, and more. The codec uses single-key marker dicts to preserve these types through the roundtrip.
Tuples use @t#
val = (1, "two", 3.0)
pickled = pickle.dumps(val, protocol=3)
json_str = zodb_json_codec.pickle_to_json(pickled)
print(json_str)
Output:
{
"@t": [1, "two", 3.0]
}
The @t marker wraps the tuple elements in a JSON array.
Without it, there
would be no way to distinguish a tuple from a list after roundtripping.
Bytes use @b#
Binary data is base64-encoded:
val = b"\x00\x01\x02\xff"
pickled = pickle.dumps(val, protocol=3)
json_str = zodb_json_codec.pickle_to_json(pickled)
print(json_str)
Output:
{
"@b": "AAEC/w=="
}
Sets use @set and @fset#
val = {1, 2, 3}
pickled = pickle.dumps(val, protocol=3)
result = zodb_json_codec.pickle_to_dict(pickled)
print(result)
# {'@set': [1, 2, 3]}
Frozensets use the @fset marker:
val = frozenset(["a", "b"])
pickled = pickle.dumps(val, protocol=3)
result = zodb_json_codec.pickle_to_dict(pickled)
print(result)
# {'@fset': ['a', 'b']}
Datetimes use @dt, @date, @time#
The codec recognizes common Python types serialized via pickle’s REDUCE opcode and produces human-readable, JSONB-queryable markers:
from datetime import datetime, date, time
# datetime
val = datetime(2025, 6, 15, 12, 30, 45)
pickled = pickle.dumps(val, protocol=3)
result = zodb_json_codec.pickle_to_dict(pickled)
print(result)
# {'@dt': '2025-06-15T12:30:45'}
# date
val = date(2025, 6, 15)
pickled = pickle.dumps(val, protocol=3)
result = zodb_json_codec.pickle_to_dict(pickled)
print(result)
# {'@date': '2025-06-15'}
# time
val = time(12, 30, 45)
pickled = pickle.dumps(val, protocol=3)
result = zodb_json_codec.pickle_to_dict(pickled)
print(result)
# {'@time': '12:30:45'}
Other known types include @td (timedelta), @dec (Decimal), and @uuid
(UUID). See the type mapping reference for the
complete list.
Dicts with non-string keys use @d#
JSON objects only support string keys. When a Python dict has non-string keys, the codec uses an array-of-pairs representation:
val = {1: "a", 2: "b"}
pickled = pickle.dumps(val, protocol=3)
result = zodb_json_codec.pickle_to_dict(pickled)
print(result)
# {'@d': [[1, 'a'], [2, 'b']]}
Roundtrip verification#
A key property of the codec is round-trip fidelity: encoding to JSON and back yields a pickle that unpickles to an equal object.
import pickle
import zodb_json_codec
# Start with a value that exercises multiple markers
original = {
"name": "Alice",
"scores": (95, 87, 92),
"avatar": b"\x89PNG\r\n",
"active": True,
"tags": ["staff", "admin"],
}
pickled = pickle.dumps(original, protocol=3)
# Roundtrip via JSON string
json_str = zodb_json_codec.pickle_to_json(pickled)
restored_pickle = zodb_json_codec.json_to_pickle(json_str)
assert pickle.loads(restored_pickle) == original
# Roundtrip via Python dict
as_dict = zodb_json_codec.pickle_to_dict(pickled)
restored_pickle2 = zodb_json_codec.dict_to_pickle(as_dict)
assert pickle.loads(restored_pickle2) == original
Both paths – JSON string and Python dict – give back a pickle that unpickles
to an object equal to the original. The bytes are not the input bytes: memo
puts for values that are never read back are dropped, the protocol header is
rewritten (json_to_pickle writes protocol 3, dict_to_pickle and the record
functions protocol 2), pickle_to_json sorts dict keys (the record functions
keep the order), shared sub-objects come back as equal copies, and a ZODB
class pickle comes back in its ((module, name), None) form. That is enough
to transcode ZODB data to JSON for storage and querying and give ZODB a pickle
it loads identically when it needs the object back. The test suite runs this
example.
Nested structures#
Markers compose naturally inside larger structures:
val = {
"items": [(1, "a"), (2, "b")],
"metadata": {"created": b"\x01\x02"},
}
pickled = pickle.dumps(val, protocol=3)
json_str = zodb_json_codec.pickle_to_json(pickled)
print(json_str)
Output:
{
"items": [
{"@t": [1, "a"]},
{"@t": [2, "b"]}
],
"metadata": {
"created": {"@b": "AQI="}
}
}
Tuples inside a list each get their own @t marker, and bytes inside a nested
dict get a @b marker.
The structure is human-readable and fully queryable
when stored as PostgreSQL JSONB.
What’s next#
Now that you understand the basics, move on to Working with ZODB Records to learn how to decode and encode the two-pickle format that ZODB uses for persistent object storage.