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
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.
 

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"}
```
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):

pip install -e .
parse_log_pytest
requirements.txt
nose ddt testtools pytest
apt-get update apt-get install -y gcc
3.5
tools/test-requires
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
attrs==22.2.0
certifi==2021.5.30
charset-normalizer==2.0.12
coverage==6.2
ddt==1.7.2
-e git+https://github.com/falconry/falcon.git@2177a7a44c62f9159125049b232eb3cf668a69cd#egg=falcon
fixtures==4.0.1
idna==3.10
importlib-metadata==4.8.3
iniconfig==1.1.1
nose==1.3.7
packaging==21.3
pbr==6.1.1
pluggy==1.0.0
py==1.11.0
pyparsing==3.1.4
pytest==7.0.1
python-mimeparse==1.6.0
PyYAML==6.0.1
requests==2.27.1
six==1.17.0
testtools==2.6.0
tomli==1.2.3
typing_extensions==4.1.1
urllib3==1.26.20
zipp==3.6.0

name: falcon
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - certifi=2021.5.30=py36h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.3=he6710b0_2
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=1.1.1w=h7f8727e_0
  - pip=21.2.2=py36h06a4308_0
  - python=3.6.13=h12debd9_1
  - readline=8.2=h5eee18b_0
  - setuptools=58.0.4=py36h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - wheel=0.37.1=pyhd3eb1b0_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - attrs==22.2.0
      - charset-normalizer==2.0.12
      - coverage==6.2
      - ddt==1.7.2
      - fixtures==4.0.1
      - idna==3.10
      - importlib-metadata==4.8.3
      - iniconfig==1.1.1
      - nose==1.3.7
      - packaging==21.3
      - pbr==6.1.1
      - pluggy==1.0.0
      - py==1.11.0
      - pyparsing==3.1.4
      - pytest==7.0.1
      - python-mimeparse==1.6.0
      - pyyaml==6.0.1
      - requests==2.27.1
      - six==1.17.0
      - testtools==2.6.0
      - tomli==1.2.3
      - typing-extensions==4.1.1
      - urllib3==1.26.20
      - zipp==3.6.0
prefix: /opt/conda/envs/falcon

tests/test_query_params.py::_TestQueryParams::test_get_dict_invalid tests/test_query_params.py::_TestQueryParams::test_get_dict_missing_param tests/test_query_params.py::_TestQueryParams::test_get_dict_store tests/test_query_params.py::_TestQueryParams::test_get_dict_valid tests/test_query_params.py::PostQueryParams::test_get_dict_invalid tests/test_query_params.py::PostQueryParams::test_get_dict_missing_param tests/test_query_params.py::PostQueryParams::test_get_dict_store tests/test_query_params.py::PostQueryParams::test_get_dict_valid tests/test_query_params.py::GetQueryParams::test_get_dict_invalid tests/test_query_params.py::GetQueryParams::test_get_dict_missing_param tests/test_query_params.py::GetQueryParams::test_get_dict_store tests/test_query_params.py::GetQueryParams::test_get_dict_valid
tests/test_query_params.py::_TestQueryParams::test_allowed_names tests/test_query_params.py::_TestQueryParams::test_bad_percentage tests/test_query_params.py::_TestQueryParams::test_blank tests/test_query_params.py::_TestQueryParams::test_boolean tests/test_query_params.py::_TestQueryParams::test_boolean_blank tests/test_query_params.py::_TestQueryParams::test_get_date_invalid tests/test_query_params.py::_TestQueryParams::test_get_date_missing_param tests/test_query_params.py::_TestQueryParams::test_get_date_store tests/test_query_params.py::_TestQueryParams::test_get_date_valid tests/test_query_params.py::_TestQueryParams::test_get_date_valid_with_format tests/test_query_params.py::_TestQueryParams::test_int tests/test_query_params.py::_TestQueryParams::test_int_neg tests/test_query_params.py::_TestQueryParams::test_list_transformer tests/test_query_params.py::_TestQueryParams::test_list_type tests/test_query_params.py::_TestQueryParams::test_list_type_blank tests/test_query_params.py::_TestQueryParams::test_multiple_form_keys tests/test_query_params.py::_TestQueryParams::test_multiple_form_keys_as_list tests/test_query_params.py::_TestQueryParams::test_multiple_keys_as_bool tests/test_query_params.py::_TestQueryParams::test_multiple_keys_as_int tests/test_query_params.py::_TestQueryParams::test_none tests/test_query_params.py::_TestQueryParams::test_param_property tests/test_query_params.py::_TestQueryParams::test_percent_encoded tests/test_query_params.py::_TestQueryParams::test_required_1_get_param tests/test_query_params.py::_TestQueryParams::test_required_2_get_param_as_int tests/test_query_params.py::_TestQueryParams::test_required_3_get_param_as_bool tests/test_query_params.py::_TestQueryParams::test_required_4_get_param_as_list tests/test_query_params.py::_TestQueryParams::test_simple tests/test_query_params.py::PostQueryParams::test_allowed_names tests/test_query_params.py::PostQueryParams::test_bad_percentage tests/test_query_params.py::PostQueryParams::test_blank tests/test_query_params.py::PostQueryParams::test_boolean tests/test_query_params.py::PostQueryParams::test_boolean_blank tests/test_query_params.py::PostQueryParams::test_explicitly_disable_auto_parse tests/test_query_params.py::PostQueryParams::test_get_date_invalid tests/test_query_params.py::PostQueryParams::test_get_date_missing_param tests/test_query_params.py::PostQueryParams::test_get_date_store tests/test_query_params.py::PostQueryParams::test_get_date_valid tests/test_query_params.py::PostQueryParams::test_get_date_valid_with_format tests/test_query_params.py::PostQueryParams::test_int tests/test_query_params.py::PostQueryParams::test_int_neg tests/test_query_params.py::PostQueryParams::test_list_transformer tests/test_query_params.py::PostQueryParams::test_list_type tests/test_query_params.py::PostQueryParams::test_list_type_blank tests/test_query_params.py::PostQueryParams::test_multiple_form_keys tests/test_query_params.py::PostQueryParams::test_multiple_form_keys_as_list tests/test_query_params.py::PostQueryParams::test_multiple_keys_as_bool tests/test_query_params.py::PostQueryParams::test_multiple_keys_as_int tests/test_query_params.py::PostQueryParams::test_non_ascii tests/test_query_params.py::PostQueryParams::test_none tests/test_query_params.py::PostQueryParams::test_param_property tests/test_query_params.py::PostQueryParams::test_percent_encoded tests/test_query_params.py::PostQueryParams::test_required_1_get_param tests/test_query_params.py::PostQueryParams::test_required_2_get_param_as_int tests/test_query_params.py::PostQueryParams::test_required_3_get_param_as_bool tests/test_query_params.py::PostQueryParams::test_required_4_get_param_as_list tests/test_query_params.py::PostQueryParams::test_simple tests/test_query_params.py::GetQueryParams::test_allowed_names tests/test_query_params.py::GetQueryParams::test_bad_percentage tests/test_query_params.py::GetQueryParams::test_blank tests/test_query_params.py::GetQueryParams::test_boolean tests/test_query_params.py::GetQueryParams::test_boolean_blank tests/test_query_params.py::GetQueryParams::test_get_date_invalid tests/test_query_params.py::GetQueryParams::test_get_date_missing_param tests/test_query_params.py::GetQueryParams::test_get_date_store tests/test_query_params.py::GetQueryParams::test_get_date_valid tests/test_query_params.py::GetQueryParams::test_get_date_valid_with_format tests/test_query_params.py::GetQueryParams::test_int tests/test_query_params.py::GetQueryParams::test_int_neg tests/test_query_params.py::GetQueryParams::test_list_transformer tests/test_query_params.py::GetQueryParams::test_list_type tests/test_query_params.py::GetQueryParams::test_list_type_blank tests/test_query_params.py::GetQueryParams::test_multiple_form_keys tests/test_query_params.py::GetQueryParams::test_multiple_form_keys_as_list tests/test_query_params.py::GetQueryParams::test_multiple_keys_as_bool tests/test_query_params.py::GetQueryParams::test_multiple_keys_as_int tests/test_query_params.py::GetQueryParams::test_none tests/test_query_params.py::GetQueryParams::test_param_property tests/test_query_params.py::GetQueryParams::test_percent_encoded tests/test_query_params.py::GetQueryParams::test_required_1_get_param tests/test_query_params.py::GetQueryParams::test_required_2_get_param_as_int tests/test_query_params.py::GetQueryParams::test_required_3_get_param_as_bool tests/test_query_params.py::GetQueryParams::test_required_4_get_param_as_list tests/test_query_params.py::GetQueryParams::test_simple tests/test_query_params.py::PostQueryParamsDefaultBehavior::test_dont_auto_parse_by_default
falcon/request.py
falcon/request.py