Skip to content

Commit 4f21357

Browse files
authored
Add optional orjson serializer support (#152)
1 parent 7e76f8d commit 4f21357

File tree

6 files changed

+99
-33
lines changed

6 files changed

+99
-33
lines changed

docs/sphinx/serializers.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ Serializers
99
.. autoclass:: JsonSerializer
1010
:members:
1111

12+
.. autoclass:: OrjsonSerializer
13+
:members:
14+
1215
.. autoclass:: TextSerializer
1316
:members:
1417

elastic_transport/__init__.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,13 @@
9999
"Urllib3HttpNode",
100100
]
101101

102+
try:
103+
from elastic_transport._serializer import OrjsonSerializer # noqa: F401
104+
105+
__all__.append("OrjsonSerializer")
106+
except ModuleNotFoundError:
107+
pass
108+
102109
_logger = logging.getLogger("elastic_transport")
103110
_logger.addHandler(logging.NullHandler())
104111
del _logger

elastic_transport/_serializer.py

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,15 @@
2424

2525
from ._exceptions import SerializationError
2626

27+
try:
28+
import orjson
29+
except ModuleNotFoundError:
30+
orjson = None # type: ignore[assignment]
31+
2732

2833
class Serializer:
34+
"""Serializer interface."""
35+
2936
mimetype: ClassVar[str]
3037

3138
def loads(self, data: bytes) -> Any: # pragma: nocover
@@ -36,6 +43,8 @@ def dumps(self, data: Any) -> bytes: # pragma: nocover
3643

3744

3845
class TextSerializer(Serializer):
46+
"""Text serializer to and from UTF-8."""
47+
3948
mimetype: ClassVar[str] = "text/*"
4049

4150
def loads(self, data: bytes) -> str:
@@ -62,6 +71,8 @@ def dumps(self, data: str) -> bytes:
6271

6372

6473
class JsonSerializer(Serializer):
74+
"""JSON serializer relying on the standard library json module."""
75+
6576
mimetype: ClassVar[str] = "application/json"
6677

6778
def default(self, data: Any) -> Any:
@@ -81,14 +92,15 @@ def json_dumps(self, data: Any) -> bytes:
8192
).encode("utf-8", "surrogatepass")
8293

8394
def json_loads(self, data: bytes) -> Any:
95+
return json.loads(data)
96+
97+
def loads(self, data: bytes) -> Any:
8498
# Sometimes responses use Content-Type: json but actually
8599
# don't contain any data. We should return something instead
86100
# of erroring in these cases.
87101
if data == b"":
88102
return None
89-
return json.loads(data)
90103

91-
def loads(self, data: bytes) -> Any:
92104
try:
93105
return self.json_loads(data)
94106
except (ValueError, TypeError) as e:
@@ -115,7 +127,26 @@ def dumps(self, data: Any) -> bytes:
115127
)
116128

117129

130+
if orjson is not None:
131+
132+
class OrjsonSerializer(JsonSerializer):
133+
"""JSON serializer relying on the orjson package.
134+
135+
Only available if orjson if installed. It is faster, especially for vectors, but is also stricter.
136+
"""
137+
138+
def json_dumps(self, data: Any) -> bytes:
139+
return orjson.dumps(
140+
data, default=self.default, option=orjson.OPT_SERIALIZE_NUMPY
141+
)
142+
143+
def json_loads(self, data: bytes) -> Any:
144+
return orjson.loads(data)
145+
146+
118147
class NdjsonSerializer(JsonSerializer):
148+
"""Newline delimited JSON (NDJSON) serializer relying on the standard library json module."""
149+
119150
mimetype: ClassVar[str] = "application/x-ndjson"
120151

121152
def loads(self, data: bytes) -> Any:

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@
7070
"respx",
7171
"opentelemetry-api",
7272
"opentelemetry-sdk",
73+
"orjson",
7374
# Override Read the Docs default (sphinx<2)
7475
"sphinx>2",
7576
"furo",

tests/test_package.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,11 @@
2525

2626
@modules
2727
def test__all__sorted(module):
28-
print(sorted(module.__all__))
29-
assert module.__all__ == sorted(module.__all__)
28+
module_all = module.__all__.copy()
29+
# Optional dependencies are added at the end
30+
if "OrjsonSerializer" in module_all:
31+
module_all.remove("OrjsonSerializer")
32+
assert module_all == sorted(module_all)
3033

3134

3235
@modules

tests/test_serializer.py

Lines changed: 50 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
from elastic_transport import (
2525
JsonSerializer,
2626
NdjsonSerializer,
27+
OrjsonSerializer,
2728
SerializationError,
2829
SerializerCollection,
2930
TextSerializer,
@@ -33,66 +34,86 @@
3334
serializers = SerializerCollection(DEFAULT_SERIALIZERS)
3435

3536

36-
def test_date_serialization():
37-
assert b'{"d":"2010-10-01"}' == JsonSerializer().dumps({"d": date(2010, 10, 1)})
37+
@pytest.fixture(params=[JsonSerializer, OrjsonSerializer])
38+
def json_serializer(request: pytest.FixtureRequest):
39+
yield request.param()
3840

3941

40-
def test_decimal_serialization():
41-
assert b'{"d":3.8}' == JsonSerializer().dumps({"d": Decimal("3.8")})
42+
def test_date_serialization(json_serializer):
43+
assert b'{"d":"2010-10-01"}' == json_serializer.dumps({"d": date(2010, 10, 1)})
4244

4345

44-
def test_uuid_serialization():
45-
assert b'{"d":"00000000-0000-0000-0000-000000000003"}' == JsonSerializer().dumps(
46+
def test_decimal_serialization(json_serializer):
47+
assert b'{"d":3.8}' == json_serializer.dumps({"d": Decimal("3.8")})
48+
49+
50+
def test_uuid_serialization(json_serializer):
51+
assert b'{"d":"00000000-0000-0000-0000-000000000003"}' == json_serializer.dumps(
4652
{"d": uuid.UUID("00000000-0000-0000-0000-000000000003")}
4753
)
4854

4955

5056
def test_serializes_nan():
5157
assert b'{"d":NaN}' == JsonSerializer().dumps({"d": float("NaN")})
58+
# NaN is invalid JSON, and orjson silently converts it to null
59+
assert b'{"d":null}' == OrjsonSerializer().dumps({"d": float("NaN")})
5260

5361

54-
def test_raises_serialization_error_on_dump_error():
62+
def test_raises_serialization_error_on_dump_error(json_serializer):
5563
with pytest.raises(SerializationError):
56-
JsonSerializer().dumps(object())
64+
json_serializer.dumps(object())
5765
with pytest.raises(SerializationError):
5866
TextSerializer().dumps({})
5967

6068

61-
def test_raises_serialization_error_on_load_error():
69+
def test_raises_serialization_error_on_load_error(json_serializer):
6270
with pytest.raises(SerializationError):
63-
JsonSerializer().loads(object())
71+
json_serializer.loads(object())
6472
with pytest.raises(SerializationError):
65-
JsonSerializer().loads(b"{{")
73+
json_serializer.loads(b"{{")
6674

6775

68-
def test_unicode_is_handled():
69-
j = JsonSerializer()
76+
def test_json_unicode_is_handled(json_serializer):
7077
assert (
71-
j.dumps({"你好": "你好"})
78+
json_serializer.dumps({"你好": "你好"})
7279
== b'{"\xe4\xbd\xa0\xe5\xa5\xbd":"\xe4\xbd\xa0\xe5\xa5\xbd"}'
7380
)
74-
assert j.loads(b'{"\xe4\xbd\xa0\xe5\xa5\xbd":"\xe4\xbd\xa0\xe5\xa5\xbd"}') == {
75-
"你好": "你好"
76-
}
81+
assert json_serializer.loads(
82+
b'{"\xe4\xbd\xa0\xe5\xa5\xbd":"\xe4\xbd\xa0\xe5\xa5\xbd"}'
83+
) == {"你好": "你好"}
84+
7785

78-
t = TextSerializer()
79-
assert t.dumps("你好") == b"\xe4\xbd\xa0\xe5\xa5\xbd"
80-
assert t.loads(b"\xe4\xbd\xa0\xe5\xa5\xbd") == "你好"
86+
def test_text_unicode_is_handled():
87+
text_serializer = TextSerializer()
88+
assert text_serializer.dumps("你好") == b"\xe4\xbd\xa0\xe5\xa5\xbd"
89+
assert text_serializer.loads(b"\xe4\xbd\xa0\xe5\xa5\xbd") == "你好"
8190

8291

83-
def test_unicode_surrogates_handled():
84-
j = JsonSerializer()
92+
def test_json_unicode_surrogates_handled():
8593
assert (
86-
j.dumps({"key": "你好\uda6a"})
94+
JsonSerializer().dumps({"key": "你好\uda6a"})
8795
== b'{"key":"\xe4\xbd\xa0\xe5\xa5\xbd\xed\xa9\xaa"}'
8896
)
89-
assert j.loads(b'{"key":"\xe4\xbd\xa0\xe5\xa5\xbd\xed\xa9\xaa"}') == {
90-
"key": "你好\uda6a"
91-
}
97+
assert JsonSerializer().loads(
98+
b'{"key":"\xe4\xbd\xa0\xe5\xa5\xbd\xed\xa9\xaa"}'
99+
) == {"key": "你好\uda6a"}
100+
101+
# orjson is strict about UTF-8
102+
with pytest.raises(SerializationError):
103+
OrjsonSerializer().dumps({"key": "你好\uda6a"})
104+
105+
with pytest.raises(SerializationError):
106+
OrjsonSerializer().loads(b'{"key":"\xe4\xbd\xa0\xe5\xa5\xbd\xed\xa9\xaa"}')
107+
92108

93-
t = TextSerializer()
94-
assert t.dumps("你好\uda6a") == b"\xe4\xbd\xa0\xe5\xa5\xbd\xed\xa9\xaa"
95-
assert t.loads(b"\xe4\xbd\xa0\xe5\xa5\xbd\xed\xa9\xaa") == "你好\uda6a"
109+
def test_text_unicode_surrogates_handled(json_serializer):
110+
text_serializer = TextSerializer()
111+
assert (
112+
text_serializer.dumps("你好\uda6a") == b"\xe4\xbd\xa0\xe5\xa5\xbd\xed\xa9\xaa"
113+
)
114+
assert (
115+
text_serializer.loads(b"\xe4\xbd\xa0\xe5\xa5\xbd\xed\xa9\xaa") == "你好\uda6a"
116+
)
96117

97118

98119
def test_deserializes_json_by_default():

0 commit comments

Comments
 (0)