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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- Updated dev and build-time dependencies.

### Fixed

- Fixed an issue where setting a non-JSON datacontenttype (e.g., application/octet-stream) with dict data produced non-JSON-decodable output; now best-effort json-encoding is applied regardless of datacontenttype. ([#291])
- Updated behavior when datacontenttype is unset: now treats events as application/json in line with spec recommendation. ([#291])

## [2.1.0]

### Added
Expand Down
25 changes: 17 additions & 8 deletions src/cloudevents/core/formats/json.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ def default(self, obj: Any) -> Any:

class JSONFormat(Format):
CONTENT_TYPE: Final[str] = "application/cloudevents+json"
DEFAULT_CONTENT_TYPE: Final[str] = "application/json"
JSON_CONTENT_TYPE_PATTERN: Pattern[str] = re.compile(
r"^(application|text)/([a-zA-Z0-9\-\.]+\+)?json(;.*)?$"
)
Expand Down Expand Up @@ -135,7 +136,9 @@ def write(self, event: BaseCloudEvent) -> bytes:
"utf-8"
)
else:
datacontenttype = event_dict.get("datacontenttype", "application/json")
datacontenttype = event_dict.get(
"datacontenttype", self.DEFAULT_CONTENT_TYPE
)
if re.match(JSONFormat.JSON_CONTENT_TYPE_PATTERN, datacontenttype):
event_dict["data"] = event_data
else:
Expand Down Expand Up @@ -171,12 +174,19 @@ def write_data(

# If data is a dict and content type is JSON, serialize as JSON
if isinstance(data, dict):
if datacontenttype and re.match(
JSONFormat.JSON_CONTENT_TYPE_PATTERN, datacontenttype
):
content_type = datacontenttype or self.DEFAULT_CONTENT_TYPE
if re.match(JSONFormat.JSON_CONTENT_TYPE_PATTERN, content_type):
return dumps(data, cls=_JSONEncoderWithDatetime).encode("utf-8")

# Default: convert to string and encode
# for other contenttypes we still try to generate a json-decodable string
# if not possible, a string representing
try:
return dumps(data, cls=_JSONEncoderWithDatetime).encode("utf-8")
except TypeError:
pass

# according to the spec, we return an encoded string per default
# careful: the result is not json-decodable as the dict keys are single-quoted
return str(data).encode("utf-8")

def read_data(
Expand All @@ -196,9 +206,8 @@ def read_data(
return None

# If content type indicates JSON, try to parse as JSON
if datacontenttype and re.match(
JSONFormat.JSON_CONTENT_TYPE_PATTERN, datacontenttype
):
content_type = datacontenttype or self.DEFAULT_CONTENT_TYPE
if re.match(JSONFormat.JSON_CONTENT_TYPE_PATTERN, content_type):
try:
decoded = body.decode("utf-8")
parsed: dict[str, Any] = loads(decoded)
Expand Down
25 changes: 24 additions & 1 deletion tests/test_core/test_format/test_json.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@
# License for the specific language governing permissions and limitations
# under the License.


from datetime import datetime, timezone
from json import loads

import pytest

from cloudevents.core.formats.json import JSONFormat
from cloudevents.core.v1.event import CloudEvent
Expand Down Expand Up @@ -323,3 +325,24 @@ def test_read_cloud_event_from_string_input() -> None:

assert result.get_id() == "123"
assert result.get_source() == "source"


@pytest.mark.parametrize(
"content_type", [None, "application/json", "application/octet-stream"]
)
def test_write_data_dict(content_type: str) -> None:
formatter = JSONFormat()
data = {"key": "value", "nested": {"a": 1}}
result = formatter.write_data(data, datacontenttype=content_type)

assert isinstance(result, bytes)
assert loads(result) == data


@pytest.mark.parametrize("content_type", [None, "application/json"])
def test_read_data_json_body(content_type: str) -> None:
formatter = JSONFormat()
body = b'{"key": "value"}'
result = formatter.read_data(body, content_type)

assert result == {"key": "value"}
Loading