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

Issue:
resolveuid deserializer doesn't handle lists or nested dicts
The resloveuid deserializer currently makes the assumption that the `url` or `href` field is in the root of the block data, or it has been created by a volto_object_browser that is set to `single` mode. This means that making use of nested or lists of block data or lists (such as with the `object_list` widget in Volto) doesn't cause the field to be converted to use `resolveuid`.

For example, the following block data:

```json
{
  "123": {
    "@type": "foo",
    "my_values": [
      {
        "url": "/news"
      }
    ]
  }
}
```

Should end up being stored as this:

```json
{
  "123": {
    "@type": "foo",
    "my_values": [
      {
        "url": "../resolveuid/{doc_uid}"
      }
    ]
  }
}
```

But instead is saved as in the original


Discussion / hints:

Fix patch:
diff --git a/news/1595.feature b/news/1595.feature
new file mode 100644
index 0000000000..1ed3c42b51
--- /dev/null
+++ b/news/1595.feature
@@ -0,0 +1,2 @@
+Added support for nested schemas with resolveuid deserializer.
+[JeffersonBledsoe]
diff --git a/src/plone/restapi/deserializer/blocks.py b/src/plone/restapi/deserializer/blocks.py
index b5a7573c01..0fe4dfb044 100644
--- a/src/plone/restapi/deserializer/blocks.py
+++ b/src/plone/restapi/deserializer/blocks.py
@@ -107,27 +107,30 @@ def __init__(self, context, request):
 
     def __call__(self, block):
         # Convert absolute links to resolveuid
-        for field in self.fields:
-            link = block.get(field, "")
-            if link and isinstance(link, str):
-                block[field] = path2uid(context=self.context, link=link)
-            elif link and isinstance(link, list):
-                # Detect if it has an object inside with an "@id" key (object_widget)
-                if len(link) > 0 and isinstance(link[0], dict) and "@id" in link[0]:
-                    result = []
-                    for item in link:
-                        item_clone = deepcopy(item)
-                        item_clone["@id"] = path2uid(
-                            context=self.context, link=item_clone["@id"]
-                        )
-                        result.append(item_clone)
-
-                    block[field] = result
-                elif len(link) > 0 and isinstance(link[0], str):
-                    block[field] = [
-                        path2uid(context=self.context, link=item) for item in link
-                    ]
-        return block
+        return self._process_data(block)
+
+    def _process_data(self, data, field=None):
+        if isinstance(data, str) and field in self.fields:
+            return path2uid(context=self.context, link=data)
+        if isinstance(data, list):
+            return [self._process_data(data=value, field=field) for value in data]
+        if isinstance(data, dict):
+            if data.get("@type", None) == "URL" and data.get("value", None):
+                data["value"] = path2uid(context=self.context, link=data["value"])
+            elif data.get("@id", None):
+                item_clone = deepcopy(data)
+                item_clone["@id"] = path2uid(
+                    context=self.context, link=item_clone["@id"]
+                )
+                return {
+                    field: self._process_data(data=value, field=field)
+                    for field, value in item_clone.items()
+                }
+            return {
+                field: self._process_data(data=value, field=field)
+                for field, value in data.items()
+            }
+        return data
 
 
 class TextBlockDeserializerBase:
@@ -162,7 +165,6 @@ def __init__(self, context, request):
         self.request = request
 
     def __call__(self, block):
-
         portal_transforms = api.portal.get_tool(name="portal_transforms")
         raw_html = block.get("html", "")
         data = portal_transforms.convertTo(
@@ -301,7 +303,6 @@ class SlateBlockDeserializerRoot(SlateBlockDeserializerBase):
 
 class SlateTableBlockTransformer(SlateBlockTransformer):
     def __call__(self, block):
-
         rows = block.get("table", {}).get("rows", [])
         for row in rows:
             cells = row.get("cells", [])
diff --git a/src/plone/restapi/serializer/blocks.py b/src/plone/restapi/serializer/blocks.py
index a53f1734e5..e239233ebd 100644
--- a/src/plone/restapi/serializer/blocks.py
+++ b/src/plone/restapi/serializer/blocks.py
@@ -97,24 +97,29 @@ def __init__(self, context, request):
         self.context = context
         self.request = request
 
-    def __call__(self, value):
-        for field in self.fields:
-            if field in value.keys():
-                link = value.get(field, "")
-                if isinstance(link, str):
-                    value[field] = uid_to_url(link)
-                elif isinstance(link, list):
-                    if len(link) > 0 and isinstance(link[0], dict) and "@id" in link[0]:
-                        result = []
-                        for item in link:
-                            item_clone = deepcopy(item)
-                            item_clone["@id"] = uid_to_url(item_clone["@id"])
-                            result.append(item_clone)
-
-                        value[field] = result
-                    elif len(link) > 0 and isinstance(link[0], str):
-                        value[field] = [uid_to_url(item) for item in link]
-        return value
+    def __call__(self, block):
+        return self._process_data(block)
+
+    def _process_data(self, data, field=None):
+        if isinstance(data, str) and field in self.fields:
+            return uid_to_url(data)
+        if isinstance(data, list):
+            return [self._process_data(data=value, field=field) for value in data]
+        if isinstance(data, dict):
+            if data.get("@type", None) == "URL" and data.get("value", None):
+                data["value"] = uid_to_url(data["value"])
+            elif data.get("@id", None):
+                item_clone = deepcopy(data)
+                item_clone["@id"] = uid_to_url(item_clone["@id"])
+                return {
+                    field: self._process_data(data=value, field=field)
+                    for field, value in item_clone.items()
+                }
+            return {
+                field: self._process_data(data=value, field=field)
+                for field, value in data.items()
+            }
+        return data
 
 
 class TextBlockSerializerBase:


Test patch:
diff --git a/src/plone/restapi/tests/test_blocks_deserializer.py b/src/plone/restapi/tests/test_blocks_deserializer.py
index 36f7a72d12..a51b984e94 100644
--- a/src/plone/restapi/tests/test_blocks_deserializer.py
+++ b/src/plone/restapi/tests/test_blocks_deserializer.py
@@ -269,6 +269,35 @@ def test_blocks_custom_block_doesnt_resolve_non_standard_fields(self):
             self.portal.doc1.absolute_url(),
         )
 
+    def test_blocks_custom_block_resolve_standard_fields_nested(self):
+        self.deserialize(
+            blocks={
+                "123": {
+                    "@type": "foo",
+                    "bar": [{"url": self.portal.doc1.absolute_url()}],
+                }
+            }
+        )
+        doc_uid = IUUID(self.portal.doc1)
+
+        self.assertEqual(
+            self.portal.doc1.blocks["123"]["bar"][0]["url"], f"../resolveuid/{doc_uid}"
+        )
+
+        self.deserialize(
+            blocks={
+                "123": {
+                    "@type": "foo",
+                    "bar": [{"href": self.portal.doc1.absolute_url()}],
+                }
+            }
+        )
+        doc_uid = IUUID(self.portal.doc1)
+
+        self.assertEqual(
+            self.portal.doc1.blocks["123"]["bar"][0]["href"], f"../resolveuid/{doc_uid}"
+        )
+
     def test_deserialize_blocks_smart_href_array_volto_object_browser(self):
         self.deserialize(
             blocks={
@@ -285,6 +314,22 @@ def test_deserialize_blocks_smart_href_array_volto_object_browser(self):
             f"../resolveuid/{doc_uid}",
         )
 
+    def test_deserialize_blocks_smart_href_array_volto_object_browser_nested(self):
+        self.deserialize(
+            blocks={
+                "123": {
+                    "@type": "foo",
+                    "bar": [{"href": [{"@id": self.portal.doc1.absolute_url()}]}],
+                }
+            }
+        )
+        doc_uid = IUUID(self.portal.doc1)
+
+        self.assertEqual(
+            self.portal.doc1.blocks["123"]["bar"][0]["href"][0]["@id"],
+            f"../resolveuid/{doc_uid}",
+        )
+
     def test_deserialize_blocks_smart_href_array(self):
         self.deserialize(
             blocks={