Repository: voxel51/fiftyone

Issue:
[BUG] numpy.array sample fields trigger server error when serialized
### System information

-   **OS Platform and Distribution (e.g., Linux Ubuntu 16.04)**: Ubuntu 20.04
-   **FiftyOne installed from (pip or source)**: source
-   **FiftyOne version (run `fiftyone --version`)**: v0.5.6 (e86c3570) - does not occur in v0.5.5
-   **Python version**: 3.6

### Commands to reproduce

1. Start server with `python fiftyone/server/main.py`
2. Start app with `yarn dev`
3. Run the code below

### Describe the problem

The server fails to serialize the sample (see traceback) and the sample does not display in the app.


### Code to reproduce issue
```python
import fiftyone as fo
import numpy as np
dataset = fo.Dataset()
dataset.add_sample(fo.Sample('/path/to/image', field=np.array([1,2,3])))
session = fo.launch_app(remote=True, dataset=dataset)
```

### Other info / logs

Probably introduced in #543, since that changed JSON encoding. Previously, this field was serialized as:
```
    "field": {
        "$binary": "eJyb7BfqGxDJyFDGUK2eklqcXKRupaBuk2mhrqOgnpZfVFKUmBefX5SSChJ3S8wpTgWKF2ckFqQC+RrGOpo6CrUKFAAuRgYIYILSzFAaAOdAG2c=",
        "$type": "00"
    }
```

Server traceback:
```
  File "/usr/lib/python3.6/threading.py", line 916, in _bootstrap_inner
    self.run()
  File "/usr/lib/python3.6/threading.py", line 864, in run
    self._target(*self._args, **self._kwargs)
  File ".../lib/python3.6/site-packages/socketio/server.py", line 696, in _handle_event_internal
    binary=binary))
  File ".../lib/python3.6/site-packages/socketio/server.py", line 607, in _send_packet
    encoded_packet = pkt.encode()
  File ".../lib/python3.6/site-packages/socketio/packet.py", line 71, in encode
    encoded_packet += self.json.dumps(data, separators=(',', ':'))
  File "/home/alan/code/fiftyone/fiftyone/server/json_util.py", line 47, in dumps
    json_util.dumps(*args, **kwargs), parse_constant=lambda c: c
  File ".../lib/python3.6/site-packages/bson/json_util.py", line 383, in dumps
    return json.dumps(_json_convert(obj, json_options), *args, **kwargs)
  File "/usr/lib/python3.6/json/__init__.py", line 238, in dumps
    **kw).encode(obj)
  File ".../lib/python3.6/site-packages/simplejson/encoder.py", line 275, in encode
    chunks = self.iterencode(o, _one_shot=True)
  File ".../lib/python3.6/site-packages/simplejson/encoder.py", line 357, in iterencode
    return _iterencode(o, 0)
UnicodeDecodeError: 'utf-8' codec can't decode byte 0x9c in position 1: invalid start byte
```

### What areas of FiftyOne does this bug affect?

-   [ ] `App`: FiftyOne application issue
-   [ ] `Core`: Core `fiftyone` Python library issue
-   [x] `Server`: Fiftyone server issue



Discussion:

Resolving patch:
diff --git a/fiftyone/server/json_util.py b/fiftyone/server/json_util.py
--- a/fiftyone/server/json_util.py
+++ b/fiftyone/server/json_util.py
@@ -10,6 +10,16 @@
 
 from fiftyone.core.sample import Sample, SampleView
 from fiftyone.core.stages import ViewStage
+import fiftyone.core.utils as fou
+
+
+def _handle_bytes(o):
+    for k, v in o.items():
+        if isinstance(v, bytes):
+            o[k] = str(fou.deserialize_numpy_array(v).shape)
+        if isinstance(v, dict):
+            o[k] = _handle_bytes(v)
+    return o
 
 
 class FiftyOneJSONEncoder(JSONEncoder):
@@ -29,7 +39,7 @@ def default(self, o):  # pylint: disable=E0202
             str
         """
         if isinstance(o, (Sample, SampleView)):
-            return o.to_mongo_dict()
+            return _handle_bytes(o.to_mongo_dict())
         if issubclass(type(o), ViewStage):
             return o._serialize()
         if isinstance(o, ObjectId):


Test patch:
diff --git a/tests/isolated/server_tests.py b/tests/isolated/server_tests.py
--- a/tests/isolated/server_tests.py
+++ b/tests/isolated/server_tests.py
@@ -18,6 +18,7 @@
 import urllib
 
 from bson import ObjectId
+import numpy as np
 import socketio
 
 import eta.core.utils as etau
@@ -31,6 +32,7 @@
     _subscribed_sessions,
 )
 from fiftyone.core.state import StateDescriptionWithDerivables
+import fiftyone.core.utils as fou
 from fiftyone.server.json_util import FiftyOneJSONEncoder
 
 
@@ -212,6 +214,8 @@ def step_json_encoder(self):
         self.assertEqual(enc.dumps(ObjectId(oid)), '{"$oid": "%s"}' % oid)
         self.assertEqual(enc.dumps(float("nan")), '"NaN"')
         self.assertEqual(enc.dumps(float("inf")), '"Infinity"')
+        sample = fo.Sample(filepath="/path.jpg", field=np.array([[1]]))
+        self.assertEqual(enc.loads(enc.dumps(sample))["field"], "(1, 1)")
 
     def test_steps(self):
         for name, step in self.steps():