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

Issue:
User with datetime schema field is not serialized correctly
If you add a datetime field to a userschema, it is saved as a "DateTime" object, but if later you request to serialize that object you get a "Datetime object is not JSON serializable" error.

The point is that the @users endpoint doesn't convert the values to JSON compatible values using the existing json_compatible function.


Discussion / hints:

Fix patch:
diff --git a/news/1493.bugfix b/news/1493.bugfix
new file mode 100644
index 0000000000..4def9665a0
--- /dev/null
+++ b/news/1493.bugfix
@@ -0,0 +1,2 @@
+Use json_compatible when serializing users in @users endpoint
+[erral]
diff --git a/src/plone/restapi/serializer/user.py b/src/plone/restapi/serializer/user.py
index bb37113f92..b02fdfe32a 100644
--- a/src/plone/restapi/serializer/user.py
+++ b/src/plone/restapi/serializer/user.py
@@ -1,6 +1,8 @@
+from plone.app.users.browser.userdatapanel import getUserDataSchema
+from plone.restapi.batching import HypermediaBatch
 from plone.restapi.interfaces import ISerializeToJson
 from plone.restapi.interfaces import ISerializeToJsonSummary
-from plone.restapi.batching import HypermediaBatch
+from plone.restapi.serializer.converters import json_compatible
 from Products.CMFCore.interfaces._tools import IMemberData
 from Products.CMFCore.utils import getToolByName
 from Products.CMFPlone.utils import safe_unicode
@@ -9,7 +11,6 @@
 from zope.interface import implementer
 from zope.publisher.interfaces import IRequest
 from zope.schema import getFieldNames
-from plone.app.users.browser.userdatapanel import getUserDataSchema
 
 
 class BaseSerializer:
@@ -53,7 +54,7 @@ def __call__(self):
                     value = None
                 if value:
                     value = safe_unicode(value)
-            data[name] = value
+            data[name] = json_compatible(value)
 
         return data
 


Test patch:
diff --git a/src/plone/restapi/tests/test_serializer_user.py b/src/plone/restapi/tests/test_serializer_user.py
index cf5ffefc5d..4983fdf040 100644
--- a/src/plone/restapi/tests/test_serializer_user.py
+++ b/src/plone/restapi/tests/test_serializer_user.py
@@ -1,7 +1,9 @@
+from DateTime import DateTime
 from plone import api
 from plone.restapi.interfaces import ISerializeToJson
 from plone.restapi.testing import PLONE_RESTAPI_DX_INTEGRATION_TESTING
 from zope.component import getMultiAdapter
+from plone.app.users.browser.schemaeditor import applySchema
 
 import unittest
 
@@ -76,3 +78,61 @@ def test_serialize_custom_member_schema(self):
         res = self.serialize(user)
         self.assertIn("twitter", res)
         self.assertEqual(res["twitter"], "TheRealDuck")
+
+
+class TestSerializeUserCustomSchemaToJsonAdapter(unittest.TestCase):
+
+    layer = PLONE_RESTAPI_DX_INTEGRATION_TESTING
+
+    def setUp(self):
+        self.app = self.layer["app"]
+        self.portal = self.layer["portal"]
+        self.request = self.layer["request"]
+        member_schema = """
+            <model xmlns="http://namespaces.plone.org/supermodel/schema"
+                xmlns:form="http://namespaces.plone.org/supermodel/form"
+                xmlns:users="http://namespaces.plone.org/supermodel/users"
+                xmlns:i18n="http://xml.zope.org/namespaces/i18n"
+                i18n:domain="plone">
+              <schema name="member-fields">
+                <field name="birthdate" type="zope.schema.Date"
+                         users:forms="In User Profile">
+                  <description i18n:translate="help_birthdate">
+                    Birthdate
+                  </description>
+                  <required>False</required>
+                  <title i18n:translate="label_birthdate">Birthdate</title>
+                </field>
+                <field name="registration_datetime" type="zope.schema.Datetime"
+                         users:forms="In User Profile">
+                  <description i18n:translate="help_registration_datetime">
+                    Registration datetime
+                  </description>
+                  <required>False</required>
+                  <title i18n:translate="label_registration_datetime">Registration datetime</title>
+                </field>
+              </schema>
+            </model>
+        """
+        applySchema(member_schema)
+        self.user = api.user.create(
+            email="donald.duck@example.com",
+            username="donald",
+            properties={
+                "birthdate": DateTime("2022-01-10"),
+                "registration_datetime": DateTime("2022-01-10 14:00:00"),
+            },
+        )
+
+    def serialize(self, user):
+        serializer = getMultiAdapter((user, self.request), ISerializeToJson)
+        return serializer()
+
+    def test_serialize_with_datetime(self):
+        """test that when we have a datetime field in user schema, the response is serialized correctly"""
+
+        res = self.serialize(self.user)
+
+        self.assertIn("birthdate", res)
+        self.assertEqual(res["birthdate"], "2022-01-10T00:00:00")
+        self.assertEqual(res["registration_datetime"], "2022-01-10T14:00:00")
diff --git a/src/plone/restapi/tests/test_services_users.py b/src/plone/restapi/tests/test_services_users.py
index ccc8161dd6..23fbd8fc61 100644
--- a/src/plone/restapi/tests/test_services_users.py
+++ b/src/plone/restapi/tests/test_services_users.py
@@ -1,3 +1,4 @@
+from DateTime import DateTime
 from plone import api
 from plone.app.testing import setRoles
 from plone.app.testing import SITE_OWNER_NAME
@@ -69,9 +70,10 @@ def tearDown(self):
         self.anon_api_session.close()
 
     def makeRealImage(self):
-        import Products.PlonePAS as ppas
         from Products.PlonePAS.tests import dummy
 
+        import Products.PlonePAS as ppas
+
         pas_path = os.path.dirname(ppas.__file__)
         path = os.path.join(pas_path, "tool.gif")
         image = open(path, "rb")
@@ -1008,3 +1010,51 @@ def test_get_user_portrait_anonymous(self):
 
         self.assertEqual(200, response.status_code)
         self.assertEqual(response.headers["Content-Type"], "image/gif")
+
+    def test_user_with_datetime(self):
+        """test that when using a datetime field in the user schema
+        the endpoints works correctly
+        """
+        from plone.app.users.browser.schemaeditor import applySchema
+
+        member_schema = """
+            <model xmlns="http://namespaces.plone.org/supermodel/schema"
+                xmlns:form="http://namespaces.plone.org/supermodel/form"
+                xmlns:users="http://namespaces.plone.org/supermodel/users"
+                xmlns:i18n="http://xml.zope.org/namespaces/i18n"
+                i18n:domain="plone">
+              <schema name="member-fields">
+                <field name="birthdate" type="zope.schema.Date"
+                         users:forms="In User Profile">
+                  <description i18n:translate="help_birthdate">
+                    Birthdate
+                  </description>
+                  <required>False</required>
+                  <title i18n:translate="label_birthdate">Birthdate</title>
+                </field>
+                <field name="registration_datetime" type="zope.schema.Datetime"
+                         users:forms="In User Profile">
+                  <description i18n:translate="help_registration_datetime">
+                    Registration datetime
+                  </description>
+                  <required>False</required>
+                  <title i18n:translate="label_registration_datetime">Registration datetime</title>
+                </field>
+              </schema>
+            </model>
+        """
+        applySchema(member_schema)
+        api.user.create(
+            email="donald.duck@example.com",
+            username="donald",
+            properties={
+                "birthdate": DateTime("2022-01-10"),
+                "registration_datetime": DateTime("2022-01-10 14:00:00"),
+            },
+        )
+        transaction.commit()
+
+        response = self.api_session.get("/@users/donald")
+        self.assertEqual(200, response.status_code)
+        self.assertIn("birthdate", response.json())
+        self.assertIn("registration_datetime", response.json())