Skip to content

Commit ee22756

Browse files
committed
feat: add quick to_dict() method to all objects
1 parent 5915b3e commit ee22756

7 files changed

Lines changed: 192 additions & 0 deletions

File tree

SKILL.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,18 @@ print(player.hexdump())
290290

291291
Struct hexdumps annotate lines with field names. Primitive hexdumps show raw bytes.
292292

293+
### Dict / JSON Export
294+
295+
```python
296+
point = point_t.from_bytes(memory)
297+
point.to_dict() # {"x": 10, "y": 20}
298+
299+
import json
300+
json.dumps(entity.to_dict()) # nested structs produce nested dicts
301+
```
302+
303+
`to_dict()` works on all types: primitives return their value, structs return `{name: value}` dicts, arrays return lists, unions return variant values, enums return their int value.
304+
293305
### Freeze / Diff / Reset
294306

295307
```python

docs/basics/structs.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,31 @@ print(repr(header))
104104
# }
105105
```
106106

107+
## Dict / JSON Export
108+
109+
Use `to_dict()` to get a JSON-serializable dictionary of field names to values:
110+
111+
```python
112+
header = header_t.from_bytes(data)
113+
print(header.to_dict())
114+
# {"magic": 3735928559, "version": 1, "size": 4096}
115+
```
116+
117+
Nested structs produce nested dicts, arrays become lists:
118+
119+
```python
120+
import json
121+
122+
rect = rect_t.from_bytes(data)
123+
print(json.dumps(rect.to_dict(), indent=2))
124+
# {
125+
# "origin": {"x": 0, "y": 0},
126+
# "size": {"x": 0, "y": 0}
127+
# }
128+
```
129+
130+
`to_dict()` also works on individual fields — primitives return their Python value, enums return their integer value.
131+
107132
## Equality
108133

109134
Two struct instances are equal if they have the same members with the same values:

libdestruct/common/array/array_impl.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,10 @@ def set(self: array_impl, _: list[obj]) -> None:
6060
"""Set the array from a list."""
6161
raise NotImplementedError("Cannot set items in an array.")
6262

63+
def to_dict(self: array_impl) -> list[object]:
64+
"""Return a JSON-serializable list of element values."""
65+
return [elem.to_dict() for elem in self]
66+
6367
def to_bytes(self: array_impl) -> bytes:
6468
"""Return the serialized representation of the array."""
6569
return b"".join(bytes(x) for x in self)

libdestruct/common/obj.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,10 @@ def __ge__(self: obj, other: object) -> bool:
181181
return NotImplemented
182182
return pair[0] >= pair[1]
183183

184+
def to_dict(self: obj) -> object:
185+
"""Return a JSON-serializable representation of the object."""
186+
return self.value
187+
184188
def hexdump(self: obj) -> str:
185189
"""Return a hex dump of this object's bytes."""
186190
address = self.address if not self._frozen else 0

libdestruct/common/struct/struct_impl.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,10 @@ def to_bytes(self: struct_impl) -> bytes:
211211
"""Return the serialized representation of the struct."""
212212
return b"".join(member.to_bytes() for member in self._members.values())
213213

214+
def to_dict(self: struct_impl) -> dict[str, object]:
215+
"""Return a JSON-serializable dict of field names to values."""
216+
return {name: member.to_dict() for name, member in self._members.items()}
217+
214218
def hexdump(self: struct_impl) -> str:
215219
"""Return a hex dump of this struct's bytes with field annotations."""
216220
annotations = {}

libdestruct/common/union/union.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,14 @@ def _set(self: union, value: object) -> None:
6666
raise RuntimeError("Cannot set the value of a union without an active variant.")
6767
self._variant._set(value)
6868

69+
def to_dict(self: union) -> object:
70+
"""Return a JSON-serializable representation of the union."""
71+
if self._variant is not None:
72+
return self._variant.to_dict()
73+
if self._variants:
74+
return {name: v.to_dict() for name, v in self._variants.items()}
75+
return None
76+
6977
def to_bytes(self: union) -> bytes:
7078
"""Return the full union-sized region as bytes."""
7179
if self._frozen_bytes is not None:

test/scripts/to_dict_test.py

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
#
2+
# This file is part of libdestruct (https://github.com/mrindeciso/libdestruct).
3+
# Copyright (c) 2026 Roberto Alessandro Bertolini. All rights reserved.
4+
# Licensed under the MIT license. See LICENSE file in the project root for details.
5+
#
6+
7+
import json
8+
import struct as pystruct
9+
import unittest
10+
from enum import IntEnum
11+
12+
from libdestruct import (
13+
array_of,
14+
c_float,
15+
c_int,
16+
c_long,
17+
c_str,
18+
enum_of,
19+
inflater,
20+
ptr_to,
21+
struct,
22+
)
23+
from libdestruct.common.enum import enum
24+
from libdestruct.common.union import tagged_union, union, union_of
25+
26+
27+
class ToDictTest(unittest.TestCase):
28+
def test_primitive_to_dict(self):
29+
"""Primitive to_dict returns its Python value."""
30+
x = c_int.from_bytes(b"\x2a\x00\x00\x00")
31+
self.assertEqual(x.to_dict(), 42)
32+
33+
def test_struct_to_dict(self):
34+
"""Struct to_dict returns a dict of field names to values."""
35+
class point_t(struct):
36+
x: c_int
37+
y: c_int
38+
39+
memory = pystruct.pack("<ii", 10, 20)
40+
point = point_t.from_bytes(memory)
41+
result = point.to_dict()
42+
self.assertEqual(result, {"x": 10, "y": 20})
43+
44+
def test_nested_struct_to_dict(self):
45+
"""Nested struct produces nested dicts."""
46+
class vec2(struct):
47+
x: c_int
48+
y: c_int
49+
50+
class entity_t(struct):
51+
id: c_int
52+
pos: vec2
53+
54+
memory = pystruct.pack("<iii", 1, 10, 20)
55+
entity = entity_t.from_bytes(memory)
56+
result = entity.to_dict()
57+
self.assertEqual(result, {"id": 1, "pos": {"x": 10, "y": 20}})
58+
59+
def test_struct_with_ptr_to_dict(self):
60+
"""Pointer field returns its address as int."""
61+
class data_t(struct):
62+
value: c_int
63+
ref: c_long
64+
65+
memory = pystruct.pack("<iq", 42, 0x1000)
66+
data = data_t.from_bytes(memory)
67+
result = data.to_dict()
68+
self.assertEqual(result, {"value": 42, "ref": 0x1000})
69+
70+
def test_struct_with_enum_to_dict(self):
71+
"""Enum field returns its integer value."""
72+
class Color(IntEnum):
73+
RED = 0
74+
GREEN = 1
75+
BLUE = 2
76+
77+
class pixel_t(struct):
78+
color: enum = enum_of(Color)
79+
alpha: c_int
80+
81+
memory = pystruct.pack("<ii", 1, 255)
82+
pixel = pixel_t.from_bytes(memory)
83+
result = pixel.to_dict()
84+
self.assertEqual(result["color"], 1)
85+
self.assertEqual(result["alpha"], 255)
86+
87+
def test_struct_with_array_to_dict(self):
88+
"""Array field returns a list of values."""
89+
class packet_t(struct):
90+
data: c_int = array_of(c_int, 3)
91+
92+
memory = pystruct.pack("<iii", 10, 20, 30)
93+
pkt = packet_t.from_bytes(memory)
94+
result = pkt.to_dict()
95+
self.assertEqual(result, {"data": [10, 20, 30]})
96+
97+
def test_struct_with_tagged_union_to_dict(self):
98+
"""Tagged union returns the active variant's value."""
99+
class msg_t(struct):
100+
type: c_int
101+
payload: union = tagged_union("type", {0: c_int, 1: c_float})
102+
103+
memory = pystruct.pack("<ii", 0, 42)
104+
msg = msg_t.from_bytes(memory)
105+
result = msg.to_dict()
106+
self.assertEqual(result["type"], 0)
107+
self.assertEqual(result["payload"], 42)
108+
109+
def test_struct_with_plain_union_to_dict(self):
110+
"""Plain union returns a dict of all variant values."""
111+
class packet_t(struct):
112+
data: union = union_of({"i": c_int, "f": c_float})
113+
114+
memory = pystruct.pack("<i", 42)
115+
pkt = packet_t.from_bytes(memory)
116+
result = pkt.to_dict()
117+
self.assertIn("i", result["data"])
118+
self.assertIn("f", result["data"])
119+
self.assertEqual(result["data"]["i"], 42)
120+
121+
def test_to_dict_is_json_serializable(self):
122+
"""to_dict output can be passed to json.dumps."""
123+
class point_t(struct):
124+
x: c_int
125+
y: c_int
126+
127+
memory = pystruct.pack("<ii", 10, 20)
128+
point = point_t.from_bytes(memory)
129+
result = json.dumps(point.to_dict())
130+
self.assertEqual(json.loads(result), {"x": 10, "y": 20})
131+
132+
def test_float_to_dict(self):
133+
"""Float to_dict returns its Python float value."""
134+
f = c_float.from_bytes(pystruct.pack("<f", 3.14))
135+
self.assertAlmostEqual(f.to_dict(), 3.14, places=2)

0 commit comments

Comments
 (0)