Repository: pandas-dev/pandas
Pull request: Raise `FileNotFoundError` in `read_json` if input looks like file path but file is missing
- [x] closes #29102
- [x] [Tests added and passed](https://pandas.pydata.org/pandas-docs/dev/development/contributing_codebase.html#writing-tests) if fixing a bug or adding a new feature
- [x] All [code checks passed](https://pandas.pydata.org/pandas-docs/dev/development/contributing_codebase.html#pre-commit).
- [x] Added an entry in the latest `doc/source/whatsnew/v1.5.0.rst` file if fixing a bug or adding a new feature.


Problem statement:
Misleading error messages when opening inexistent json file
When opening a json file that doesn't exist with `read_json`, we get a `ValueError` with error messages like:

```python
>>> import pandas
>>> pandas.read_json('no_file.json')
ValueError: Unexpected character found when decoding 'null'

>>> pandas.read_json('a_dir/no_file.json')
ValueError: Expected object or value
```

It'd probably be better to raise an exception like:
```
FileNotFoundError: No such file: 'no_file.json'
```

Hints from issue discussion:
Hi, @datapythonista can I take this up?
sure, thanks
Hi! Would like to work on this if it is still open.
take
Hey @sathyz if you don't finish it by the end of the month I think I'll give it a shot.
take
@devjeetr  Is this issue still open? Can I give a shot?
Hi, I would like to to take this issue if it's still not resolved.
take
Anyone willing to take this up might want to go through this thread once. 
https://github.com/pandas-dev/pandas/pull/29104
I had taken this 2 years back, but it was closed after some discussion.

Commit message: add changelog entry in doc/source/whatsnew/v1.5.0.rst

Patch to review:
diff --git a/doc/source/whatsnew/v1.5.0.rst b/doc/source/whatsnew/v1.5.0.rst
index 4920622a15f3f..f11bc718fe300 100644
--- a/doc/source/whatsnew/v1.5.0.rst
+++ b/doc/source/whatsnew/v1.5.0.rst
@@ -566,6 +566,7 @@ I/O
 - Bug in Parquet roundtrip for Interval dtype with ``datetime64[ns]`` subtype (:issue:`45881`)
 - Bug in :func:`read_excel` when reading a ``.ods`` file with newlines between xml elements (:issue:`45598`)
 - Bug in :func:`read_parquet` when ``engine="fastparquet"`` where the file was not closed on error (:issue:`46555`)
+- :func:`read_json` now raises ``FileNotFoundError`` (previously ``ValueError``) when input is a string ending in one of ``.json``, ``.json.gz``, ``.json.bz2`` but no such file exists. (:issue:`29102`)
 
 Period
 ^^^^^^
diff --git a/pandas/io/json/_json.py b/pandas/io/json/_json.py
index 2a9ed9f15cd11..3dfbc54543dc0 100644
--- a/pandas/io/json/_json.py
+++ b/pandas/io/json/_json.py
@@ -698,6 +698,9 @@ def _get_data_from_filepath(self, filepath_or_buffer):
 
         This method turns (1) into (2) to simplify the rest of the processing.
         It returns input types (2) and (3) unchanged.
+
+        It raises FileNotFoundError if the input is a string ending in
+        one of .json, .json.gz, .json.bz2 but no such file exists.
         """
         # if it is a string but the file does not exist, it might be a JSON string
         filepath_or_buffer = stringify_path(filepath_or_buffer)
@@ -716,6 +719,12 @@ def _get_data_from_filepath(self, filepath_or_buffer):
                 errors=self.encoding_errors,
             )
             filepath_or_buffer = self.handles.handle
+        elif (
+            isinstance(filepath_or_buffer, str)
+            and filepath_or_buffer.lower().endswith((".json", ".json.gz", ".json.bz2"))
+            and not file_exists(filepath_or_buffer)
+        ):
+            raise FileNotFoundError(f"File {filepath_or_buffer} does not exist")
 
         return filepath_or_buffer
 
diff --git a/pandas/tests/io/test_common.py b/pandas/tests/io/test_common.py
index ca6809470b2b1..ee563c82d2695 100644
--- a/pandas/tests/io/test_common.py
+++ b/pandas/tests/io/test_common.py
@@ -187,7 +187,7 @@ def test_iterator(self):
             (pd.read_hdf, "tables", FileNotFoundError, "h5"),
             (pd.read_stata, "os", FileNotFoundError, "dta"),
             (pd.read_sas, "os", FileNotFoundError, "sas7bdat"),
-            (pd.read_json, "os", ValueError, "json"),
+            (pd.read_json, "os", FileNotFoundError, "json"),
             (pd.read_pickle, "os", FileNotFoundError, "pickle"),
         ],
     )
@@ -253,7 +253,7 @@ def test_write_missing_parent_directory(self, method, module, error_class, fn_ex
             (pd.read_hdf, "tables", FileNotFoundError, "h5"),
             (pd.read_stata, "os", FileNotFoundError, "dta"),
             (pd.read_sas, "os", FileNotFoundError, "sas7bdat"),
-            (pd.read_json, "os", ValueError, "json"),
+            (pd.read_json, "os", FileNotFoundError, "json"),
             (pd.read_pickle, "os", FileNotFoundError, "pickle"),
         ],
     )


Code review comments:
diff_hunk: @@ -716,6 +719,12 @@ def _get_data_from_filepath(self, filepath_or_buffer):
                 errors=self.encoding_errors,
             )
             filepath_or_buffer = self.handles.handle
+        elif (
+            isinstance(filepath_or_buffer, str)
+            and filepath_or_buffer.lower().endswith((".json", ".json.gz", ".json.bz2"))
path: pandas/io/json/_json.py
text: @user1:
We have a list of all supported compression extensions:
```py
from pandas.io.common import _compression_to_extension

.endswith([".json"] +  [f".json.{compression}" for compression in _compression_to_extension])
```

@author:
Oh good, I was looking for that.