From 78fec9ffef5c65af0a1f3d31dd34da1087c46fdd Mon Sep 17 00:00:00 2001 From: syf2211 Date: Fri, 3 Jul 2026 12:06:12 +0000 Subject: [PATCH] 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. --- python/copilot/generated/session_events.py | 13 +++++++++++-- python/test_event_forward_compatibility.py | 11 +++++++++++ scripts/codegen/python.ts | 15 +++++++++++---- 3 files changed, 33 insertions(+), 6 deletions(-) diff --git a/python/copilot/generated/session_events.py b/python/copilot/generated/session_events.py index 00f6bb35cd..b6479d2171 100644 --- a/python/copilot/generated/session_events.py +++ b/python/copilot/generated/session_events.py @@ -290,16 +290,25 @@ class Data: def __init__(self, **kwargs: Any): self._values = {key: _compat_from_json_value(value) for key, value in kwargs.items()} + self._json_keys: dict[str, str] = {} for key, value in self._values.items(): setattr(self, key, value) @staticmethod def from_dict(obj: Any) -> "Data": assert isinstance(obj, dict) - return Data(**{_compat_to_python_key(key): _compat_from_json_value(value) for key, value in obj.items()}) + data = Data() + data._values = {} + data._json_keys = {} + for key, value in obj.items(): + py_key = _compat_to_python_key(key) + data._values[py_key] = _compat_from_json_value(value) + data._json_keys[py_key] = key + setattr(data, py_key, data._values[py_key]) + return data def to_dict(self) -> dict: - return {_compat_to_json_key(key): _compat_to_json_value(value) for key, value in self._values.items() if value is not None} + 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} # Experimental: this type is part of an experimental API and may change or be removed. diff --git a/python/test_event_forward_compatibility.py b/python/test_event_forward_compatibility.py index 13fa4f09e5..eeb4c5ec95 100644 --- a/python/test_event_forward_compatibility.py +++ b/python/test_event_forward_compatibility.py @@ -138,6 +138,17 @@ def test_data_shim_preserves_raw_mapping_values(self): constructed = Data(arguments={"tool_call_id": "call-1"}) assert constructed.to_dict() == {"arguments": {"tool_call_id": "call-1"}} + def test_data_shim_preserves_abbreviation_json_keys_on_round_trip(self): + """Data.from_dict(x).to_dict() should preserve JSON keys with abbreviations. + + Regression test for github/copilot-sdk#1138: keys like userURL, sessionID, + and OAuthToken were rewritten on round-trip because _compat_to_json_key could + not reconstruct the original camelCase abbreviation casing. + """ + for key in ["userURL", "sessionID", "XMLPayload", "serverIP", "OAuthToken"]: + incoming = {key: 42} + assert Data.from_dict(incoming).to_dict() == incoming + def test_missing_optional_fields_remain_none_after_parsing(self): """Generated event models should leave missing optional fields as None. diff --git a/scripts/codegen/python.ts b/scripts/codegen/python.ts index e0c4e21412..cdb9dbf49f 100644 --- a/scripts/codegen/python.ts +++ b/scripts/codegen/python.ts @@ -2673,19 +2673,26 @@ export function generatePythonSessionEventsCode(schema: JSONSchema7): string { out.push(``); out.push(` def __init__(self, **kwargs: Any):`); out.push(` self._values = {key: _compat_from_json_value(value) for key, value in kwargs.items()}`); + out.push(` self._json_keys: dict[str, str] = {}`); out.push(` for key, value in self._values.items():`); out.push(` setattr(self, key, value)`); out.push(``); out.push(` @staticmethod`); out.push(` def from_dict(obj: Any) -> "Data":`); out.push(` assert isinstance(obj, dict)`); - out.push( - ` return Data(**{_compat_to_python_key(key): _compat_from_json_value(value) for key, value in obj.items()})` - ); + out.push(` data = Data()`); + out.push(` data._values = {}`); + out.push(` data._json_keys = {}`); + out.push(` for key, value in obj.items():`); + out.push(` py_key = _compat_to_python_key(key)`); + out.push(` data._values[py_key] = _compat_from_json_value(value)`); + out.push(` data._json_keys[py_key] = key`); + out.push(` setattr(data, py_key, data._values[py_key])`); + out.push(` return data`); out.push(``); out.push(` def to_dict(self) -> dict:`); out.push( - ` return {_compat_to_json_key(key): _compat_to_json_value(value) for key, value in self._values.items() if value is not None}` + ` 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}` ); out.push(``); out.push(``);