Skip to content

Commit f3ebf3e

Browse files
syf2211rojiCopilot
authored
fix(python): preserve original JSON keys in Data shim round-trips (#1900)
* fix(python): preserve original JSON keys in Data shim round-trips Fixes #1138 The Data compatibility shim converted JSON keys to snake_case for attribute access but could not reconstruct abbreviation-heavy camelCase keys (userURL, sessionID, OAuthToken) on to_dict(). Store the original JSON key per field during from_dict() and prefer it when serializing back. Includes regression tests for the abbreviation key cases described in the issue. * fix(python): preserve colliding Data JSON keys Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: syf2211 <syf2211@users.noreply.github.com> Co-authored-by: Shay Rojansky <roji@roji.org> Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
1 parent 34e89cd commit f3ebf3e

3 files changed

Lines changed: 54 additions & 8 deletions

File tree

python/copilot/generated/session_events.py

Lines changed: 19 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

python/test_event_forward_compatibility.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,22 @@ def test_data_shim_preserves_raw_mapping_values(self):
138138
constructed = Data(arguments={"tool_call_id": "call-1"})
139139
assert constructed.to_dict() == {"arguments": {"tool_call_id": "call-1"}}
140140

141+
def test_data_shim_preserves_abbreviation_json_keys_on_round_trip(self):
142+
"""Data.from_dict(x).to_dict() should preserve JSON keys with abbreviations.
143+
144+
Regression test for github/copilot-sdk#1138: keys like userURL, sessionID,
145+
and OAuthToken were rewritten on round-trip because _compat_to_json_key could
146+
not reconstruct the original camelCase abbreviation casing.
147+
"""
148+
for key in ["userURL", "sessionID", "XMLPayload", "serverIP", "OAuthToken"]:
149+
incoming = {key: 42}
150+
assert Data.from_dict(incoming).to_dict() == incoming
151+
152+
def test_data_shim_preserves_colliding_json_keys_on_round_trip(self):
153+
"""Data.from_dict(x).to_dict() should preserve keys with the same Python name."""
154+
colliding_keys = {"userURL": 42, "userUrl": 43}
155+
assert Data.from_dict(colliding_keys).to_dict() == colliding_keys
156+
141157
def test_missing_optional_fields_remain_none_after_parsing(self):
142158
"""Generated event models should leave missing optional fields as None.
143159

scripts/codegen/python.ts

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2673,19 +2673,34 @@ export function generatePythonSessionEventsCode(schema: JSONSchema7): string {
26732673
out.push(``);
26742674
out.push(` def __init__(self, **kwargs: Any):`);
26752675
out.push(` self._values = {key: _compat_from_json_value(value) for key, value in kwargs.items()}`);
2676+
out.push(` self._json_keys: dict[str, str] = {}`);
2677+
out.push(` self._json_values: dict[str, Any] | None = None`);
26762678
out.push(` for key, value in self._values.items():`);
26772679
out.push(` setattr(self, key, value)`);
26782680
out.push(``);
26792681
out.push(` @staticmethod`);
26802682
out.push(` def from_dict(obj: Any) -> "Data":`);
26812683
out.push(` assert isinstance(obj, dict)`);
2682-
out.push(
2683-
` return Data(**{_compat_to_python_key(key): _compat_from_json_value(value) for key, value in obj.items()})`
2684-
);
2684+
out.push(` data = Data()`);
2685+
out.push(` data._values = {}`);
2686+
out.push(` data._json_keys = {}`);
2687+
out.push(` data._json_values = {}`);
2688+
out.push(` for key, value in obj.items():`);
2689+
out.push(` py_key = _compat_to_python_key(key)`);
2690+
out.push(` json_value = _compat_from_json_value(value)`);
2691+
out.push(` data._values[py_key] = json_value`);
2692+
out.push(` data._json_keys[py_key] = key`);
2693+
out.push(` data._json_values[key] = json_value`);
2694+
out.push(` setattr(data, py_key, data._values[py_key])`);
2695+
out.push(` return data`);
26852696
out.push(``);
26862697
out.push(` def to_dict(self) -> dict:`);
2698+
out.push(` if self._json_values is not None:`);
2699+
out.push(
2700+
` return {key: _compat_to_json_value(value) for key, value in self._json_values.items() if value is not None}`
2701+
);
26872702
out.push(
2688-
` return {_compat_to_json_key(key): _compat_to_json_value(value) for key, value in self._values.items() if value is not None}`
2703+
` return {(self._json_keys.get(key) or _compat_to_json_key(key)): _compat_to_json_value(value) for key, value in self._values.items() if value is not None}`
26892704
);
26902705
out.push(``);
26912706
out.push(``);

0 commit comments

Comments
 (0)