Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

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

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

Expand Down
2 changes: 1 addition & 1 deletion pylintrc
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
# A comma-separated list of package or module names from where C extensions may
# be loaded. Extensions are loading into the active Python interpreter and may
# run arbitrary code.
extension-pkg-whitelist=orjson
extension-pkg-whitelist=orjson,ujson

# Add files or directories to the blacklist. They should be base names, not
# paths.
Expand Down
5 changes: 4 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,11 @@ GitHub = "https://github.com/nhairs/python-json-logger"
[project.optional-dependencies]
dev = [
## Optional but required for dev
"orjson;implementation_name!='pypy'",
"msgspec;implementation_name!='pypy'",
"orjson;implementation_name!='pypy'",
"simplejson",
"types-simplejson",
"ujson",
## Lint
"validate-pyproject[all]",
"black",
Expand Down
4 changes: 3 additions & 1 deletion src/pythonjsonlogger/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,7 @@

### CONSTANTS
### ============================================================================
ORJSON_AVAILABLE = utils.package_is_available("orjson")
MSGSPEC_AVAILABLE = utils.package_is_available("msgspec")
ORJSON_AVAILABLE = utils.package_is_available("orjson")
SIMPLEJSON_AVAILABLE = utils.package_is_available("simplejson")
ULTRAJSON_AVAILABLE = utils.package_is_available("ujson")
87 changes: 87 additions & 0 deletions src/pythonjsonlogger/simplejson.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
"""JSON Formatter using [simplejson](https://github.com/simplejson/simplejson/tree/master)"""

### IMPORTS
### ============================================================================
## Future
from __future__ import annotations

## Standard Library
from typing import Any, Optional, Union, Callable

## Installed

## Application
from . import core
from . import defaults as d
from .utils import package_is_available

# We import simplejson after checking it is available
package_is_available("simplejson", throw_error=True)
import simplejson # pylint: disable=wrong-import-position,wrong-import-order


### FUNCTIONS
### ============================================================================
def simplejson_default(obj: Any) -> Any:
"""simplejson default encoder function for non-standard types"""
if d.use_exception_default(obj):
return d.exception_default(obj)

if d.use_traceback_default(obj):
return d.traceback_default(obj)

if d.use_enum_default(obj):
return d.enum_default(obj)

if d.use_dataclass_default(obj):
return d.dataclass_default(obj)

if d.use_type_default(obj):
return d.type_default(obj)

return d.unknown_default(obj)


### CLASSES
### ============================================================================
class SimpleJsonFormatter(core.BaseJsonFormatter):
"""JSON formatter using [simplejson](https://github.com/simplejson/simplejson/tree/master) for encoding.

!!! warning "Note that simplejson handles certain input different to other encoders in python-json-logger."

`datetime.datetime` objects use `' '` as the delimiter instead of `'T'`.

`bytes` can only be encoded if they are valid `utf-8`.


New in `4.1`
"""

def __init__(
self,
*args,
json_default: Optional[Callable] = simplejson_default,
json_indent: Optional[Union[int, str]] = None,
**kwargs,
) -> None:
"""
Args:
args: see [BaseJsonFormatter][pythonjsonlogger.core.BaseJsonFormatter]
json_default: a function for encoding non-standard objects
json_indent: indent output with this number of spaces or with the given string.
kwargs: see [BaseJsonFormatter][pythonjsonlogger.core.BaseJsonFormatter]
"""
super().__init__(*args, **kwargs)

# TODO: consider supporting for_json
# REF: https://github.com/simplejson/simplejson/blob/master/simplejson/encoder.py#L220

self.json_encoder = simplejson.JSONEncoder(
default=json_default,
indent=json_indent,
)
return

def jsonify_log_record(self, log_data: core.LogData) -> str:
"""Returns a json string of the log data."""
return self.json_encoder.encode(log_data)
89 changes: 89 additions & 0 deletions src/pythonjsonlogger/ultrajson.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
"""JSON Formatter using [ultrajson](https://github.com/ultrajson/ultrajson)"""

### IMPORTS
### ============================================================================
## Future
from __future__ import annotations

## Standard Library
from typing import Any, Optional, Callable

## Installed

## Application
from . import core
from . import defaults as d
from .utils import package_is_available

# We import ujson after checking it is available
package_is_available("ujson", throw_error=True)
import ujson # pylint: disable=wrong-import-position,wrong-import-order


### FUNCTIONS
### ============================================================================
def ujson_default(obj: Any) -> Any:
"""ujson default encoder function for non-standard types"""

if d.use_exception_default(obj):
return d.exception_default(obj)

if d.use_traceback_default(obj):
return d.traceback_default(obj)

if d.use_enum_default(obj):
return d.enum_default(obj)

if d.use_dataclass_default(obj):
return d.dataclass_default(obj)

if d.use_type_default(obj):
return d.type_default(obj)

return d.unknown_default(obj)


### CLASSES
### ============================================================================
class UltraJsonFormatter(core.BaseJsonFormatter):
"""JSON formatter using [ultrajson](https://github.com/ultrajson/ultrajson) (`ujson`) for encoding.

!!! warning "UltraJSON is in maintenance mode"
[Per README](https://github.com/ultrajson/ultrajson/tree/main?tab=readme-ov-file#project-status) users
are encouraged to move to another JSON encoder such as `orjson`.

!!! warning "Note that ultrajson handles certain input different to other encoders in python-json-logger."

`datetime.datetime` objects use `' '` as the delimiter instead of `'T'`.

`bytes` can only be encoded if they are valid `utf-8`.

New in `4.1`
"""

def __init__(
self,
*args,
json_default: Optional[Callable] = ujson_default,
json_indent: int = 0,
**kwargs,
) -> None:
"""
Args:
args: see [BaseJsonFormatter][pythonjsonlogger.core.BaseJsonFormatter]
json_default: a function for encoding non-standard objects
json_indent: indent output with this number of spaces.
kwargs: see [BaseJsonFormatter][pythonjsonlogger.core.BaseJsonFormatter]
"""
super().__init__(*args, **kwargs)

self.json_default = json_default
self.json_indent = json_indent

return

def jsonify_log_record(self, log_data: core.LogData) -> str:
"""Returns a json string of the log data."""
return ujson.dumps(
log_data, indent=self.json_indent, default=self.json_default, reject_bytes=False
)
55 changes: 50 additions & 5 deletions tests/test_formatters.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,20 +31,34 @@
from pythonjsonlogger.core import RESERVED_ATTRS, BaseJsonFormatter, merge_record_extra
from pythonjsonlogger.json import JsonFormatter

if pythonjsonlogger.MSGSPEC_AVAILABLE:
from pythonjsonlogger.msgspec import MsgspecFormatter

if pythonjsonlogger.ORJSON_AVAILABLE:
from pythonjsonlogger.orjson import OrjsonFormatter

if pythonjsonlogger.MSGSPEC_AVAILABLE:
from pythonjsonlogger.msgspec import MsgspecFormatter
if pythonjsonlogger.SIMPLEJSON_AVAILABLE:
from pythonjsonlogger.simplejson import SimpleJsonFormatter

if pythonjsonlogger.ULTRAJSON_AVAILABLE:
from pythonjsonlogger.ultrajson import UltraJsonFormatter

### SETUP
### ============================================================================
ALL_FORMATTERS: list[type[BaseJsonFormatter]] = [JsonFormatter]
if pythonjsonlogger.ORJSON_AVAILABLE:
ALL_FORMATTERS.append(OrjsonFormatter)

if pythonjsonlogger.MSGSPEC_AVAILABLE:
ALL_FORMATTERS.append(MsgspecFormatter)

if pythonjsonlogger.ORJSON_AVAILABLE:
ALL_FORMATTERS.append(OrjsonFormatter)

if pythonjsonlogger.SIMPLEJSON_AVAILABLE:
ALL_FORMATTERS.append(SimpleJsonFormatter)

if pythonjsonlogger.ULTRAJSON_AVAILABLE:
ALL_FORMATTERS.append(UltraJsonFormatter)

_LOGGER_COUNT = 0


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

assert log_json["timestamp"] == "2017-07-14T02:40:00+00:00"
expected = "2017-07-14T02:40:00+00:00"

if (pythonjsonlogger.SIMPLEJSON_AVAILABLE and class_ is SimpleJsonFormatter) or (
pythonjsonlogger.ULTRAJSON_AVAILABLE and class_ is UltraJsonFormatter
):
# simplejson and ujson do not use sep=T
expected = expected.replace("T", " ")

assert log_json["timestamp"] == expected
return


Expand Down Expand Up @@ -616,6 +638,29 @@ def test_common_types_encoded(
):
pytest.xfail()

if (pythonjsonlogger.SIMPLEJSON_AVAILABLE and class_ is SimpleJsonFormatter) or (
pythonjsonlogger.ULTRAJSON_AVAILABLE and class_ is UltraJsonFormatter
):
if obj == b"fancy-bytes-\xf0\xf1":
# simplejson attempts to encode using `utf-8` and thus does not support arbitrary bytes
# ultrajson prevents bytes or errors when receiving non `utf-8` bytes
pytest.xfail()

## Overrides
if (pythonjsonlogger.SIMPLEJSON_AVAILABLE and class_ is SimpleJsonFormatter) or (
pythonjsonlogger.ULTRAJSON_AVAILABLE and class_ is UltraJsonFormatter
):
if isinstance(obj, datetime.datetime):
# simplejson and ujson do not use sep=T
expected = expected.replace("T", " ")

elif obj is MultiEnum.BYTES or obj == b"some-bytes":
expected = "some-bytes"

elif obj is MultiEnum:
expected = list(expected)
expected[4] = "some-bytes"

## Test
env.set_formatter(class_())
extra = {
Expand Down
Loading