You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Structs with nested array_like Structs asdict #830
Hey folks, I went over #594 because of the same assumptions.
I was expecting asdict to work on nested Structs, at least as an option.
This is where I'm struggling:
import msgspec
class Dog(msgspec.Struct, array_like=True):
name: str
age: int
class Owner(msgspec.Struct):
name: str
dogs: list[Dog]
owner = Owner('A dog owner', [Dog('Doggie', 2), Dog('Max', 1)])
msgspec.structs.asdict(owner)
>> {'name': 'A dog owner', 'dogs': [Dog(name='Doggie', age=2), Dog(name='Max', age=1)]}
msgspec.to_builtins(owner)
>> {'name': 'A dog owner', 'dogs': [['Doggie', 2], ['Max', 1]]}
I need to come up with the following:
>>> {'name': 'A dog owner', 'dogs': [{'name': 'Doggie', 'age': 2}, {'name': 'Max', 'age': 1}]}
I tried working around the issue with the following, but most of the performance gains vanish.
def asdict(obj):
if isinstance(obj, msgspec.Struct):
return asdict(msgspec.structs.asdict(obj))
if isinstance(obj, dict):
return {key: asdict(value) for key, value in obj.items()}
if isinstance(obj, list):
return [asdict(item) for item in obj]
return obj
Is there any plan to support the nested structs this way in asdict in the short term? or can you guys think of a good workaround? Thank you
The behavior is intentional: msgspec.structs.asdict is shallow by design (#299 and #297). The two intended paths are:
msgspec.structs.asdict / astuple - one-to-one conversion of a single struct instance, no recursion, no encoder settings applied. Modeled after dataclasses.asdict / dataclasses.astuple but, unlike those, doesn't descend into nested values.
msgspec.to_builtins - the encoding pipeline, recurses into nested structs and applies all encoder settings, including array_like.
Your case sits in between: you want recursion (like to_builtins) but not array_like (like asdict). msgspec doesn't expose a third mode for that. With array_like=True on Dog, to_builtins(owner) will always emit lists for the dogs - that's the trade-off array_like opts you into across the entire encoder surface.
asdict_recursive(owner)
# {'name': 'A dog owner', 'dogs': [{'name': 'Doggie', 'age': 2}, {'name': 'Max', 'age': 1}]}
This is pure Python and noticeably slower than the C path (in a quick local bench it's ~7x slower than to_builtins(owner)), but for one-off conversions outside hot loops it's usually fine.
Closing as not planned since adding an ignore_array_like (or similar) flag conflicts with the deliberate split between asdict and to_builtins. If you have a use case where the recursive helper isn't sufficient, happy to reopen the discussion.
Activity
Expand list of differences between asdict/astuple and to_builtins
Siyet commentedon May 26, 2026
Apologies for the long silence on this.
The behavior is intentional:
msgspec.structs.asdictis shallow by design (#299 and #297). The two intended paths are:msgspec.structs.asdict/astuple- one-to-one conversion of a single struct instance, no recursion, no encoder settings applied. Modeled afterdataclasses.asdict/dataclasses.astuplebut, unlike those, doesn't descend into nested values.msgspec.to_builtins- the encoding pipeline, recurses into nested structs and applies all encoder settings, includingarray_like.Your case sits in between: you want recursion (like
to_builtins) but notarray_like(likeasdict). msgspec doesn't expose a third mode for that. Witharray_like=TrueonDog,to_builtins(owner)will always emit lists for the dogs - that's the trade-offarray_likeopts you into across the entire encoder surface.The new docs section in #1025 spells the split out explicitly: Converting to and from Builtin Types. Hopefully that makes the boundary easier to find next time.
For the dict-with-nested-dicts output, the cleanest workaround is a small recursive helper that walks
__struct_fields__:For your example:
This is pure Python and noticeably slower than the C path (in a quick local bench it's ~7x slower than
to_builtins(owner)), but for one-off conversions outside hot loops it's usually fine.Closing as not planned since adding an
ignore_array_like(or similar) flag conflicts with the deliberate split betweenasdictandto_builtins. If you have a use case where the recursive helper isn't sufficient, happy to reopen the discussion.