Skip to content

Commit ad4d0d1

Browse files
committed
[simplejson,ultrajson] Implement formatters for simplejson and ultrajson (ujson)
1 parent 06165bb commit ad4d0d1

7 files changed

Lines changed: 235 additions & 8 deletions

File tree

docs/changelog.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
88

99
### Added
1010
- Add support for Python 3.14, PyPy 3.11
11+
- Add support for [simplejson](https://github.com/simplejson/simplejson)
12+
- Add support for [ultrajson](https://github.com/ultrajson/ultrajson/tree/main?tab=readme-ov-file#project-status) (`ujson`)
1113

1214
## [4.0.0](https://github.com/nhairs/python-json-logger/compare/v3.3.3...v4.0.0) - 2025-10-06
1315

pylintrc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
# A comma-separated list of package or module names from where C extensions may
44
# be loaded. Extensions are loading into the active Python interpreter and may
55
# run arbitrary code.
6-
extension-pkg-whitelist=orjson
6+
extension-pkg-whitelist=orjson,ujson
77

88
# Add files or directories to the blacklist. They should be base names, not
99
# paths.

pyproject.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,11 @@ GitHub = "https://github.com/nhairs/python-json-logger"
4747
[project.optional-dependencies]
4848
dev = [
4949
## Optional but required for dev
50-
"orjson;implementation_name!='pypy'",
5150
"msgspec;implementation_name!='pypy'",
51+
"orjson;implementation_name!='pypy'",
52+
"simplejson",
53+
"types-simplejson",
54+
"ujson",
5255
## Lint
5356
"validate-pyproject[all]",
5457
"black",

src/pythonjsonlogger/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,7 @@
1313

1414
### CONSTANTS
1515
### ============================================================================
16-
ORJSON_AVAILABLE = utils.package_is_available("orjson")
1716
MSGSPEC_AVAILABLE = utils.package_is_available("msgspec")
17+
ORJSON_AVAILABLE = utils.package_is_available("orjson")
18+
SIMPLEJSON_AVAILABLE = utils.package_is_available("simplejson")
19+
ULTRAJSON_AVAILABLE = utils.package_is_available("ujson")

src/pythonjsonlogger/simplejson.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
"""JSON Formatter using [simplejson](https://github.com/simplejson/simplejson/tree/master)"""
2+
3+
### IMPORTS
4+
### ============================================================================
5+
## Future
6+
from __future__ import annotations
7+
8+
## Standard Library
9+
from typing import Any, Optional, Union, Callable
10+
11+
## Installed
12+
13+
## Application
14+
from . import core
15+
from . import defaults as d
16+
from .utils import package_is_available
17+
18+
# We import simplejson after checking it is available
19+
package_is_available("simplejson", throw_error=True)
20+
import simplejson # pylint: disable=wrong-import-position,wrong-import-order
21+
22+
23+
### FUNCTIONS
24+
### ============================================================================
25+
def simplejson_default(obj: Any) -> Any:
26+
"""simplejson default encoder function for non-standard types"""
27+
if d.use_exception_default(obj):
28+
return d.exception_default(obj)
29+
30+
if d.use_traceback_default(obj):
31+
return d.traceback_default(obj)
32+
33+
if d.use_enum_default(obj):
34+
return d.enum_default(obj)
35+
36+
if d.use_dataclass_default(obj):
37+
return d.dataclass_default(obj)
38+
39+
if d.use_type_default(obj):
40+
return d.type_default(obj)
41+
42+
return d.unknown_default(obj)
43+
44+
45+
### CLASSES
46+
### ============================================================================
47+
class SimpleJsonFormatter(core.BaseJsonFormatter):
48+
"""JSON formatter using [simplejson](https://github.com/simplejson/simplejson/tree/master) for encoding.
49+
50+
!!! warning "Note that simplejson handles certain input different to other encoders in python-json-logger."
51+
52+
`datetime.datetime` objects use `' '` as the delimiter instead of `'T'`.
53+
54+
`bytes` can only be encoded if they are valid `utf-8`.
55+
56+
57+
"""
58+
59+
def __init__(
60+
self,
61+
*args,
62+
json_default: Optional[Callable] = simplejson_default,
63+
json_indent: Optional[Union[int, str]] = None,
64+
**kwargs,
65+
) -> None:
66+
"""
67+
Args:
68+
args: see [BaseJsonFormatter][pythonjsonlogger.core.BaseJsonFormatter]
69+
json_default: a function for encoding non-standard objects
70+
json_indent: indent output with this number of spaces or with the given string.
71+
kwargs: see [BaseJsonFormatter][pythonjsonlogger.core.BaseJsonFormatter]
72+
"""
73+
super().__init__(*args, **kwargs)
74+
75+
# TODO: consider supporting for_json
76+
# REF: https://github.com/simplejson/simplejson/blob/master/simplejson/encoder.py#L220
77+
78+
self.json_encoder = simplejson.JSONEncoder(
79+
default=json_default,
80+
indent=json_indent,
81+
)
82+
return
83+
84+
def jsonify_log_record(self, log_data: core.LogData) -> str:
85+
"""Returns a json string of the log data."""
86+
return self.json_encoder.encode(log_data)

src/pythonjsonlogger/ultrajson.py

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
"""JSON Formatter using [ultrajson](https://github.com/ultrajson/ultrajson)"""
2+
3+
### IMPORTS
4+
### ============================================================================
5+
## Future
6+
from __future__ import annotations
7+
8+
## Standard Library
9+
from typing import Any, Optional, Callable
10+
11+
## Installed
12+
13+
## Application
14+
from . import core
15+
from . import defaults as d
16+
from .utils import package_is_available
17+
18+
# We import ujson after checking it is available
19+
package_is_available("ujson", throw_error=True)
20+
import ujson # pylint: disable=wrong-import-position,wrong-import-order
21+
22+
23+
### FUNCTIONS
24+
### ============================================================================
25+
def ujson_default(obj: Any) -> Any:
26+
"""ujson default encoder function for non-standard types"""
27+
28+
if d.use_exception_default(obj):
29+
return d.exception_default(obj)
30+
31+
if d.use_traceback_default(obj):
32+
return d.traceback_default(obj)
33+
34+
if d.use_enum_default(obj):
35+
return d.enum_default(obj)
36+
37+
if d.use_dataclass_default(obj):
38+
return d.dataclass_default(obj)
39+
40+
if d.use_type_default(obj):
41+
return d.type_default(obj)
42+
43+
return d.unknown_default(obj)
44+
45+
46+
### CLASSES
47+
### ============================================================================
48+
class UltraJsonFormatter(core.BaseJsonFormatter):
49+
"""JSON formatter using [ultrajson](https://github.com/ultrajson/ultrajson) (`ujson`) for encoding.
50+
51+
!!! warning "UltraJSON is in maintenance mode"
52+
[Per README](https://github.com/ultrajson/ultrajson/tree/main?tab=readme-ov-file#project-status) users
53+
are encouraged to move to another JSON encoder such as `orjson`.
54+
55+
!!! warning "Note that ultrajson handles certain input different to other encoders in python-json-logger."
56+
57+
`datetime.datetime` objects use `' '` as the delimiter instead of `'T'`.
58+
59+
`bytes` can only be encoded if they are valid `utf-8`.
60+
61+
62+
"""
63+
64+
def __init__(
65+
self,
66+
*args,
67+
json_default: Optional[Callable] = ujson_default,
68+
json_indent: int = 0,
69+
**kwargs,
70+
) -> None:
71+
"""
72+
Args:
73+
args: see [BaseJsonFormatter][pythonjsonlogger.core.BaseJsonFormatter]
74+
json_default: a function for encoding non-standard objects
75+
json_indent: indent output with this number of spaces.
76+
kwargs: see [BaseJsonFormatter][pythonjsonlogger.core.BaseJsonFormatter]
77+
"""
78+
super().__init__(*args, **kwargs)
79+
80+
self.json_default = json_default
81+
self.json_indent = json_indent
82+
83+
return
84+
85+
def jsonify_log_record(self, log_data: core.LogData) -> str:
86+
"""Returns a json string of the log data."""
87+
return ujson.dumps(
88+
log_data, indent=self.json_indent, default=self.json_default, reject_bytes=False
89+
)

tests/test_formatters.py

Lines changed: 50 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,20 +31,34 @@
3131
from pythonjsonlogger.core import RESERVED_ATTRS, BaseJsonFormatter, merge_record_extra
3232
from pythonjsonlogger.json import JsonFormatter
3333

34+
if pythonjsonlogger.MSGSPEC_AVAILABLE:
35+
from pythonjsonlogger.msgspec import MsgspecFormatter
36+
3437
if pythonjsonlogger.ORJSON_AVAILABLE:
3538
from pythonjsonlogger.orjson import OrjsonFormatter
3639

37-
if pythonjsonlogger.MSGSPEC_AVAILABLE:
38-
from pythonjsonlogger.msgspec import MsgspecFormatter
40+
if pythonjsonlogger.SIMPLEJSON_AVAILABLE:
41+
from pythonjsonlogger.simplejson import SimpleJsonFormatter
42+
43+
if pythonjsonlogger.ULTRAJSON_AVAILABLE:
44+
from pythonjsonlogger.ultrajson import UltraJsonFormatter
3945

4046
### SETUP
4147
### ============================================================================
4248
ALL_FORMATTERS: list[type[BaseJsonFormatter]] = [JsonFormatter]
43-
if pythonjsonlogger.ORJSON_AVAILABLE:
44-
ALL_FORMATTERS.append(OrjsonFormatter)
49+
4550
if pythonjsonlogger.MSGSPEC_AVAILABLE:
4651
ALL_FORMATTERS.append(MsgspecFormatter)
4752

53+
if pythonjsonlogger.ORJSON_AVAILABLE:
54+
ALL_FORMATTERS.append(OrjsonFormatter)
55+
56+
if pythonjsonlogger.SIMPLEJSON_AVAILABLE:
57+
ALL_FORMATTERS.append(SimpleJsonFormatter)
58+
59+
if pythonjsonlogger.ULTRAJSON_AVAILABLE:
60+
ALL_FORMATTERS.append(UltraJsonFormatter)
61+
4862
_LOGGER_COUNT = 0
4963

5064

@@ -543,7 +557,15 @@ def json_default(obj: Any) -> Any:
543557
env.logger.info("Hello")
544558
log_json = env.load_json()
545559

546-
assert log_json["timestamp"] == "2017-07-14T02:40:00+00:00"
560+
expected = "2017-07-14T02:40:00+00:00"
561+
562+
if (pythonjsonlogger.SIMPLEJSON_AVAILABLE and class_ is SimpleJsonFormatter) or (
563+
pythonjsonlogger.ULTRAJSON_AVAILABLE and class_ is UltraJsonFormatter
564+
):
565+
# simplejson and ujson do not use sep=T
566+
expected = expected.replace("T", " ")
567+
568+
assert log_json["timestamp"] == expected
547569
return
548570

549571

@@ -616,6 +638,29 @@ def test_common_types_encoded(
616638
):
617639
pytest.xfail()
618640

641+
if (pythonjsonlogger.SIMPLEJSON_AVAILABLE and class_ is SimpleJsonFormatter) or (
642+
pythonjsonlogger.ULTRAJSON_AVAILABLE and class_ is UltraJsonFormatter
643+
):
644+
if obj == b"fancy-bytes-\xf0\xf1":
645+
# simplejson attempts to encode using `utf-8` and thus does not support arbitrary bytes
646+
# ultrajson prevents bytes or errors when receiving non `utf-8` bytes
647+
pytest.xfail()
648+
649+
## Overrides
650+
if (pythonjsonlogger.SIMPLEJSON_AVAILABLE and class_ is SimpleJsonFormatter) or (
651+
pythonjsonlogger.ULTRAJSON_AVAILABLE and class_ is UltraJsonFormatter
652+
):
653+
if isinstance(obj, datetime.datetime):
654+
# simplejson and ujson do not use sep=T
655+
expected = expected.replace("T", " ")
656+
657+
elif obj is MultiEnum.BYTES or obj == b"some-bytes":
658+
expected = "some-bytes"
659+
660+
elif obj is MultiEnum:
661+
expected = list(expected)
662+
expected[4] = "some-bytes"
663+
619664
## Test
620665
env.set_formatter(class_())
621666
extra = {

0 commit comments

Comments
 (0)