Skip to content

Commit 92db8e9

Browse files
committed
✨ Enhance JSON serialization with a fallback representation encoder and update logging to utilize it
1 parent 4aedb35 commit 92db8e9

File tree

5 files changed

+57
-10
lines changed

5 files changed

+57
-10
lines changed

packages/common-library/src/common_library/json_serialization.py

+25-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
""" Helpers for json serialization
2-
- built-in json-like API
3-
- implemented using orjson, which performs better. SEE https://github.com/ijl/orjson?tab=readme-ov-file#performance
1+
"""Helpers for json serialization
2+
- built-in json-like API
3+
- implemented using orjson, which performs better. SEE https://github.com/ijl/orjson?tab=readme-ov-file#performance
44
"""
55

66
import datetime
@@ -118,6 +118,28 @@ def pydantic_encoder(obj: Any) -> Any:
118118
raise TypeError(msg)
119119

120120

121+
def representation_encoder(obj: Any):
122+
"""
123+
A fallback encoder that uses `pydantic_encoder` to serialize objects.
124+
If serialization fails, it falls back to using `str(obj)`.
125+
126+
This is practical for representation purposes, such as logging or debugging.
127+
128+
Example:
129+
>>> from common_library.json_serialization import json_dumps, representation_encoder
130+
>>> class CustomObject:
131+
... def __str__(self):
132+
... return "CustomObjectRepresentation"
133+
>>> obj = CustomObject()
134+
>>> json_dumps(obj, default=representation_encoder)
135+
'"CustomObjectRepresentation"'
136+
"""
137+
try:
138+
return pydantic_encoder(obj)
139+
except TypeError:
140+
return str(obj)
141+
142+
121143
def json_dumps(
122144
obj: Any,
123145
*,

packages/common-library/tests/test_json_serialization.py

+23
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
SeparatorTuple,
1414
json_dumps,
1515
json_loads,
16+
representation_encoder,
1617
)
1718
from faker import Faker
1819
from pydantic import AnyHttpUrl, AnyUrl, BaseModel, Field, HttpUrl, TypeAdapter
@@ -110,3 +111,25 @@ class M(BaseModel):
110111
http_url=faker.url(),
111112
)
112113
json_dumps(obj)
114+
115+
116+
def test_json_dumps_with_representation_encoder():
117+
class CustomObject:
118+
def __str__(self):
119+
return "CustomObjectRepresentation"
120+
121+
class SomeModel(BaseModel):
122+
x: int
123+
124+
obj = {
125+
"custom": CustomObject(),
126+
"some": SomeModel(x=42),
127+
}
128+
129+
# Using representation_encoder as the default encoder
130+
result = json_dumps(obj, default=representation_encoder, indent=1)
131+
132+
assert (
133+
result
134+
== '{\n "custom": "CustomObjectRepresentation",\n "some": {\n "x": 42\n }\n}'
135+
)

packages/service-library/src/servicelib/logging_errors.py

+5-4
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import logging
2-
from pprint import pformat
32
from typing import Any, TypedDict
43

54
from common_library.error_codes import ErrorCodeStr
65
from common_library.errors_classes import OsparcErrorMixin
6+
from common_library.json_serialization import json_dumps, representation_encoder
77

88
from .logging_utils import LogExtra, get_log_record_extra
99

@@ -27,14 +27,15 @@ def create_troubleshotting_log_message(
2727
error_context -- Additional context surrounding the exception, such as environment variables or function-specific data. This can be derived from exc.error_context() (relevant when using the OsparcErrorMixin)
2828
tip -- Helpful suggestions or possible solutions explaining why the error may have occurred and how it could potentially be resolved
2929
"""
30-
debug_data = pformat(
30+
debug_data = json_dumps(
3131
{
3232
"exception_type": f"{type(error)}",
3333
"exception_details": f"{error}",
3434
"error_code": error_code,
35-
"context": pformat(error_context, indent=1),
35+
"context": error_context,
3636
"tip": tip,
3737
},
38+
default=representation_encoder,
3839
indent=1,
3940
)
4041

@@ -82,7 +83,7 @@ def create_troubleshotting_log_kwargs(
8283
error=error,
8384
error_code=error_code,
8485
error_context=context,
85-
tip=tip,
86+
tip=tip or getattr(error, "tip", None),
8687
)
8788

8889
return {

services/web/server/src/simcore_service_webserver/users/_users_service.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -362,7 +362,8 @@ async def get_my_profile(
362362
)
363363
except GroupExtraPropertiesNotFoundError as err:
364364
raise MissingGroupExtraPropertiesForProductError(
365-
user_id=user_id, product_name=product_name
365+
user_id=user_id,
366+
product_name=product_name,
366367
) from err
367368

368369
return my_profile, preferences

services/web/server/src/simcore_service_webserver/users/exceptions.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,7 @@
33
from ..errors import WebServerBaseError
44

55

6-
class UsersBaseError(WebServerBaseError):
7-
...
6+
class UsersBaseError(WebServerBaseError): ...
87

98

109
class UserNotFoundError(UsersBaseError):
@@ -64,3 +63,4 @@ class BillingDetailsNotFoundError(UsersBaseError):
6463

6564
class MissingGroupExtraPropertiesForProductError(UsersBaseError):
6665
msg_template = "Missing group_extra_property for product_name={product_name}"
66+
tip = "Add a new row in group_extra_property table and assign it to this product"

0 commit comments

Comments
 (0)