0
# Custom Object Serialization
1
2
Framework for creating custom BSON-serializable objects through the BSONCoding abstract base class. This system enables automatic serialization and deserialization of custom Python objects with class registration and metadata preservation.
3
4
## Capabilities
5
6
### BSONCoding Abstract Base Class
7
8
Abstract base class that defines the interface for custom BSON-serializable objects, requiring implementation of encoding and initialization methods.
9
10
```python { .api }
11
from abc import ABCMeta, abstractmethod
12
13
class BSONCoding:
14
__metaclass__ = ABCMeta
15
16
@abstractmethod
17
def bson_encode(self):
18
"""
19
Serialize object state to dictionary for BSON encoding.
20
21
Returns:
22
dict: Dictionary representation of object state
23
"""
24
25
@abstractmethod
26
def bson_init(self, raw_values):
27
"""
28
Initialize object from deserialized BSON data.
29
30
Parameters:
31
- raw_values: dict, deserialized data including class metadata
32
33
Returns:
34
object: Initialized object instance (usually self) or alternative object
35
"""
36
```
37
38
Usage example:
39
40
```python
41
from bson.codec import BSONCoding
42
import bson
43
44
class Person(BSONCoding):
45
def __init__(self, name, age, email=None):
46
self.name = name
47
self.age = age
48
self.email = email
49
50
def bson_encode(self):
51
"""Convert to dict for BSON serialization"""
52
data = {
53
"name": self.name,
54
"age": self.age
55
}
56
if self.email:
57
data["email"] = self.email
58
return data
59
60
def bson_init(self, raw_values):
61
"""Initialize from BSON data"""
62
self.name = raw_values["name"]
63
self.age = raw_values["age"]
64
self.email = raw_values.get("email")
65
return self
66
67
def __repr__(self):
68
return f"Person(name='{self.name}', age={self.age}, email='{self.email}')"
69
70
# Register class for deserialization
71
bson.import_class(Person)
72
73
# Create and serialize
74
person = Person("Alice", 30, "alice@example.com")
75
bson_data = bson.dumps(person)
76
77
# Deserialize back to object
78
restored_person = bson.loads(bson_data)
79
print(type(restored_person)) # <class '__main__.Person'>
80
print(restored_person) # Person(name='Alice', age=30, email='alice@example.com')
81
```
82
83
### Class Registration Functions
84
85
Functions for registering BSONCoding subclasses to enable automatic deserialization by class name.
86
87
```python { .api }
88
def import_class(cls):
89
"""
90
Register a BSONCoding subclass for deserialization.
91
92
Parameters:
93
- cls: BSONCoding subclass to register
94
95
Note: Only registers classes that inherit from BSONCoding
96
"""
97
98
def import_classes(*args):
99
"""
100
Register multiple BSONCoding subclasses.
101
102
Parameters:
103
- args: Variable number of BSONCoding subclasses
104
"""
105
106
def import_classes_from_modules(*args):
107
"""
108
Register all BSONCoding subclasses from modules.
109
110
Parameters:
111
- args: Variable number of module objects to scan for BSONCoding classes
112
"""
113
```
114
115
Usage example:
116
117
```python
118
from bson.codec import BSONCoding, import_class, import_classes, import_classes_from_modules
119
import bson
120
121
class User(BSONCoding):
122
def __init__(self, username, role="user"):
123
self.username = username
124
self.role = role
125
126
def bson_encode(self):
127
return {"username": self.username, "role": self.role}
128
129
def bson_init(self, raw_values):
130
self.username = raw_values["username"]
131
self.role = raw_values.get("role", "user")
132
return self
133
134
class Product(BSONCoding):
135
def __init__(self, name, price):
136
self.name = name
137
self.price = price
138
139
def bson_encode(self):
140
return {"name": self.name, "price": self.price}
141
142
def bson_init(self, raw_values):
143
self.name = raw_values["name"]
144
self.price = raw_values["price"]
145
return self
146
147
# Register individual classes
148
import_class(User)
149
bson.import_class(Product) # Alternative syntax
150
151
# Register multiple classes at once
152
# import_classes(User, Product)
153
154
# Register all BSONCoding classes from a module
155
# import bson.import_classes_from_modules(my_models_module)
156
157
# Test serialization
158
user = User("alice", "admin")
159
product = Product("Laptop", 999.99)
160
161
data = {
162
"user": user,
163
"product": product,
164
"timestamp": "2023-01-01"
165
}
166
167
bson_data = bson.dumps(data)
168
restored = bson.loads(bson_data)
169
170
print(type(restored["user"])) # <class '__main__.User'>
171
print(type(restored["product"])) # <class '__main__.Product'>
172
```
173
174
### Advanced BSONCoding Patterns
175
176
#### Alternative Object Return
177
178
The `bson_init` method can return a different object instance:
179
180
```python
181
from bson.codec import BSONCoding
182
import bson
183
184
class Singleton(BSONCoding):
185
_instance = None
186
187
def __new__(cls, *args, **kwargs):
188
if cls._instance is None:
189
cls._instance = super().__new__(cls)
190
return cls._instance
191
192
def __init__(self, name="default"):
193
if not hasattr(self, 'initialized'):
194
self.name = name
195
self.initialized = True
196
197
def bson_encode(self):
198
return {"name": self.name}
199
200
def bson_init(self, raw_values):
201
# Return the singleton instance instead of self
202
instance = Singleton(raw_values["name"])
203
return instance
204
205
bson.import_class(Singleton)
206
207
# Test singleton behavior through BSON
208
obj1 = Singleton("test")
209
bson_data = bson.dumps(obj1)
210
obj2 = bson.loads(bson_data)
211
212
print(obj1 is obj2) # True (same instance)
213
```
214
215
#### Nested Custom Objects
216
217
Custom objects can contain other custom objects:
218
219
```python
220
from bson.codec import BSONCoding
221
import bson
222
223
class Address(BSONCoding):
224
def __init__(self, street, city, country):
225
self.street = street
226
self.city = city
227
self.country = country
228
229
def bson_encode(self):
230
return {
231
"street": self.street,
232
"city": self.city,
233
"country": self.country
234
}
235
236
def bson_init(self, raw_values):
237
self.street = raw_values["street"]
238
self.city = raw_values["city"]
239
self.country = raw_values["country"]
240
return self
241
242
class Customer(BSONCoding):
243
def __init__(self, name, address, orders=None):
244
self.name = name
245
self.address = address # Address object
246
self.orders = orders or []
247
248
def bson_encode(self):
249
return {
250
"name": self.name,
251
"address": self.address, # Will be recursively encoded
252
"orders": self.orders
253
}
254
255
def bson_init(self, raw_values):
256
self.name = raw_values["name"]
257
self.address = raw_values["address"] # Already deserialized as Address
258
self.orders = raw_values.get("orders", [])
259
return self
260
261
# Register both classes
262
bson.import_classes(Address, Customer)
263
264
# Create nested objects
265
address = Address("123 Main St", "Anytown", "USA")
266
customer = Customer("John Doe", address, ["order1", "order2"])
267
268
# Serialize and deserialize
269
bson_data = bson.dumps(customer)
270
restored = bson.loads(bson_data)
271
272
print(type(restored)) # <class '__main__.Customer'>
273
print(type(restored.address)) # <class '__main__.Address'>
274
print(restored.address.city) # "Anytown"
275
```
276
277
### Object Encoding and Decoding Process
278
279
#### Encoding Process
280
281
When a BSONCoding object is serialized:
282
283
1. `dumps()` detects BSONCoding instance
284
2. Calls `obj.bson_encode()` to get dictionary representation
285
3. Adds special `"$$__CLASS_NAME__$$"` field with class name
286
4. Encodes resulting dictionary as BSON document
287
288
```python
289
# Internal encoding process example
290
class MyClass(BSONCoding):
291
def __init__(self, value):
292
self.value = value
293
294
def bson_encode(self):
295
return {"value": self.value}
296
297
def bson_init(self, raw_values):
298
self.value = raw_values["value"]
299
return self
300
301
obj = MyClass(42)
302
303
# When dumps(obj) is called:
304
# 1. obj.bson_encode() returns {"value": 42}
305
# 2. Class name added: {"value": 42, "$$__CLASS_NAME__$$": "MyClass"}
306
# 3. Dictionary encoded as BSON
307
```
308
309
#### Decoding Process
310
311
When a BSON document with class metadata is deserialized:
312
313
1. `loads()` detects `"$$__CLASS_NAME__$$"` field in dictionary
314
2. Looks up registered class by name
315
3. Creates empty instance with special `_EmptyClass` technique
316
4. Calls `instance.bson_init(raw_values)` to initialize
317
5. Returns initialized object
318
319
```python
320
# Internal decoding process
321
# 1. BSON decoded to: {"value": 42, "$$__CLASS_NAME__$$": "MyClass"}
322
# 2. Class lookup finds MyClass in registry
323
# 3. Empty instance created and class changed to MyClass
324
# 4. instance.bson_init({"value": 42, "$$__CLASS_NAME__$$": "MyClass"}) called
325
# 5. Initialized MyClass instance returned
326
```
327
328
## Error Handling
329
330
### Missing Class Registration
331
332
```python { .api }
333
class MissingClassDefinition(ValueError):
334
"""Raised when trying to deserialize unknown class"""
335
def __init__(self, class_name): ...
336
```
337
338
Occurs when BSON data contains class name not registered with `import_class()`:
339
340
```python
341
from bson.codec import BSONCoding, MissingClassDefinition
342
import bson
343
344
class UnregisteredClass(BSONCoding):
345
def bson_encode(self):
346
return {"data": "test"}
347
348
def bson_init(self, raw_values):
349
return self
350
351
# Serialize without registering
352
obj = UnregisteredClass()
353
bson_data = bson.dumps(obj)
354
355
# Attempting to deserialize fails
356
try:
357
restored = bson.loads(bson_data)
358
except MissingClassDefinition as e:
359
print(f"Class not registered: {e}")
360
361
# Solution: register the class
362
bson.import_class(UnregisteredClass)
363
restored = bson.loads(bson_data) # Now works
364
```
365
366
### BSONCoding Interface Violations
367
368
Classes must properly implement the abstract methods:
369
370
```python
371
from bson.codec import BSONCoding
372
373
# Incorrect - missing abstract method implementations
374
class BadClass(BSONCoding):
375
pass
376
377
try:
378
obj = BadClass() # Raises TypeError
379
except TypeError as e:
380
print(f"Abstract method error: {e}")
381
382
# Correct - implement both abstract methods
383
class GoodClass(BSONCoding):
384
def bson_encode(self):
385
return {}
386
387
def bson_init(self, raw_values):
388
return self
389
```
390
391
## Best Practices
392
393
### Serialization Design
394
395
- Include only essential data in `bson_encode()` return value
396
- Handle optional fields gracefully in `bson_init()`
397
- Consider version compatibility when changing object structure
398
- Use meaningful field names that won't conflict with BSON metadata
399
400
### Class Registration
401
402
- Register classes immediately after definition or in module initialization
403
- Use `import_classes_from_modules()` for automatic registration
404
- Be careful with class name conflicts in different modules
405
406
### Performance Considerations
407
408
- BSONCoding objects add overhead compared to plain dictionaries
409
- Nested custom objects multiply serialization cost
410
- Consider using regular dictionaries for simple data structures