Repository: plone/plone.restapi (language: python)

Issue:
Deserializer error: UnboundLocalError: local variable 'resolved_by' referenced before assignment
An import  (collective.exportimport) with the latest version 8.33.3 give us the following error:

```
-> import_content(jsonfile=data, return_json=True)
  /Users/ajung/src/plone6.buildout/src/collective.exportimport/src/collective/exportimport/import_content.py(171)__call__()
-> msg = self.do_import(data)
  /Users/ajung/src/plone6.buildout/src/collective.exportimport/src/collective/exportimport/import_content.py(229)do_import()
-> added = self.import_new_content(data)
> /Users/ajung/src/plone6.buildout/src/collective.exportimport/src/collective/exportimport/import_content.py(366)import_new_content()
-> new = deserializer(validate_all=False, data=item)
  /Users/ajung/src/plone6.buildout/eggs/plone.restapi-8.33.3-py3.9.egg/plone/restapi/deserializer/dxcontent.py(44)__call__()
-> schema_data, errors = self.get_schema_data(data, validate_all, create)
  /Users/ajung/src/plone6.buildout/eggs/plone.restapi-8.33.3-py3.9.egg/plone/restapi/deserializer/dxcontent.py(130)get_schema_data()
-> value = deserializer(data[name])
  /Users/ajung/src/plone6.buildout/eggs/plone.restapi-8.33.3-py3.9.egg/plone/restapi/deserializer/relationfield.py(53)__call__()
-> raise ValueError(f"Could not resolve object for {resolved_by}={value}")
```

https://github.com/plone/plone.restapi/blob/master/src/plone/restapi/deserializer/relationfield.py#L53


Discussion / hints:
@zopyx That is probably not the problem of plone.restapi but exporrimport/you importing a content tree with an item that has a relation to a yet to be imported further in the list.

working on similar issue this week with relation fields on moaisc tiles
@fredvd Yes but plone.restapi should not be trying to interpolate `resolved_by` into the exception message when it is not assigned a value

Fix patch:
diff --git a/news/1600.bugfix b/news/1600.bugfix
new file mode 100644
index 0000000000..34a6cc7964
--- /dev/null
+++ b/news/1600.bugfix
@@ -0,0 +1,1 @@
+Fix UnboundLocalError in RelationChoice deserializer. @davisagli
diff --git a/src/plone/restapi/deserializer/relationfield.py b/src/plone/restapi/deserializer/relationfield.py
index 1317fd3e8c..27c302a349 100644
--- a/src/plone/restapi/deserializer/relationfield.py
+++ b/src/plone/restapi/deserializer/relationfield.py
@@ -16,6 +16,7 @@
 class RelationChoiceFieldDeserializer(DefaultFieldDeserializer):
     def __call__(self, value):
         obj = None
+        resolved_by = None
 
         if isinstance(value, dict):
             # We are trying to deserialize the output of a serialization
@@ -50,7 +51,10 @@ def __call__(self, value):
 
         if obj is None:
             self.request.response.setStatus(400)
-            raise ValueError(f"Could not resolve object for {resolved_by}={value}")
+            msg = f"Could not resolve object for {value}"
+            if resolved_by:
+                msg += f" (resolved by {resolved_by})"
+            raise ValueError(msg)
 
         self.field.validate(obj)
         return obj


Test patch:
diff --git a/src/plone/restapi/tests/test_dxfield_deserializer.py b/src/plone/restapi/tests/test_dxfield_deserializer.py
index 0410a1e049..02c255fa3a 100644
--- a/src/plone/restapi/tests/test_dxfield_deserializer.py
+++ b/src/plone/restapi/tests/test_dxfield_deserializer.py
@@ -421,7 +421,8 @@ def test_relationchoice_deserialization_from_invalid_intid_raises(self):
         with self.assertRaises(ValueError) as cm:
             self.deserialize("test_relationchoice_field", 123456789)
         self.assertEqual(
-            str(cm.exception), "Could not resolve object for intid=123456789"
+            str(cm.exception),
+            "Could not resolve object for 123456789 (resolved by intid)",
         )
         self.assertEqual(400, self.request.response.getStatus())
 
@@ -433,7 +434,7 @@ def test_relationchoice_deserialization_from_invalid_uid_raises(self):
             )
         self.assertEqual(
             str(cm.exception),
-            "Could not resolve object for UID=ac12b24913cf45c6863937367aacc263",
+            "Could not resolve object for ac12b24913cf45c6863937367aacc263 (resolved by UID)",
         )
         self.assertEqual(400, self.request.response.getStatus())
 
@@ -445,7 +446,7 @@ def test_relationchoice_deserialization_from_invalid_url_raises(self):
             )
         self.assertEqual(
             str(cm.exception),
-            "Could not resolve object for URL=http://nohost/plone/doesnotexist",
+            "Could not resolve object for http://nohost/plone/doesnotexist (resolved by URL)",
         )
         self.assertEqual(400, self.request.response.getStatus())
 
@@ -453,10 +454,17 @@ def test_relationchoice_deserialization_from_invalid_path_raises(self):
         with self.assertRaises(ValueError) as cm:
             self.deserialize("test_relationchoice_field", "/doesnotexist")
         self.assertEqual(
-            str(cm.exception), "Could not resolve object for path=/doesnotexist"
+            str(cm.exception),
+            "Could not resolve object for /doesnotexist (resolved by path)",
         )
         self.assertEqual(400, self.request.response.getStatus())
 
+    def test_relationchoice_deserialization_from_wrong_type_raises(self):
+        with self.assertRaises(ValueError) as cm:
+            self.deserialize("test_relationchoice_field", None)
+        self.assertEqual(str(cm.exception), "Could not resolve object for None")
+        self.assertEqual(400, self.request.response.getStatus())
+
     def test_relationlist_deserialization_returns_list_of_documents(self):
         doc2 = self.portal[
             self.portal.invokeFactory(