Repository: voxel51/fiftyone
Issue: [BUG] views created by select_objects/exclude_objects fail to serialize

### System information

-   **OS Platform and Distribution (e.g., Linux Ubuntu 16.04)**: Ubuntu 18.04
-   **FiftyOne installed from (pip or source)**: source
-   **FiftyOne version (run `fiftyone --version`)**: 0.6.3 (85c01425)
-   **Python version**: 3.6

### Commands to reproduce

On `obj-selection` (currently at 85c01425), select some objects in the app, then run:

```python
import fiftyone.utils.selection as fous
session.view = fous.select_objects(dataset, session.selected_objects)
session.view = fous.exclude_objects(dataset, session.selected_objects)
```

### Describe the problem

Both of the above views trigger the following error:

```
Uncaught exception
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "fiftyone/core/client.py", line 155, in __setattr__
    super().__setattr__(name, value)
  File "fiftyone/core/session.py", line 85, in wrapper
    self._update_state()
  File "fiftyone/core/session.py", line 313, in _update_state
    self.state = self.state
  File "fiftyone/core/client.py", line 153, in __setattr__
    self._hc_client.update(value)
  File "fiftyone/core/client.py", line 101, in update
    self.emit("update", {"data": data.serialize(), "include_self": False})
  File "/lib/python3.6/site-packages/socketio/namespace.py", line 171, in emit
    callback=callback)
  File "/lib/python3.6/site-packages/socketio/client.py", line 344, in emit
    binary=binary))
  File "/lib/python3.6/site-packages/socketio/client.py", line 452, 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 "/usr/lib/python3.6/json/__init__.py", line 238, in dumps
    **kw).encode(obj)
  File "/usr/lib/python3.6/json/encoder.py", line 199, in encode
    chunks = self.iterencode(o, _one_shot=True)
  File "/usr/lib/python3.6/json/encoder.py", line 257, in iterencode
    return _iterencode(o, 0)
  File "/usr/lib/python3.6/json/encoder.py", line 180, in default
    o.__class__.__name__)
TypeError: Object of type 'ObjectId' is not JSON serializable
```

### Other info / logs

This was likely introduced in the original implementation, #616 

I was also able to reproduce on develop by obtaining a sample/field ID manually from the app, so this is not specific to obj-selection:
```python
session.view = fous.exclude_objects(dataset, [{'sample_id': '5f96f32da6350af28e420e65', 'object_id': '5f96f32da6350af28e420ec9', 'field': 'fieldname'}])
```

### What areas of FiftyOne does this bug affect?

-   [ ] `App`: FiftyOne application issue
-   [x] `Core`: Core `fiftyone` Python library issue
-   [ ] `Server`: Fiftyone server issue


Changed files:
fiftyone/utils/selection.py, fiftyone/core/stages.py, fiftyone/core/expressions.py, fiftyone/core/labels.py, fiftyone/core/odm/document.py

diff --git a/fiftyone/core/expressions.py b/fiftyone/core/expressions.py
index 5c484c14c..1c3ed756c 100644
--- a/fiftyone/core/expressions.py
+++ b/fiftyone/core/expressions.py
@@ -8,6 +8,8 @@ Expressions for :class:`fiftyone.core.stages.ViewStage` definitions.
 from copy import deepcopy
 import re
 
+import bson
+
 import eta.core.utils as etau
 
 import fiftyone.core.utils as fou
@@ -795,6 +797,42 @@ class ViewField(ViewExpression, metaclass=_MetaViewField):
         return "$" + self._expr if self._expr else "$this"
 
 
+class ObjectId(ViewExpression, metaclass=_MetaViewField):
+    """A :class:`ViewExpression` that refers to an
+    `ObjectId <https://docs.mongodb.com/manual/reference/method/ObjectId>`_ of
+    a document.
+
+    The typical use case for this class is writing an expression that involves
+    checking if the ID of a document matches a particular known ID.
+
+    Example::
+
+        from fiftyone import ViewField as F
+        from fiftyone.core.expressions import ObjectId
+
+        # Check if the ID of the document matches the given ID
+        expr = F("_id") == ObjectId("5f452489ef00e6374aad384a")
+
+    Args:
+        oid: the object ID string
+    """
+
+    def __init__(self, oid):
+        _ = bson.ObjectId(oid)  # validates that `oid` is valid value
+        super().__init__(oid)
+
+    def to_mongo(self, prefix=None):
+        """Returns a MongoDB representation of the ObjectId.
+
+        Args:
+            prefix (None): unused
+
+        Returns:
+            a MongoDB expression
+        """
+        return {"$toObjectId": self._expr}
+
+
 def _escape_regex_chars(str_or_strs):
     # Must escape `[`, `]`, `-`, and `\\` because they have special meaning
     # inside the `[]` that will be used in the replacement regex
diff --git a/fiftyone/core/labels.py b/fiftyone/core/labels.py
index 667296e20..e02848421 100644
--- a/fiftyone/core/labels.py
+++ b/fiftyone/core/labels.py
@@ -5,7 +5,7 @@ Labels stored in dataset samples.
 | `voxel51.com <https://voxel51.com/>`_
 |
 """
-from bson.objectid import ObjectId
+from bson import ObjectId
 
 import eta.core.data as etad
 import eta.core.geometry as etag
diff --git a/fiftyone/core/odm/document.py b/fiftyone/core/odm/document.py
index bda3a8881..3f3507bbd 100644
--- a/fiftyone/core/odm/document.py
+++ b/fiftyone/core/odm/document.py
@@ -9,8 +9,7 @@ from copy import deepcopy
 import json
 import re
 
-from bson import json_util
-from bson.objectid import ObjectId
+from bson import json_util, ObjectId
 import mongoengine
 import pymongo
 
diff --git a/fiftyone/core/stages.py b/fiftyone/core/stages.py
index 2010f32e7..e33e455b2 100644
--- a/fiftyone/core/stages.py
+++ b/fiftyone/core/stages.py
@@ -108,9 +108,9 @@ class ViewStage(object):
             self._uuid = str(uuid.uuid4())
 
         return {
-            "kwargs": self._kwargs(),
             "_cls": etau.get_class_name(self),
             "_uuid": self._uuid,
+            "kwargs": self._kwargs(),
         }
 
     def _kwargs(self):
diff --git a/fiftyone/utils/selection.py b/fiftyone/utils/selection.py
index 23cdb83f9..f1c8dd383 100644
--- a/fiftyone/utils/selection.py
+++ b/fiftyone/utils/selection.py
@@ -8,10 +8,9 @@ Utilities for selecting content from datasets.
 from collections import defaultdict
 import warnings
 
-from bson import ObjectId
-
 import fiftyone.core.fields as fof
 import fiftyone.core.labels as fol
+from fiftyone.core.expressions import ObjectId
 from fiftyone.core.expressions import ViewField as F
 
 
@@ -85,7 +84,7 @@ def select_objects(sample_collection, objects):
     view = view.select_fields(list(object_ids.keys()))
 
     for field, object_ids in object_ids.items():
-        label_filter = F("_id").is_in(object_ids)
+        label_filter = F("_id").is_in([ObjectId(oid) for oid in object_ids])
         view = _apply_label_filter(view, label_schema, field, label_filter)
 
     return view
@@ -128,7 +127,7 @@ def exclude_objects(sample_collection, objects):
 
     view = sample_collection
     for field, object_ids in object_ids.items():
-        label_filter = ~F("_id").is_in(object_ids)
+        label_filter = ~F("_id").is_in([ObjectId(oid) for oid in object_ids])
         view = _apply_label_filter(view, label_schema, field, label_filter)
 
     return view
@@ -139,7 +138,7 @@ def _parse_objects(objects):
     object_ids = defaultdict(set)
     for obj in objects:
         sample_ids.add(obj["sample_id"])
-        object_ids[obj["field"]].add(ObjectId(obj["object_id"]))
+        object_ids[obj["field"]].add(obj["object_id"])
 
     return sample_ids, object_ids