Repository: creare-com/podpac

Issue:
Custom JSONEncoder for serialization
**Description**
Node and Coordinate objects need to be serializable.

**Describe the solution you'd like**
The Node and Coordinate classes contain a `definition` property and a `json` property that serializes this definition. We should use a custom JSONEncoder to handle unserializable objects and use it to implement the `json` property. The JSONEncoder needs to handle *at least*:

 * numpy array (numbers, datetime64, timedelta64)
 * np.datetime64 and np.timedelta64
 * Coordinates (for when they are a Node attribute)
 * Interpolator (for when they are a Node attribute)

**Sample Code**

The custom JSONEncoder is partially implemented in the podpac utils.

```
class JSONEncoder(json.JSONEncoder):
    def default(self, obj):
        # numpy arrays
        if isinstance(obj, np.ndarray):
            if np.issubdtype(obj.dtype, np.datetime64):
                return obj.astype(str).tolist()
            elif np.issubdtype(obj.dtype, np.timedelta64):
                f = np.vectorize(podpac.core.coordinates.utils.make_timedelta_string)
                return f(obj).tolist()
            elif np.issubdtype(obj.dtype, np.number):
                return obj.tolist()
        
        # datetime64
        elif isinstance(obj, np.datetime64):
            return obj.astype(str)

        # timedelta64
        elif isinstance(obj, np.timedelta64):
            return podpac.core.coordinates.utils.make_timedelta_string(obj)
        
        # default
        return json.JSONEncoder.default(self, obj)
```

The Coordinates are using this JSONEncoder.

```
    @property
    def json(self):
        return json.dumps(self.definition, cls=podpac.core.utils.JSONEncoder)
```

This means for example that ArrayCoordinates1d no longer have to convert numpy arrays to a list manually in their `definition` property.

**See Also**

#177 Interpolation attributes should be serializable
#135 Array data_source


Discussion:

Patch:
diff --git a/podpac/core/data/types.py b/podpac/core/data/types.py
index 36d41583..23f7c663 100644
--- a/podpac/core/data/types.py
+++ b/podpac/core/data/types.py
@@ -83,6 +83,17 @@ class Array(DataSource):
         d = self.create_output_array(coordinates, data=self.source[s])
         return d
 
+class NumpyArray(Array):
+    """Create a DataSource from a numpy array.
+
+    .. deprecated:: 0.2.0
+          `NumpyArray` will be removed in podpac 0.2.0, it is replaced by `Array`.
+    """
+
+    def init(self):
+        warnings.warn('NumpyArray been renamed Array. ' +
+                      'Backwards compatibility will be removed in future releases', DeprecationWarning)
+
 
 @common_doc(COMMON_DATA_DOC)
 class PyDAP(DataSource):
diff --git a/podpac/core/node.py b/podpac/core/node.py
index 46eaef77..482a36ac 100644
--- a/podpac/core/node.py
+++ b/podpac/core/node.py
@@ -298,19 +298,16 @@ class Node(tl.HasTraits):
 
             attr = getattr(self, key)
 
+            # check serializable
+            try:
+                json.dumps(attr, cls=JSONEncoder)
+            except:
+                raise NodeException("Cannot serialize attr '%s' with type '%s'" % (key, type(attr)))
+            
             if isinstance(attr, Node):
                 lookup_attrs[key] = attr
-            elif isinstance(attr, np.ndarray):
-                attrs[key] = attr.tolist()
-            elif isinstance(attr, Coordinates):
-                attrs[key] = attr.definition
             else:
-                try:
-                    json.dumps(attr, cls=JSONEncoder)
-                except:
-                    raise NodeException("Cannot serialize attr '%s' with type '%s'" % (key, type(attr)))
-                else:
-                    attrs[key] = attr
+                attrs[key] = attr
 
         if attrs:
             d['attrs'] = OrderedDict([(key, attrs[key]) for key in sorted(attrs.keys())])
diff --git a/podpac/core/utils.py b/podpac/core/utils.py
index 2d274afb..e73db6c3 100644
--- a/podpac/core/utils.py
+++ b/podpac/core/utils.py
@@ -229,8 +229,16 @@ class ArrayTrait(tl.TraitType):
 
 class JSONEncoder(json.JSONEncoder):
     def default(self, obj):
+        # podpac Coordinates objects
+        if isinstance(obj, podpac.Coordinates):
+            return obj.definition
+
+        # podpac Coordinates objects
+        elif isinstance(obj, podpac.Node):
+            return obj.definition
+
         # numpy arrays
-        if isinstance(obj, np.ndarray):
+        elif isinstance(obj, np.ndarray):
             if np.issubdtype(obj.dtype, np.datetime64):
                 return obj.astype(str).tolist()
             elif np.issubdtype(obj.dtype, np.timedelta64):


Test patch:
diff --git a/podpac/core/data/test/test_types.py b/podpac/core/data/test/test_types.py
index b8c5cb57..b00c9112 100644
--- a/podpac/core/data/test/test_types.py
+++ b/podpac/core/data/test/test_types.py
@@ -644,9 +644,7 @@ class TestReprojectedSource(object):
 
         node = ReprojectedSource(source=self.source, reprojected_coordinates=self.reprojected_coordinates)
         d = node.base_definition
-        c = Coordinates.from_definition(d['attrs']['reprojected_coordinates'])
-        
-        assert c == self.reprojected_coordinates
+        assert d['attrs']['reprojected_coordinates'] == self.reprojected_coordinates
 
     def test_deserialize_reprojected_coordinates(self):
         node1 = ReprojectedSource(source=self.source, reprojected_coordinates=self.reprojected_coordinates)
diff --git a/podpac/core/test/test_node.py b/podpac/core/test/test_node.py
index 25c3d435..f91c1ac0 100644
--- a/podpac/core/test/test_node.py
+++ b/podpac/core/test/test_node.py
@@ -58,9 +58,7 @@ class TestNode(object):
 
         node = N(my_attr=podpac.Coordinates([[0, 1], [1, 2, 3]], dims=['lat', 'lon']))
         d = node.base_definition
-        my_attr = podpac.Coordinates.from_definition(d['attrs']['my_attr'])
-        
-        assert my_attr == node.my_attr
+        assert d['attrs']['my_attr'] == node.my_attr
 
     def test_base_definition_unserializable(self):
         class N(Node):