Repository: dapr/python-sdk

Issue:
Support default json serializer in DaprClient and add .json helper
## Describe the proposal

In [the convenient layer spec](https://github.com/dapr/python-sdk/issues/67), DaprClient doesn't have the default json serializer, but let users to handle JSON serialization. Like the other python pkgs, such as flask, requests, etc, DaprClient can have default json serializer to serialize and deserialize python dict type or primitive objects and provide .json() helper to DaprResponse and DataContent in AppCallback Layer. It will improve development experience instead of serializing and deserializing data object.

Discussion:
codecov[bot]: # [Codecov](https://codecov.io/gh/dapr/python-sdk/pull/293?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=dapr) Report
> Merging [#293](https://codecov.io/gh/dapr/python-sdk/pull/293?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=dapr) (2ce2e36) into [master](https://codecov.io/gh/dapr/python-sdk/commit/33e7d48ffbed9b175ff095d18247f328e990d4ca?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=dapr) (33e7d48) will **decrease** coverage by `0.47%`.
> The diff coverage is `88.88%`.

[![Impacted file tree graph](https://codecov.io/gh/dapr/python-sdk/pull/293/graphs/tree.svg?width=650&height=150&src=pr&token=JTTIYXA9JP&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=dapr)](https://codecov.io/gh/dapr/python-sdk/pull/293?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=dapr)

```diff
@@            Coverage Diff             @@
##           master     #293      +/-   ##
==========================================
- Coverage   89.38%   88.90%   -0.48%     
==========================================
  Files          52       52              
  Lines        2147     2155       +8     
==========================================
- Hits         1919     1916       -3     
- Misses        228      239      +11     
```


| [Impacted Files](https://codecov.io/gh/dapr/python-sdk/pull/293?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=dapr) | Coverage Δ | |
|---|---|---|
| [dapr/clients/grpc/\_response.py](https://codecov.io/gh/dapr/python-sdk/pull/293/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=dapr#diff-ZGFwci9jbGllbnRzL2dycGMvX3Jlc3BvbnNlLnB5) | `94.63% <88.88%> (+0.34%)` | :arrow_up: |
| [ext/flask\_dapr/flask\_dapr/actor.py](https://codecov.io/gh/dapr/python-sdk/pull/293/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=dapr#diff-ZXh0L2ZsYXNrX2RhcHIvZmxhc2tfZGFwci9hY3Rvci5weQ==) | `50.00% <0.00%> (-7.96%)` | :arrow_down: |
| [ext/dapr-ext-fastapi/dapr/ext/fastapi/actor.py](https://codecov.io/gh/dapr/python-sdk/pull/293/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=dapr#diff-ZXh0L2RhcHItZXh0LWZhc3RhcGkvZGFwci9leHQvZmFzdGFwaS9hY3Rvci5weQ==) | `55.81% <0.00%> (-3.49%)` | :arrow_down: |
| [dapr/actor/runtime/\_type\_utils.py](https://codecov.io/gh/dapr/python-sdk/pull/293/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=dapr#diff-ZGFwci9hY3Rvci9ydW50aW1lL190eXBlX3V0aWxzLnB5) | `95.45% <0.00%> (-2.28%)` | :arrow_down: |
| [dapr/clients/grpc/client.py](https://codecov.io/gh/dapr/python-sdk/pull/293/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=dapr#diff-ZGFwci9jbGllbnRzL2dycGMvY2xpZW50LnB5) | `91.19% <0.00%> (-0.06%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/dapr/python-sdk/pull/293?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=dapr).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=dapr)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/dapr/python-sdk/pull/293?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=dapr). Last update [33e7d48...2ce2e36](https://codecov.io/gh/dapr/python-sdk/pull/293?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=dapr). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=dapr).


Patch:
diff --git a/dapr/clients/grpc/_response.py b/dapr/clients/grpc/_response.py
index 383a67a..d037a3f 100644
--- a/dapr/clients/grpc/_response.py
+++ b/dapr/clients/grpc/_response.py
@@ -20,6 +20,8 @@ from dapr.clients.grpc._helpers import (
     unpack,
 )
 
+import json
+
 
 class DaprResponse:
     """A base class for Dapr Response.
@@ -153,6 +155,15 @@ class InvokeMethodResponse(DaprResponse):
         """
         return to_str(self.data)
 
+    def json(self) -> Dict[str, object]:
+        """Gets the content as json if the response data content is not a serialized
+        protocol buffer message.
+
+        Returns:
+            str: [description]
+        """
+        return json.loads(to_str(self.data))
+
     @property
     def content_type(self) -> Optional[str]:
         """Gets the content type of content attribute."""
@@ -233,6 +244,10 @@ class BindingResponse(DaprResponse):
         """Gets content as str."""
         return to_str(self._data)
 
+    def json(self) -> Dict[str, object]:
+        """Gets content as deserialized JSON dictionary."""
+        return json.loads(to_str(self._data))
+
     @property
     def data(self) -> bytes:
         """Gets raw bytes data."""
@@ -340,6 +355,10 @@ class StateResponse(DaprResponse):
         """Gets content as str."""
         return to_str(self._data)
 
+    def json(self) -> Dict[str, object]:
+        """Gets content as deserialized JSON dictionary."""
+        return json.loads(to_str(self._data))
+
     @property
     def etag(self) -> str:
         """Gets etag."""
@@ -389,6 +408,10 @@ class BulkStateItem:
         """Gets content as str."""
         return to_str(self._data)
 
+    def json(self) -> Dict[str, object]:
+        """Gets content as deserialized JSON dictionary."""
+        return json.loads(to_str(self._data))
+
     @property
     def key(self) -> str:
         """Gets key."""


Test patch:
diff --git a/tests/clients/test_dapr_grpc_response.py b/tests/clients/test_dapr_grpc_response.py
index 20d5fc4..afdabe6 100644
--- a/tests/clients/test_dapr_grpc_response.py
+++ b/tests/clients/test_dapr_grpc_response.py
@@ -9,7 +9,11 @@ import unittest
 
 from google.protobuf.any_pb2 import Any as GrpcAny
 
-from dapr.clients.grpc._response import DaprResponse, InvokeMethodResponse, BindingResponse
+from dapr.clients.grpc._response import (
+    DaprResponse, InvokeMethodResponse, BindingResponse, StateResponse,
+    BulkStateItem
+)
+
 from dapr.proto import common_v1
 
 
@@ -64,6 +68,10 @@ class InvokeMethodResponseTests(unittest.TestCase):
         self.assertEqual('hello dapr', resp.text())
         self.assertEqual('application/json', resp.content_type)
 
+    def test_json_data(self):
+        resp = InvokeMethodResponse(data=b'{ "status": "ok" }', content_type='application/json')
+        self.assertEqual({'status': 'ok'}, resp.json())
+
     def test_unpack(self):
         # arrange
         fake_req = common_v1.InvokeRequest(method="test")
@@ -84,6 +92,10 @@ class InvokeBindingResponseTests(unittest.TestCase):
         self.assertEqual(b'data', resp.data)
         self.assertEqual('data', resp.text())
 
+    def test_json_data(self):
+        resp = BindingResponse(data=b'{"status": "ok"}', binding_metadata={})
+        self.assertEqual({'status': 'ok'}, resp.json())
+
     def test_metadata(self):
         resp = BindingResponse(data=b'data', binding_metadata={'status': 'ok'})
         self.assertEqual({'status': 'ok'}, resp.binding_metadata)
@@ -91,5 +103,22 @@ class InvokeBindingResponseTests(unittest.TestCase):
         self.assertEqual('data', resp.text())
 
 
+class StateResponseTests(unittest.TestCase):
+    def test_data(self):
+        resp = StateResponse(data=b'hello dapr')
+        self.assertEqual('hello dapr', resp.text())
+        self.assertEqual(b'hello dapr', resp.data)
+
+    def test_json_data(self):
+        resp = StateResponse(data=b'{"status": "ok"}')
+        self.assertEqual({'status': 'ok'}, resp.json())
+
+
+class BulkStateItemTests(unittest.TestCase):
+    def test_data(self):
+        item = BulkStateItem(key='item1', data=b'{ "status": "ok" }')
+        self.assertEqual({'status': 'ok'}, item.json())
+
+
 if __name__ == '__main__':
     unittest.main()