Repository: modin-project/modin

Issue:
pd.read_json() does not support JSON string input
### System information
- **OS Platform and Distribution (e.g., Linux Ubuntu 16.04)**: MacOS 10.15.4
- **Modin version** (`modin.__version__`): 0.7.2
- **Python version**:3.7.7
- **Pandas version**: 1.0.1
- **Ray version**: 0.8.0
- **Code we can use to reproduce**:
```
import modin.pandas as pd
print(pd.read_json("""[{"name": "hamx0r"}]"""))
```
Related to #554 

### Describe the problem
Even though #715 adds read_json(), it is limited to:

1. Reading JSON files, and not strings (ie fetched from a web source)
2. Each row of the JSON file has to be a complete DataFrame row's worth of data (and all columns must be present in every row).

I would like to see the ability to read JSON strings too, or at least allow some kind or workaround using BytesIO or similar. Currently, modin fails even a simple BytesIO hack:

```
import modin.pandas as pd
from io import BytesIO
j = b"""[
{"name": "hamx0r"}
]"""
bio = BytesIO(j)
df = pd.read_json(bio, lines=True)
print(df.head())
```
...results in this error:

...

>   File "...modin/engines/base/io/file_reader.py", line 15, in get_path
>     if S3_ADDRESS_REGEX.search(file_path):
> TypeError: expected string or bytes-like object

The only workaround I've found is to load with regular pandas, then convert the dataframe to Modin:

```
import pandas
import modin.pandas as pd
j = """[{"name": "hamx0r"}]"""
df = pandas.read_json(j)
df = pd.DataFrame(df)
print(df.head())
```

### Source code / logs
This traceback comes from running the "Code we can use to reproduce" above:
```
Traceback (most recent call last):
  File "code.py", line 90, in runcode
    exec(code, self.locals)
  File "<input>", line 2, in <module>
  File "/modin/pandas/io.py", line 143, in read_json
    return DataFrame(query_compiler=BaseFactory.read_json(**kwargs))
  File "/modin/data_management/factories.py", line 60, in read_json
    return cls._determine_engine()._read_json(**kwargs)
  File "/modin/data_management/factories.py", line 64, in _read_json
    return cls.io_cls.read_json(**kwargs)
  File "/modin/engines/base/io/text/json_reader.py", line 13, in read
    return cls.single_worker_read(path_or_buf, **kwargs)
  File "/modin/backends/pandas/parsers.py", line 57, in single_worker_read
    pandas_frame = cls.parse(fname, **kwargs)
  File "/modin/backends/pandas/parsers.py", line 117, in parse
    return pandas.read_json(fname, **kwargs)
  File "/pandas/util/_decorators.py", line 186, in wrapper
    return func(*args, **kwargs)
  File "/pandas/io/json/_json.py", line 608, in read_json
    result = json_reader.read()
  File "/pandas/io/json/_json.py", line 731, in read
    obj = self._get_object_parser(self.data)
  File "/pandas/io/json/_json.py", line 753, in _get_object_parser
    obj = FrameParser(json, **kwargs).parse()
  File "/pandas/io/json/_json.py", line 857, in parse
    self._parse_no_numpy()
  File "/pandas/io/json/_json.py", line 1089, in _parse_no_numpy
    loads(json, precise_float=self.precise_float), dtype=None
ValueError: Expected object or value
```


Discussion:
Thanks @hamx0r for creating the issue! This is failing when trying to default to the pandas implementation, and the error earlier in your comment happens when we try to open the file. 

We will do our best to get this out in the next release, which is planned for next week.


Resolving patch:
diff --git a/modin/engines/base/io/text/csv_reader.py b/modin/engines/base/io/text/csv_reader.py
--- a/modin/engines/base/io/text/csv_reader.py
+++ b/modin/engines/base/io/text/csv_reader.py
@@ -18,24 +18,6 @@
 import sys
 
 
-def pathlib_or_pypath(filepath_or_buffer):
-    try:
-        import py
-
-        if isinstance(filepath_or_buffer, py.path.local):
-            return True
-    except ImportError:  # pragma: no cover
-        pass
-    try:
-        import pathlib
-
-        if isinstance(filepath_or_buffer, pathlib.Path):
-            return True
-    except ImportError:  # pragma: no cover
-        pass
-    return False
-
-
 class CSVReader(TextFileReader):
     @classmethod
     def read(cls, filepath_or_buffer, **kwargs):
@@ -43,7 +25,7 @@ def read(cls, filepath_or_buffer, **kwargs):
             if not cls.file_exists(filepath_or_buffer):
                 return cls.single_worker_read(filepath_or_buffer, **kwargs)
             filepath_or_buffer = cls.get_path(filepath_or_buffer)
-        elif not pathlib_or_pypath(filepath_or_buffer):
+        elif not cls.pathlib_or_pypath(filepath_or_buffer):
             return cls.single_worker_read(filepath_or_buffer, **kwargs)
         compression_type = cls.infer_compression(
             filepath_or_buffer, kwargs.get("compression")
diff --git a/modin/engines/base/io/text/json_reader.py b/modin/engines/base/io/text/json_reader.py
--- a/modin/engines/base/io/text/json_reader.py
+++ b/modin/engines/base/io/text/json_reader.py
@@ -21,7 +21,12 @@
 class JSONReader(TextFileReader):
     @classmethod
     def read(cls, path_or_buf, **kwargs):
-        path_or_buf = cls.get_path(path_or_buf)
+        if isinstance(path_or_buf, str):
+            if not cls.file_exists(path_or_buf):
+                return cls.single_worker_read(path_or_buf, **kwargs)
+            path_or_buf = cls.get_path(path_or_buf)
+        elif not cls.pathlib_or_pypath(path_or_buf):
+            return cls.single_worker_read(path_or_buf, **kwargs)
         if not kwargs.get("lines", False):
             return cls.single_worker_read(path_or_buf, **kwargs)
         columns = pandas.read_json(
diff --git a/modin/engines/base/io/text/text_file_reader.py b/modin/engines/base/io/text/text_file_reader.py
--- a/modin/engines/base/io/text/text_file_reader.py
+++ b/modin/engines/base/io/text/text_file_reader.py
@@ -52,3 +52,21 @@ def build_partition(cls, partition_ids, row_lengths, column_widths):
                 for i in range(len(partition_ids))
             ]
         )
+
+    @classmethod
+    def pathlib_or_pypath(cls, filepath_or_buffer):
+        try:
+            import py
+
+            if isinstance(filepath_or_buffer, py.path.local):
+                return True
+        except ImportError:  # pragma: no cover
+            pass
+        try:
+            import pathlib
+
+            if isinstance(filepath_or_buffer, pathlib.Path):
+                return True
+        except ImportError:  # pragma: no cover
+            pass
+        return False


Test patch:
diff --git a/modin/pandas/test/test_io.py b/modin/pandas/test/test_io.py
--- a/modin/pandas/test/test_io.py
+++ b/modin/pandas/test/test_io.py
@@ -23,7 +23,13 @@
 import shutil
 import sqlalchemy as sa
 
-from .utils import df_equals
+from .utils import (
+    df_equals,
+    json_short_string,
+    json_short_bytes,
+    json_long_string,
+    json_long_bytes,
+)
 
 from modin import __execution_engine__
 
@@ -456,6 +462,18 @@ def test_from_json_lines():
     teardown_json_file()
 
 
+@pytest.mark.parametrize(
+    "data", [json_short_string, json_short_bytes, json_long_string, json_long_bytes],
+)
+def test_read_json_string_bytes(data):
+    with pytest.warns(UserWarning):
+        modin_df = pd.read_json(data)
+    # For I/O objects we need to rewind to reuse the same object.
+    if hasattr(data, "seek"):
+        data.seek(0)
+    df_equals(modin_df, pandas.read_json(data))
+
+
 def test_from_html():
     setup_html_file(SMALL_ROW_SIZE)
 
diff --git a/modin/pandas/test/utils.py b/modin/pandas/test/utils.py
--- a/modin/pandas/test/utils.py
+++ b/modin/pandas/test/utils.py
@@ -16,6 +16,7 @@
 from pandas.util.testing import assert_almost_equal, assert_frame_equal
 import modin.pandas as pd
 from modin.pandas.utils import to_pandas
+from io import BytesIO
 
 random_state = np.random.RandomState(seed=42)
 
@@ -300,6 +301,48 @@
 
 # END parametrizations of common kwargs
 
+json_short_string = """[{"project": "modin"}]"""
+json_long_string = """{
+        "quiz": {
+            "sport": {
+                "q1": {
+                    "question": "Which one is correct team name in NBA?",
+                    "options": [
+                        "New York Bulls",
+                        "Los Angeles Kings",
+                        "Golden State Warriros",
+                        "Huston Rocket"
+                    ],
+                    "answer": "Huston Rocket"
+                }
+            },
+            "maths": {
+                "q1": {
+                    "question": "5 + 7 = ?",
+                    "options": [
+                        "10",
+                        "11",
+                        "12",
+                        "13"
+                    ],
+                    "answer": "12"
+                },
+                "q2": {
+                    "question": "12 - 8 = ?",
+                    "options": [
+                        "1",
+                        "2",
+                        "3",
+                        "4"
+                    ],
+                    "answer": "4"
+                }
+            }
+        }
+    }"""
+json_long_bytes = BytesIO(json_long_string.encode(encoding="UTF-8"))
+json_short_bytes = BytesIO(json_short_string.encode(encoding="UTF-8"))
+
 
 def df_equals(df1, df2):
     """Tests if df1 and df2 are equal.