Skip to content

Commit 28e69c0

Browse files
suyask-msftclaude
andcommitted
Fix @odata.bind keys being lowercased in record payloads
_lowercase_keys() unconditionally lowercased all dictionary keys, including @odata.bind keys which must retain PascalCase for the navigation property name (e.g. new_CustomerId@odata.bind). This caused 400 errors from Dataverse when creating or updating records with lookup bindings via the SDK. Keys containing @OData. are now preserved as-is. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 2af249b commit 28e69c0

2 files changed

Lines changed: 25 additions & 1 deletion

File tree

src/PowerPlatform/Dataverse/data/_odata.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,10 +96,17 @@ def _lowercase_keys(record: Dict[str, Any]) -> Dict[str, Any]:
9696
9797
Dataverse LogicalNames for attributes are stored lowercase, but users may
9898
provide PascalCase names (matching SchemaName). This normalizes the input.
99+
100+
Keys containing ``@odata.`` (e.g. ``new_CustomerId@odata.bind``) are
101+
preserved as-is because the navigation property portion before ``@``
102+
must retain its original casing (PascalCase SchemaName).
99103
"""
100104
if not isinstance(record, dict):
101105
return record
102-
return {k.lower() if isinstance(k, str) else k: v for k, v in record.items()}
106+
return {
107+
k.lower() if isinstance(k, str) and "@odata." not in k else k: v
108+
for k, v in record.items()
109+
}
103110

104111
@staticmethod
105112
def _lowercase_list(items: Optional[List[str]]) -> Optional[List[str]]:

tests/unit/data/test_odata_internal.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,23 @@ def test_record_keys_lowercased(self):
335335
self.assertIn("name", payload)
336336
self.assertNotIn("Name", payload)
337337

338+
def test_odata_bind_keys_preserve_case(self):
339+
"""@odata.bind keys must preserve PascalCase for navigation property."""
340+
self.od._upsert(
341+
"accounts", "account", {"accountnumber": "ACC-001"},
342+
{
343+
"Name": "Contoso",
344+
"new_CustomerId@odata.bind": "/contacts(00000000-0000-0000-0000-000000000001)",
345+
},
346+
)
347+
call = self._patch_call()
348+
payload = call.kwargs["json"]
349+
# Regular field is lowercased
350+
self.assertIn("name", payload)
351+
# @odata.bind key preserves original casing
352+
self.assertIn("new_CustomerId@odata.bind", payload)
353+
self.assertNotIn("new_customerid@odata.bind", payload)
354+
338355
def test_returns_none(self):
339356
"""_upsert always returns None."""
340357
result = self.od._upsert("accounts", "account", {"accountnumber": "ACC-001"}, {"name": "Contoso"})

0 commit comments

Comments
 (0)