Skip to content

Commit b7f32e2

Browse files
committed
Add max_depth argument to control recursion without explicitly declaring relationships to ignore
1 parent 8f8a4b9 commit b7f32e2

2 files changed

Lines changed: 62 additions & 2 deletions

File tree

README.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,42 @@ item.to_dict('-children.children')
187187
In this case only the first level of `children` will be included
188188
See [Max recursion](#Max-recursion)
189189

190+
Alternatively you can pass the `max_depth` kwarg, which allows to control recursion in general without having to explicitly list each attribute you don't want recursed.
191+
Please note that this check is performed at the beginning of `serialize_model()`, which means every other nested Iterable is *not affected* by this setting.
192+
The default value returned when recursion is no longer permitted is a string in the form `tablename.id` (e.g. `users.123`), with fallback to `repr(item)` in case the `id` attribute is not available for the model.
193+
This option can be disabled using `max_depth=-1`, which is also the default.
194+
```python
195+
# Stop recursion at top level, meaning all relationships will be "dropped"
196+
# For example:
197+
# item = User(id=123, name="john", articles=[Article(id=10), Article(id=11)])
198+
# will be serialized to
199+
# {
200+
# "id": 123, "name": "john",
201+
# "articles": ["articles.10", "articles.11"]
202+
# }
203+
item.to_dict(max_depth=0)
204+
205+
# Stop at first level, meaning only the first children will be processed
206+
# For example, the same user above will be serialized to:
207+
# {
208+
# "id": 123, "name": "john",
209+
# "articles": [
210+
# {"id": 10, "title": ...},
211+
# {"id": 11, "title": ...},
212+
# ]
213+
# }
214+
item.to_dict(max_depth=1)
215+
```
216+
The `max_depth` option can be also configured at mixin level, setting the `serialize_max_depth` class attribute which avoids passing it to each `to_dict` call.
217+
```python
218+
# Set max_depth option in mixin
219+
class SomeModel(db.Model, SerializerMixin):
220+
serialize_max_depth = 0
221+
222+
# Call to_dict without max_depth arg
223+
item.to_dict()
224+
```
225+
190226
# Custom formats
191227
If you want to change datetime/date/time/decimal format in one model you can specify it like below:
192228
```python

sqlalchemy_serializer/serializer.py

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121

2222
Options = namedtuple(
2323
"Options",
24-
"date_format datetime_format time_format decimal_format tzinfo serialize_types",
24+
"date_format datetime_format time_format decimal_format tzinfo serialize_types max_depth",
2525
)
2626

2727

@@ -197,7 +197,12 @@ def serialize_dict(self, value: dict) -> dict:
197197
logger.debug("Skip key:%s of dict", k)
198198
return res
199199

200-
def serialize_model(self, value) -> dict:
200+
def serialize_model(self, value) -> t.Any:
201+
# Check if user set a recursion limit and we can no longer recurse
202+
if self.serialization_depth > self.opts.max_depth > -1:
203+
# Return early calling internal helper
204+
return self.serialize_model_fallback(value)
205+
201206
self.schema.update(only=value.serialize_only, extend=value.serialize_rules)
202207

203208
res = {}
@@ -220,6 +225,19 @@ def serialize_model(self, value) -> dict:
220225
logger.debug("Skip key:%s of model:%s", k, get_type(value))
221226
return res
222227

228+
def serialize_model_fallback(self, value) -> t.Any:
229+
"""
230+
Called when serialization can no longer recurse.
231+
Default output is a string like "tablename.id", e.g. "users.123", if attributes
232+
are not available it will return "repr(value)".
233+
Override this method to implement own logic.
234+
:return: str
235+
"""
236+
try:
237+
return f'{value.__tablename__}.{value.id}'
238+
except AttributeError:
239+
return repr(value)
240+
223241

224242
def serialize_collection(iterable: t.Iterable, *args, **kwargs) -> list:
225243
return [item.to_dict(*args, **kwargs) for item in iterable]
@@ -254,6 +272,9 @@ class SerializerMixin:
254272
time_format = "%H:%M"
255273
decimal_format = "{}"
256274

275+
# Maximum recursion depth, setting to "-1" disables logic
276+
serialize_max_depth: int = -1
277+
257278
# Serialize fields of the model defined as @property automatically
258279
auto_serialize_properties: bool = False
259280

@@ -277,6 +298,7 @@ def to_dict(
277298
tzinfo=None,
278299
decimal_format=None,
279300
serialize_types=None,
301+
max_depth=None
280302
):
281303
"""
282304
Returns SQLAlchemy model's data in JSON compatible format
@@ -293,6 +315,7 @@ def to_dict(
293315
:param decimal_format: str
294316
:param serialize_types:
295317
:param tzinfo: datetime.tzinfo converts datetimes to local user timezone
318+
:param max_depth: int maximum recursion depth allowed, "-1" disables logic
296319
:return: data: dict
297320
"""
298321
s = self.serialize_class(
@@ -302,5 +325,6 @@ def to_dict(
302325
decimal_format=decimal_format or self.decimal_format,
303326
tzinfo=tzinfo or self.get_tzinfo(),
304327
serialize_types=serialize_types or self.serialize_types,
328+
max_depth=max_depth if max_depth is not None else self.serialize_max_depth,
305329
)
306330
return s(self, only=only, extend=rules)

0 commit comments

Comments
 (0)