Skip to content

Commit 520d689

Browse files
romanlutzCopilot
andauthored
MAINT: Standardize JSON serialization in models (microsoft#1813)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent ba02626 commit 520d689

8 files changed

Lines changed: 126 additions & 28 deletions

File tree

doc/code/memory/embeddings.ipynb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@
7777
}
7878
],
7979
"source": [
80-
"embedding_response.to_json()"
80+
"embedding_response.model_dump_json()"
8181
]
8282
},
8383
{

doc/code/memory/embeddings.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040
# To view the json of an embedding
4141

4242
# %%
43-
embedding_response.to_json()
43+
embedding_response.model_dump_json()
4444

4545
# %% [markdown]
4646
# To save an embedding to disk

pyrit/models/chat_message.py

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -35,30 +35,37 @@ class ChatMessage(BaseModel):
3535
tool_calls: Optional[list[ToolCall]] = None
3636
tool_call_id: Optional[str] = None
3737

38-
def to_json(self) -> str:
38+
def to_dict(self) -> dict[str, Any]:
3939
"""
40-
Serialize the ChatMessage to a JSON string.
40+
Convert the ChatMessage to a dictionary.
4141
4242
Returns:
43-
A JSON string representation of the message.
43+
A dictionary representation of the message, excluding None values.
4444
4545
"""
46-
return self.model_dump_json()
46+
return self.model_dump(exclude_none=True)
4747

48-
def to_dict(self) -> dict[str, Any]:
48+
def to_json(self) -> str:
4949
"""
50-
Convert the ChatMessage to a dictionary.
50+
Serialize the ChatMessage to a JSON string (deprecated, use ``model_dump_json`` instead).
5151
5252
Returns:
53-
A dictionary representation of the message, excluding None values.
53+
A JSON string representation of the message.
5454
5555
"""
56-
return self.model_dump(exclude_none=True)
56+
from pyrit.common.deprecation import print_deprecation_message
57+
58+
print_deprecation_message(
59+
old_item="ChatMessage.to_json",
60+
new_item="ChatMessage.model_dump_json",
61+
removed_in="0.15.0",
62+
)
63+
return self.model_dump_json()
5764

5865
@classmethod
5966
def from_json(cls, json_str: str) -> "ChatMessage":
6067
"""
61-
Deserialize a ChatMessage from a JSON string.
68+
Deserialize a ChatMessage from a JSON string (deprecated, use ``model_validate_json`` instead).
6269
6370
Args:
6471
json_str: A JSON string representation of a ChatMessage.
@@ -67,6 +74,13 @@ def from_json(cls, json_str: str) -> "ChatMessage":
6774
A ChatMessage instance.
6875
6976
"""
77+
from pyrit.common.deprecation import print_deprecation_message
78+
79+
print_deprecation_message(
80+
old_item="ChatMessage.from_json",
81+
new_item="ChatMessage.model_validate_json",
82+
removed_in="0.15.0",
83+
)
7084
return cls.model_validate_json(json_str)
7185

7286

pyrit/models/embeddings.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,12 +70,19 @@ def load_from_file(file_path: Path) -> EmbeddingResponse:
7070

7171
def to_json(self) -> str:
7272
"""
73-
Serialize this embedding response to JSON.
73+
Serialize this embedding response to JSON (deprecated, use ``model_dump_json`` instead).
7474
7575
Returns:
7676
str: JSON-encoded embedding response.
7777
7878
"""
79+
from pyrit.common.deprecation import print_deprecation_message
80+
81+
print_deprecation_message(
82+
old_item="EmbeddingResponse.to_json",
83+
new_item="EmbeddingResponse.model_dump_json",
84+
removed_in="0.15.0",
85+
)
7986
return self.model_dump_json()
8087

8188

pyrit/score/scorer_evaluation/scorer_metrics.py

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@ class ScorerMetrics:
2626
"""
2727
Base dataclass for storing scorer evaluation metrics.
2828
29-
This class provides methods for serializing metrics to JSON and loading them from JSON files.
29+
This class provides methods for serializing metrics to JSON strings (see
30+
:meth:`to_json`) and loading them from JSON files on disk (see
31+
:meth:`from_json_file`).
3032
3133
Args:
3234
num_responses (int): Total number of responses evaluated.
@@ -48,23 +50,34 @@ class ScorerMetrics:
4850

4951
def to_json(self) -> str:
5052
"""
51-
Convert the metrics to a JSON string.
53+
Serialize this metrics instance to a JSON string.
54+
55+
This is the canonical serialization entry point for ``ScorerMetrics`` and its
56+
subclasses. Pair it with :meth:`from_json_file` (which reads a JSON file written
57+
from this string, optionally wrapped in a ``"metrics"`` key) for round-trip
58+
(de)serialization.
5259
5360
Returns:
5461
str: The JSON string representation of the metrics.
5562
"""
5663
return json.dumps(asdict(self))
5764

5865
@classmethod
59-
def from_json(cls: type[T], file_path: Union[str, Path]) -> T:
66+
def from_json_file(cls: type[T], file_path: Union[str, Path]) -> T:
6067
"""
61-
Load the metrics from a JSON file.
68+
Load a metrics instance from a JSON file on disk.
69+
70+
This is the canonical deserialization entry point for ``ScorerMetrics`` and its
71+
subclasses. It accepts a *file path* (string or ``Path``), not a JSON string —
72+
the loader opens the file, unwraps a top-level ``"metrics"`` key if present
73+
(as used by evaluation result files), and filters out internal underscore-prefixed
74+
fields (e.g., cached ``init=False`` attributes) before constructing the instance.
6275
6376
Args:
6477
file_path (Union[str, Path]): The path to the JSON file.
6578
6679
Returns:
67-
ScorerMetrics: An instance of ScorerMetrics with the loaded data.
80+
ScorerMetrics: An instance of ScorerMetrics (or subclass) with the loaded data.
6881
6982
Raises:
7083
FileNotFoundError: If the specified file does not exist.
@@ -82,6 +95,29 @@ def from_json(cls: type[T], file_path: Union[str, Path]) -> T:
8295

8396
return cls(**filtered_data)
8497

98+
@classmethod
99+
def from_json(cls: type[T], file_path: Union[str, Path]) -> T:
100+
"""
101+
Load a metrics instance from a JSON file (deprecated alias for :meth:`from_json_file`).
102+
103+
The name ``from_json`` is misleading because it accepts a *file path*, not a JSON
104+
string. Use :meth:`from_json_file` instead.
105+
106+
Args:
107+
file_path (Union[str, Path]): The path to the JSON file.
108+
109+
Returns:
110+
ScorerMetrics: An instance of ScorerMetrics (or subclass) with the loaded data.
111+
"""
112+
from pyrit.common.deprecation import print_deprecation_message
113+
114+
print_deprecation_message(
115+
old_item=f"{cls.__name__}.from_json",
116+
new_item=f"{cls.__name__}.from_json_file",
117+
removed_in="0.15.0",
118+
)
119+
return cls.from_json_file(file_path)
120+
85121

86122
@dataclass
87123
class HarmScorerMetrics(ScorerMetrics):

tests/unit/models/test_chat_message.py

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -64,9 +64,9 @@ def test_chat_message_invalid_role():
6464
ChatMessage(role="invalid_role", content="hi")
6565

6666

67-
def test_chat_message_to_json():
67+
def test_chat_message_serializes_with_model_dump_json():
6868
msg = ChatMessage(role="user", content="test")
69-
json_str = msg.to_json()
69+
json_str = msg.model_dump_json()
7070
parsed = json.loads(json_str)
7171
assert parsed["role"] == "user"
7272
assert parsed["content"] == "test"
@@ -82,18 +82,18 @@ def test_chat_message_to_dict_excludes_none():
8282
assert d["content"] == "test"
8383

8484

85-
def test_chat_message_from_json():
85+
def test_chat_message_model_validate_json_roundtrip():
8686
original = ChatMessage(role="system", content="you are helpful")
87-
json_str = original.to_json()
88-
restored = ChatMessage.from_json(json_str)
87+
json_str = original.model_dump_json()
88+
restored = ChatMessage.model_validate_json(json_str)
8989
assert restored.role == original.role
9090
assert restored.content == original.content
9191

9292

93-
def test_chat_message_from_json_roundtrip_with_tool_calls():
93+
def test_chat_message_model_validate_json_roundtrip_with_tool_calls():
9494
tc = ToolCall(id="c1", type="function", function="fn")
9595
original = ChatMessage(role="assistant", content="ok", tool_calls=[tc], tool_call_id="c1")
96-
restored = ChatMessage.from_json(original.to_json())
96+
restored = ChatMessage.model_validate_json(original.model_dump_json())
9797
assert restored.tool_calls[0].id == "c1"
9898
assert restored.tool_call_id == "c1"
9999

@@ -104,6 +104,21 @@ def test_chat_message_accepts_all_valid_roles(role):
104104
assert msg.role == role
105105

106106

107+
def test_chat_message_to_json_is_deprecated_alias_for_model_dump_json():
108+
msg = ChatMessage(role="user", content="test")
109+
with pytest.warns(DeprecationWarning, match="ChatMessage.to_json"):
110+
result = msg.to_json()
111+
assert result == msg.model_dump_json()
112+
113+
114+
def test_chat_message_from_json_is_deprecated_alias_for_model_validate_json():
115+
original = ChatMessage(role="system", content="you are helpful")
116+
json_str = original.model_dump_json()
117+
with pytest.warns(DeprecationWarning, match="ChatMessage.from_json"):
118+
restored = ChatMessage.from_json(json_str)
119+
assert restored == original
120+
121+
107122
def test_chat_messages_dataset_init():
108123
msgs = [[ChatMessage(role="user", content="hi"), ChatMessage(role="assistant", content="hello")]]
109124
dataset = ChatMessagesDataset(name="test_ds", description="A test dataset", list_of_chat_messages=msgs)

tests/unit/models/test_embedding_response.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,9 @@ def test_save_load_loop_is_idempotent(my_embedding):
4545
output_file = my_embedding.save_to_file(Path(tmp_dir))
4646
loaded_embedding = EmbeddingResponse.load_from_file(Path(output_file))
4747
assert my_embedding == loaded_embedding
48+
49+
50+
def test_to_json_is_deprecated_alias_for_model_dump_json(my_embedding: EmbeddingResponse):
51+
with pytest.warns(DeprecationWarning, match="EmbeddingResponse.to_json"):
52+
result = my_embedding.to_json()
53+
assert result == my_embedding.model_dump_json()

tests/unit/score/test_scorer_metrics.py

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
from pathlib import Path
66
from unittest.mock import patch
77

8+
import pytest
9+
810
from pyrit.identifiers import ComponentIdentifier
911
from pyrit.score import (
1012
HarmScorerMetrics,
@@ -21,7 +23,7 @@
2123
class TestScorerMetricsSerialization:
2224
"""Tests for ScorerMetrics JSON serialization."""
2325

24-
def test_harm_metrics_to_json_and_from_json(self, tmp_path):
26+
def test_harm_metrics_to_json_and_from_json_file(self, tmp_path):
2527
metrics = HarmScorerMetrics(
2628
num_responses=10,
2729
num_human_raters=3,
@@ -41,10 +43,10 @@ def test_harm_metrics_to_json_and_from_json(self, tmp_path):
4143
file_path = tmp_path / "metrics.json"
4244
with open(file_path, "w") as f:
4345
f.write(json_str)
44-
loaded = HarmScorerMetrics.from_json(str(file_path))
46+
loaded = HarmScorerMetrics.from_json_file(str(file_path))
4547
assert loaded == metrics
4648

47-
def test_objective_metrics_to_json_and_from_json(self, tmp_path):
49+
def test_objective_metrics_to_json_and_from_json_file(self, tmp_path):
4850
metrics = ObjectiveScorerMetrics(
4951
num_responses=10,
5052
num_human_raters=3,
@@ -61,7 +63,25 @@ def test_objective_metrics_to_json_and_from_json(self, tmp_path):
6163
file_path = tmp_path / "metrics.json"
6264
with open(file_path, "w") as f:
6365
f.write(json_str)
64-
loaded = ObjectiveScorerMetrics.from_json(str(file_path))
66+
loaded = ObjectiveScorerMetrics.from_json_file(str(file_path))
67+
assert loaded == metrics
68+
69+
def test_from_json_is_deprecated_alias_for_from_json_file(self, tmp_path):
70+
metrics = ObjectiveScorerMetrics(
71+
num_responses=10,
72+
num_human_raters=3,
73+
accuracy=0.9,
74+
accuracy_standard_error=0.05,
75+
f1_score=0.8,
76+
precision=0.85,
77+
recall=0.75,
78+
)
79+
file_path = tmp_path / "metrics.json"
80+
with open(file_path, "w") as f:
81+
f.write(metrics.to_json())
82+
83+
with pytest.warns(DeprecationWarning, match="ObjectiveScorerMetrics.from_json"):
84+
loaded = ObjectiveScorerMetrics.from_json(str(file_path))
6585
assert loaded == metrics
6686

6787

0 commit comments

Comments
 (0)