Repository: falconry/falcon

Issue:
Implement get_param_as_dict()
This helper would deserialize JSON into a `dict`. For example:

`thing=%7B%22name%22%3A%20%22value%22%2C%20%22another%22%3A%20%22field%22%2C%20%22some%22%3A%20%22thing%22%7D`

...would be interpreted by req.get_param_as_dict as:

```json
{"name": "value", "another": "field", "some": "thing"}
```

Discussion:
codecov-io: ## [Current coverage][cc-pull] is **100%**
> Merging [#839][cc-pull] into [master][cc-base-branch] will not change coverage

```diff
@@           master   #839   diff @@
====================================
  Files          29     29          
  Lines        1790   1803    +13   
  Methods         0      0          
  Messages        0      0          
  Branches      299    301     +2   
====================================
+ Hits         1790   1803    +13   
  Misses          0      0          
  Partials        0      0          
```

> Powered by [Codecov](https://codecov.io?src=pr). Last updated by [2177a7a...e850a04][cc-compare]
[cc-base-branch]: https://codecov.io/gh/falconry/falcon/branch/master?src=pr
[cc-compare]: https://codecov.io/gh/falconry/falcon/compare/2177a7a44c62f9159125049b232eb3cf668a69cd...e850a04544bddfb08e0679719620d59b1ecf28e1
[cc-pull]: https://codecov.io/gh/falconry/falcon/pull/839?src=pr

Patch:
diff --git a/falcon/request.py b/falcon/request.py
index 53edeae..597ac80 100644
--- a/falcon/request.py
+++ b/falcon/request.py
@@ -11,6 +11,7 @@
 # limitations under the License.
 
 from datetime import datetime
+import json
 
 try:
     # NOTE(kgrifs): In Python 2.6 and 2.7, socket._fileobject is a
@@ -1030,7 +1031,7 @@ class Request(object):
         try:
             date = strptime(param_value, format_string).date()
         except ValueError:
-            msg = 'The date value does not match the required format'
+            msg = 'The date value does not match the required format.'
             raise errors.HTTPInvalidParam(msg, name)
 
         if store is not None:
@@ -1038,6 +1039,45 @@ class Request(object):
 
         return date
 
+    def get_param_as_dict(self, name, required=False, store=None):
+        """Return the value of a query string parameter as a dict.
+
+        Given a JSON value, parse and return it as a dict.
+
+        Args:
+            name (str): Parameter name, case-sensitive (e.g., 'payload').
+            required (bool, optional): Set to ``True`` to raise
+                ``HTTPBadRequest`` instead of returning ``None`` when the
+                parameter is not found (default ``False``).
+            store (dict, optional): A ``dict``-like object in which to place
+                the value of the param, but only if the param is found (default
+                ``None``).
+
+        Returns:
+            dict: The value of the param if it is found. Otherwise, returns
+                ``None`` unless required is ``True``.
+
+        Raises:
+            HTTPBadRequest: A required param is missing from the request.
+            HTTPInvalidParam: The parameter's value could not be parsed as JSON.
+        """
+
+        param_value = self.get_param(name, required=required)
+
+        if param_value is None:
+            return None
+
+        try:
+            val = json.loads(param_value)
+        except ValueError:
+            msg = 'It could not be parsed as JSON.'
+            raise errors.HTTPInvalidParam(msg, name)
+
+        if store is not None:
+            store[name] = val
+
+        return val
+
     def log_error(self, message):
         """Write an error message to the server's log.
 


Test patch:
diff --git a/tests/test_query_params.py b/tests/test_query_params.py
index bb01847..c588f23 100644
--- a/tests/test_query_params.py
+++ b/tests/test_query_params.py
@@ -1,4 +1,5 @@
 from datetime import date
+import json
 
 import ddt
 
@@ -473,6 +474,39 @@ class _TestQueryParams(testing.TestBase):
         self.assertRaises(HTTPInvalidParam, req.get_param_as_date,
                           'thedate', format_string=format_string)
 
+    def test_get_dict_valid(self):
+        payload_dict = {'foo': 'bar'}
+        query_string = 'payload={0}'.format(json.dumps(payload_dict))
+        self.simulate_request('/', query_string=query_string)
+        req = self.resource.req
+        self.assertEqual(req.get_param_as_dict('payload'),
+                         payload_dict)
+
+    def test_get_dict_missing_param(self):
+        payload_dict = {'foo': 'bar'}
+        query_string = 'notthepayload={0}'.format(json.dumps(payload_dict))
+        self.simulate_request('/', query_string=query_string)
+        req = self.resource.req
+        self.assertEqual(req.get_param_as_dict('payload'),
+                         None)
+
+    def test_get_dict_store(self):
+        payload_dict = {'foo': 'bar'}
+        query_string = 'payload={0}'.format(json.dumps(payload_dict))
+        self.simulate_request('/', query_string=query_string)
+        req = self.resource.req
+        store = {}
+        req.get_param_as_dict('payload', store=store)
+        self.assertNotEqual(len(store), 0)
+
+    def test_get_dict_invalid(self):
+        payload_dict = 'foobar'
+        query_string = 'payload={0}'.format(payload_dict)
+        self.simulate_request('/', query_string=query_string)
+        req = self.resource.req
+        self.assertRaises(HTTPInvalidParam, req.get_param_as_dict,
+                          'payload')
+
 
 class PostQueryParams(_TestQueryParams):
     def before(self):