Repository: falconry/falcon

Issue:
JSON validator - change exception class
I found a little bit harder to override the exception thrown by falcon.media.validators.jsonschema.validate(). When I raise an instance of HTTPBadRequest is really easy to override it to get my custom error message.

Looking at the code it seems like the exception class is hardcoded, so it will always be the pristine HTTPBadRequest that comes with falcon, not mine, so the error is not myapi-compilant.

¿Is there another way of doing this by not duplicating the validator code to raise my own exception?



Discussion:
You can rewrite the decorator.
Just copy past it and change HTTPBadREquest by MYHTTPCOmpliantBadREquest

or maybe propose a PR adding, an optional argument "CustomError" to the decorator.

Resolving patch:
diff --git a/falcon/errors.py b/falcon/errors.py
--- a/falcon/errors.py
+++ b/falcon/errors.py
@@ -84,6 +84,7 @@ def on_get(self, req, resp):
     'HTTPVersionNotSupported',
     'MediaMalformedError',
     'MediaNotFoundError',
+    'MediaValidationError',
     'OperationNotAllowed',
     'PayloadTypeError',
     'UnsupportedError',
@@ -2458,6 +2459,60 @@ def description(self, value):
         pass
 
 
+class MediaValidationError(HTTPBadRequest):
+    """400 Bad Request.
+
+    Request media is invalid. This exception is raised by a media validator
+    (such as
+    :func:`jsonschema.validate <falcon.media.validators.jsonschema.validate>`)
+    when ``req.media`` is successfully deserialized, but fails to validate
+    against the configured schema.
+
+    The cause of this exception, if any, is stored in the ``__cause__``
+    attribute using the "raise ... from" form when raising.
+
+    Note:
+        All the arguments must be passed as keyword only.
+
+    Keyword Args:
+        title (str): Error title (default '400 Bad Request').
+        description (str): Human-friendly description of the error, along with
+            a helpful suggestion or two.
+        headers (dict or list): A ``dict`` of header names and values
+            to set, or a ``list`` of (*name*, *value*) tuples. Both *name* and
+            *value* must be of type ``str`` or ``StringType``, and only
+            character values 0x00 through 0xFF may be used on platforms that
+            use wide characters.
+
+            Note:
+                The Content-Type header, if present, will be overridden. If
+                you wish to return custom error messages, you can create
+                your own HTTP error class, and install an error handler
+                to convert it into an appropriate HTTP response for the
+                client
+
+            Note:
+                Falcon can process a list of ``tuple`` slightly faster
+                than a ``dict``.
+        href (str): A URL someone can visit to find out more information
+            (default ``None``). Unicode characters are percent-encoded.
+        href_text (str): If href is given, use this as the friendly
+            title/description for the link (default 'API documentation
+            for this error').
+        code (int): An internal code that customers can reference in their
+            support request or to help them when searching for knowledge
+            base articles related to this error (default ``None``).
+    """
+
+    def __init__(self, *, title=None, description=None, headers=None, **kwargs):
+        super().__init__(
+            title=title,
+            description=description,
+            headers=headers,
+            **kwargs,
+        )
+
+
 # -----------------------------------------------------------------------------
 # Helpers
 # -----------------------------------------------------------------------------
diff --git a/falcon/media/validators/jsonschema.py b/falcon/media/validators/jsonschema.py
--- a/falcon/media/validators/jsonschema.py
+++ b/falcon/media/validators/jsonschema.py
@@ -17,6 +17,14 @@ def validate(req_schema=None, resp_schema=None, is_async=False):
     the *format* keyword is enabled for the default checkers implemented
     by ``jsonschema.FormatChecker``.
 
+    In the case of failed request media validation, an instance of
+    :class:`~falcon.MediaValidationError` is raised by the decorator. By
+    default, this error is rendered as a 400 (:class:`~falcon.HTTPBadRequest`)
+    response with the ``title`` and ``description`` attributes explaining the
+    validation failure, but this behavior can be modified by adding a
+    custom error :func:`handler <falcon.App.add_error_handler>` for
+    :class:`~falcon.MediaValidationError`.
+
     Note:
         The ``jsonschema`` package must be installed separately in order to use
         this decorator, as Falcon does not install it by default.
@@ -108,10 +116,10 @@ def wrapper(self, req, resp, *args, **kwargs):
                 jsonschema.validate(
                     req.media, req_schema, format_checker=jsonschema.FormatChecker()
                 )
-            except jsonschema.ValidationError as e:
-                raise falcon.HTTPBadRequest(
-                    title='Request data failed validation', description=e.message
-                )
+            except jsonschema.ValidationError as ex:
+                raise falcon.MediaValidationError(
+                    title='Request data failed validation', description=ex.message
+                ) from ex
 
         result = func(self, req, resp, *args, **kwargs)
 
@@ -120,13 +128,13 @@ def wrapper(self, req, resp, *args, **kwargs):
                 jsonschema.validate(
                     resp.media, resp_schema, format_checker=jsonschema.FormatChecker()
                 )
-            except jsonschema.ValidationError:
+            except jsonschema.ValidationError as ex:
                 raise falcon.HTTPInternalServerError(
                     title='Response data failed validation'
                     # Do not return 'e.message' in the response to
                     # prevent info about possible internal response
                     # formatting bugs from leaking out to users.
-                )
+                ) from ex
 
         return result
 
@@ -143,10 +151,10 @@ async def wrapper(self, req, resp, *args, **kwargs):
                 jsonschema.validate(
                     m, req_schema, format_checker=jsonschema.FormatChecker()
                 )
-            except jsonschema.ValidationError as e:
-                raise falcon.HTTPBadRequest(
-                    title='Request data failed validation', description=e.message
-                )
+            except jsonschema.ValidationError as ex:
+                raise falcon.MediaValidationError(
+                    title='Request data failed validation', description=ex.message
+                ) from ex
 
         result = await func(self, req, resp, *args, **kwargs)
 
@@ -155,13 +163,13 @@ async def wrapper(self, req, resp, *args, **kwargs):
                 jsonschema.validate(
                     resp.media, resp_schema, format_checker=jsonschema.FormatChecker()
                 )
-            except jsonschema.ValidationError:
+            except jsonschema.ValidationError as ex:
                 raise falcon.HTTPInternalServerError(
                     title='Response data failed validation'
                     # Do not return 'e.message' in the response to
                     # prevent info about possible internal response
                     # formatting bugs from leaking out to users.
-                )
+                ) from ex
 
         return result
 


Test patch:
diff --git a/tests/test_validators.py b/tests/test_validators.py
--- a/tests/test_validators.py
+++ b/tests/test_validators.py
@@ -132,8 +132,11 @@ def test_req_schema_validation_success(asgi):
 
 
 @skip_missing_dep
-def test_req_schema_validation_failure(asgi):
-    with pytest.raises(falcon.HTTPBadRequest) as excinfo:
+@pytest.mark.parametrize(
+    'exception_cls', [falcon.HTTPBadRequest, falcon.MediaValidationError]
+)
+def test_req_schema_validation_failure(asgi, exception_cls):
+    with pytest.raises(exception_cls) as excinfo:
         call_method(asgi, 'request_validated', MockReq(asgi, False), None)
 
     assert excinfo.value.description == "'message' is a required property"