Fix inconsistent exception type in response.json() method
diff --git a/HISTORY.md b/HISTORY.md
index 59e4a9f707..ccf4e17400 100644
--- a/HISTORY.md
+++ b/HISTORY.md
@@ -4,7 +4,12 @@ Release History
 dev
 ---
 
--   \[Short description of non-trivial change.\]
+- \[Short description of non-trivial change.\]
+
+- Added a `requests.exceptions.JSONDecodeError` to decrease inconsistencies
+  in the library. This gets raised in the `response.json()` method, and is
+  backwards compatible as it inherits from previously thrown exceptions.
+  Can be caught from `requests.exceptions.RequestException` as well.
 
 2.26.0 (2021-07-13)
 -------------------
diff --git a/docs/user/quickstart.rst b/docs/user/quickstart.rst
index b4649f00d5..73d0b2931a 100644
--- a/docs/user/quickstart.rst
+++ b/docs/user/quickstart.rst
@@ -153,9 +153,9 @@ There's also a builtin JSON decoder, in case you're dealing with JSON data::
 
 In case the JSON decoding fails, ``r.json()`` raises an exception. For example, if
 the response gets a 204 (No Content), or if the response contains invalid JSON,
-attempting ``r.json()`` raises ``simplejson.JSONDecodeError`` if simplejson is
-installed or raises ``ValueError: No JSON object could be decoded`` on Python 2 or
-``json.JSONDecodeError`` on Python 3.
+attempting ``r.json()`` raises ``requests.exceptions.JSONDecodeError``. This wrapper exception
+provides interoperability for multiple exceptions that may be thrown by different
+python versions and json serialization libraries.
 
 It should be noted that the success of the call to ``r.json()`` does **not**
 indicate the success of the response. Some servers may return a JSON object in a
diff --git a/requests/__init__.py b/requests/__init__.py
index 0ac7713b81..53a5b42af6 100644
--- a/requests/__init__.py
+++ b/requests/__init__.py
@@ -139,7 +139,7 @@ def _check_cryptography(cryptography_version):
 from .exceptions import (
     RequestException, Timeout, URLRequired,
     TooManyRedirects, HTTPError, ConnectionError,
-    FileModeWarning, ConnectTimeout, ReadTimeout
+    FileModeWarning, ConnectTimeout, ReadTimeout, JSONDecodeError
 )
 
 # Set default logging handler to avoid "No handler found" warnings.
diff --git a/requests/compat.py b/requests/compat.py
index 0b14f5015c..029ae62ac3 100644
--- a/requests/compat.py
+++ b/requests/compat.py
@@ -28,8 +28,10 @@
 #: Python 3.x?
 is_py3 = (_ver[0] == 3)
 
+has_simplejson = False
 try:
     import simplejson as json
+    has_simplejson = True
 except ImportError:
     import json
 
@@ -49,13 +51,13 @@
     # Keep OrderedDict for backwards compatibility.
     from collections import Callable, Mapping, MutableMapping, OrderedDict
 
-
     builtin_str = str
     bytes = str
     str = unicode
     basestring = basestring
     numeric_types = (int, long, float)
     integer_types = (int, long)
+    JSONDecodeError = ValueError
 
 elif is_py3:
     from urllib.parse import urlparse, urlunparse, urljoin, urlsplit, urlencode, quote, unquote, quote_plus, unquote_plus, urldefrag
@@ -66,6 +68,10 @@
     # Keep OrderedDict for backwards compatibility.
     from collections import OrderedDict
     from collections.abc import Callable, Mapping, MutableMapping
+    if has_simplejson:
+        from simplejson import JSONDecodeError
+    else:
+        from json import JSONDecodeError
 
     builtin_str = str
     str = str
diff --git a/requests/exceptions.py b/requests/exceptions.py
index c412ec9868..957e31f384 100644
--- a/requests/exceptions.py
+++ b/requests/exceptions.py
@@ -8,6 +8,8 @@
 """
 from urllib3.exceptions import HTTPError as BaseHTTPError
 
+from .compat import JSONDecodeError as CompatJSONDecodeError
+
 
 class RequestException(IOError):
     """There was an ambiguous exception that occurred while handling your
@@ -29,6 +31,10 @@ class InvalidJSONError(RequestException):
     """A JSON error occurred."""
 
 
+class JSONDecodeError(InvalidJSONError, CompatJSONDecodeError):
+    """Couldn't decode the text into json"""
+
+
 class HTTPError(RequestException):
     """An HTTP error occurred."""
 
diff --git a/requests/models.py b/requests/models.py
index aa6fb86e4e..e7d292d580 100644
--- a/requests/models.py
+++ b/requests/models.py
@@ -29,7 +29,9 @@
 from .cookies import cookiejar_from_dict, get_cookie_header, _copy_cookie_jar
 from .exceptions import (
     HTTPError, MissingSchema, InvalidURL, ChunkedEncodingError,
-    ContentDecodingError, ConnectionError, StreamConsumedError, InvalidJSONError)
+    ContentDecodingError, ConnectionError, StreamConsumedError,
+    InvalidJSONError)
+from .exceptions import JSONDecodeError as RequestsJSONDecodeError
 from ._internal_utils import to_native_string, unicode_is_ascii
 from .utils import (
     guess_filename, get_auth_from_url, requote_uri,
@@ -38,7 +40,7 @@
 from .compat import (
     Callable, Mapping,
     cookielib, urlunparse, urlsplit, urlencode, str, bytes,
-    is_py2, chardet, builtin_str, basestring)
+    is_py2, chardet, builtin_str, basestring, JSONDecodeError)
 from .compat import json as complexjson
 from .status_codes import codes
 
@@ -468,9 +470,9 @@ def prepare_body(self, data, files, json=None):
             content_type = 'application/json'
 
             try:
-              body = complexjson.dumps(json, allow_nan=False)
+                body = complexjson.dumps(json, allow_nan=False)
             except ValueError as ve:
-              raise InvalidJSONError(ve, request=self)
+                raise InvalidJSONError(ve, request=self)
 
             if not isinstance(body, bytes):
                 body = body.encode('utf-8')
@@ -882,12 +884,8 @@ def json(self, **kwargs):
         r"""Returns the json-encoded content of a response, if any.
 
         :param \*\*kwargs: Optional arguments that ``json.loads`` takes.
-        :raises simplejson.JSONDecodeError: If the response body does not
-            contain valid json and simplejson is installed.
-        :raises json.JSONDecodeError: If the response body does not contain
-            valid json and simplejson is not installed on Python 3.
-        :raises ValueError: If the response body does not contain valid
-            json and simplejson is not installed on Python 2.        
+        :raises requests.exceptions.JSONDecodeError: If the response body does not
+            contain valid json.
         """
 
         if not self.encoding and self.content and len(self.content) > 3:
@@ -907,7 +905,16 @@ def json(self, **kwargs):
                     # and the server didn't bother to tell us what codec *was*
                     # used.
                     pass
-        return complexjson.loads(self.text, **kwargs)
+
+        try:
+            return complexjson.loads(self.text, **kwargs)
+        except JSONDecodeError as e:
+            # Catch JSON-related errors and raise as requests.JSONDecodeError
+            # This aliases json.JSONDecodeError and simplejson.JSONDecodeError
+            if is_py2: # e is a ValueError
+                raise RequestsJSONDecodeError(e.message)
+            else:
+                raise RequestsJSONDecodeError(e.msg, e.doc, e.pos)
 
     @property
     def links(self):
diff --git a/tests/test_requests.py b/tests/test_requests.py
index b77cba007d..b6d97dd9f4 100644
--- a/tests/test_requests.py
+++ b/tests/test_requests.py
@@ -2570,4 +2570,9 @@ def test_parameters_for_nonstandard_schemes(self, input, params, expected):
     def test_post_json_nan(self, httpbin):
         data = {"foo": float("nan")}
         with pytest.raises(requests.exceptions.InvalidJSONError):
-          r = requests.post(httpbin('post'), json=data)
\ No newline at end of file
+          r = requests.post(httpbin('post'), json=data)
+
+    def test_json_decode_compatibility(self, httpbin):
+        r = requests.get(httpbin('bytes/20'))
+        with pytest.raises(requests.exceptions.JSONDecodeError):
+            r.json()
\ No newline at end of file

## Summary
Fixes the inconsistency of errors thrown in the `response.json()` method in `models.py`, while preserving backwards compatibility. 

### What we used to have
Depending on whether or not `simplejson` was installed in the user's library, and whether the user was running Python 3+ versus Python 2, a different exception would get thrown when there was an issue with decoding response text as JSON. If `simplejson` was installed, `simplejson.JSONDecodeError` would get thrown. If not, `json.JSONDecodeError` would get thrown for Python 3+ users, and `ValueError` for Python 2 users. Thus, depending on the scenario, users would find themselves having to catch either `simplejson.JSONDecodeError`, `json.JSONDecodeError`, or `ValueError` in the case of the `response.json()` method failing. This inconsistency is not ideal.

### What we have now
There is now one error class in `exceptions.py` that will represent all errors that can be thrown or caught in the `response.json()` method: `requests.JSONDecodeError`. All `simplejson` functionality was replaced with `json` functionality, but the new error type aliases both `json.JSONDecodeError` and `simplejson.JSONDecodeError`. If `simplejson` is not installed, its `JSONDecodError` is replaced with a plain `Exception` when aliased. If the user is running Python 3+, `json.JSONDecodeError` is aliased, but if the user is running Python 2, that is replaced with `ValueError` when aliased, as the `JSONDecodeError` was not previously a part of the `json` library. Now, all five error types, `json.JSONDecodeError`, `simplejson.JSONDecodeError`, `ValueError`, and `requests.JSONDecodeError` and its parent class `requests.RequestException`, will be caught from the `requests.JSONDecodeError` that will be raised.

Fixes #5794
@sigmavirus24 