Repository: Qiskit/qiskit

Issue:
serialization rules need to be extended
<!-- ⚠️ If you do not respect this template, your issue will be closed -->
<!-- ⚠️ Make sure to browse the opened and closed issues -->

### Information

- **Qiskit Terra version**:
- **Python version**:
- **Operating system**:

### What is the current behavior?
#1959 reintroduced the specialized methods for serializing different data types. However a couple were missed.

```
if isinstance(obj, numpy.ndarray):
  return cls._expand_item(obj.tolist())

if hasattr(obj, 'as_dict'):
  return obj.as_dict()
```

This is currently causing Aer master to fail.



Discussion:

Resolving patch:
diff --git a/qiskit/validation/fields/custom.py b/qiskit/validation/fields/custom.py
--- a/qiskit/validation/fields/custom.py
+++ b/qiskit/validation/fields/custom.py
@@ -51,8 +51,8 @@ class InstructionParameter(ModelTypeValidator):
 
     This field provides support for parsing objects of types that uses by
     qobj.experiments.instructions.parameters:
-    * basic Python types: complex, int, float, str
-    * ``numpy``: integer, float
+    * basic Python types: complex, int, float, str, list
+    * ``numpy``: integer, float, ndarray
     * ``sympy``: Symbol, Basic
 
     Note that by using this field, serialization-deserialization round-tripping
@@ -60,31 +60,63 @@ class InstructionParameter(ModelTypeValidator):
     type (for example, numpy.float and regular float). If possible, it is
     recommended that more specific and defined fields are used instead.
     """
+
     valid_types = (complex, int, float, str,
-                   numpy.integer, numpy.float, sympy.Basic, sympy.Symbol)
+                   numpy.integer, numpy.float, sympy.Basic, sympy.Symbol,
+                   list, numpy.ndarray)
+
+    default_error_messages = {
+        'invalid': '{input} cannot be parsed as a parameter.',
+        'format': '"{input}" cannot be formatted as a parameter.'
+    }
 
     def _serialize(self, value, attr, obj):
         # pylint: disable=too-many-return-statements
-        if isinstance(value, (float, int, str)):
-            return value
+        if is_collection(value):
+            return [self._serialize(item, attr, obj) for item in value]
+
         if isinstance(value, complex):
             return [value.real, value.imag]
         if isinstance(value, numpy.integer):
             return int(value)
         if isinstance(value, numpy.float):
             return float(value)
+        if isinstance(value, (float, int, str)):
+            return value
         if isinstance(value, sympy.Symbol):
             return str(value)
         if isinstance(value, sympy.Basic):
             if value.is_imaginary:
                 return [float(sympy.re(value)), float(sympy.im(value))]
+            if value.is_Integer:
+                return int(value.evalf())
             else:
                 return float(value.evalf())
 
-        return self.fail('invalid', input=value)
+        # Fallback for attempting serialization.
+        if hasattr(value, 'as_dict'):
+            return value.as_dict()
+
+        return self.fail('format', input=value)
 
     def _deserialize(self, value, attr, data):
-        if is_collection(value) and len(value) != 2:
-            return complex(*value)
+        if is_collection(value):
+            return [self._deserialize(item, attr, data) for item in value]
+
+        if isinstance(value, (float, int, str)):
+            return value
+
+        return self.fail('invalid', input=value)
+
+    def check_type(self, value, attr, data):
+        """Customize check_type for handling containers."""
+        # Check the type in the standard way first, in order to fail quickly
+        # in case of invalid values.
+        root_value = super(InstructionParameter, self).check_type(
+            value, attr, data)
+
+        if is_collection(value):
+            _ = [super(InstructionParameter, self).check_type(item, attr, data)
+                 for item in value]
 
-        return value
+        return root_value


Test patch:
diff --git a/test/python/validation/test_custom_fields.py b/test/python/validation/test_custom_fields.py
new file mode 100644
--- /dev/null
+++ b/test/python/validation/test_custom_fields.py
@@ -0,0 +1,116 @@
+# -*- coding: utf-8 -*-
+
+# Copyright 2019, IBM.
+#
+# This source code is licensed under the Apache License, Version 2.0 found in
+# the LICENSE.txt file in the root directory of this source tree.
+
+"""Test terra custom fields."""
+
+import numpy
+import sympy
+
+from qiskit.test import QiskitTestCase
+from qiskit.validation import fields
+from qiskit.validation.base import BaseModel, BaseSchema, bind_schema
+from qiskit.validation.exceptions import ModelValidationError
+
+
+class DogSchema(BaseSchema):
+    """Example Dog schema."""
+    barking = fields.InstructionParameter(required=True)
+
+
+@bind_schema(DogSchema)
+class Dog(BaseModel):
+    """Example Dog model."""
+    pass
+
+
+class TestFields(QiskitTestCase):
+    """Tests for Fields."""
+
+    def test_parameter_field_float(self):
+        """Test InstructionParameter with types equivalent to float."""
+        dog = Dog(barking=0.1)
+        dog_numpy = Dog(barking=numpy.dtype('float').type(0.1))
+        dog_sympy = Dog(barking=sympy.Float(0.1))
+
+        self.assertEqual(dog_numpy.barking,
+                         numpy.dtype('float').type(0.1))
+        self.assertEqual(dog.to_dict(), dog_numpy.to_dict())
+        self.assertEqual(dog.to_dict(), dog_sympy.to_dict())
+
+    def test_parameter_field_int(self):
+        """Test InstructionParameter with types equivalent to int."""
+        dog = Dog(barking=1)
+        dog_numpy = Dog(barking=numpy.dtype('int').type(1))
+        dog_sympy = Dog(barking=sympy.Integer(1))
+
+        self.assertEqual(dog_numpy.barking,
+                         numpy.dtype('int').type(1))
+        self.assertEqual(dog.to_dict(), dog_numpy.to_dict())
+        self.assertEqual(dog.to_dict(), dog_sympy.to_dict())
+
+    def test_parameter_field_str(self):
+        """Test InstructionParameter with types equivalent to str."""
+        dog = Dog(barking='woof')
+        dog_sympy = Dog(barking=sympy.Symbol('woof'))
+
+        self.assertEqual(dog_sympy.barking, sympy.Symbol('woof'))
+        self.assertEqual(dog.to_dict(), dog_sympy.to_dict())
+
+    def test_parameter_field_container(self):
+        """Test InstructionParameter with container types."""
+        dog = Dog(barking=[0.1, 2.3])
+        dog_numpy = Dog(barking=numpy.array([0.1, 2.3]))
+        dog_sympy = Dog(barking=[sympy.Float(0.1), sympy.Float(2.3)])
+        dog_complex = Dog(barking=complex(0.1, 2.3))
+
+        self.assertEqual(dog.to_dict(), dog_numpy.to_dict())
+        self.assertEqual(dog.to_dict(), dog_sympy.to_dict())
+        self.assertEqual(dog.to_dict(), dog_complex.to_dict())
+
+    def test_parameter_field_container_nested(self):
+        """Test InstructionParameter with nested container types."""
+        dog = Dog(barking=[[1, 2]])
+        dog_numpy = Dog(barking=numpy.array([[1, 2]]))
+        dog_sympy = Dog(barking=[[sympy.Integer(1),
+                                  sympy.Integer(2)]])
+
+        self.assertEqual(dog.to_dict(), dog_numpy.to_dict())
+        self.assertEqual(dog.to_dict(), dog_sympy.to_dict())
+
+    def test_parameter_field_from_dict(self):
+        """Test InstructionParameter from_dict."""
+        dog = Dog.from_dict({'barking': [0.1, 2]})
+        self.assertEqual(dog.barking, [0.1, 2])
+
+    def test_parameter_field_invalid(self):
+        """Test InstructionParameter invalid values."""
+        with self.assertRaises(ModelValidationError) as context_manager:
+            _ = Dog(barking={})
+        self.assertIn('barking', str(context_manager.exception))
+
+        with self.assertRaises(ModelValidationError) as context_manager:
+            # Invalid types inside containers are also not allowed.
+            _ = Dog(barking=[{}])
+        self.assertIn('barking', str(context_manager.exception))
+
+    def test_parameter_field_from_dict_invalid(self):
+        """Test InstructionParameter from_dict."""
+        with self.assertRaises(ModelValidationError) as context_manager:
+            _ = Dog.from_dict({'barking': {}})
+        self.assertIn('barking', str(context_manager.exception))
+
+        with self.assertRaises(ModelValidationError) as context_manager:
+            # Invalid types inside containers are also not allowed.
+            _ = Dog.from_dict({'barking': [{}]})
+        self.assertIn('barking', str(context_manager.exception))
+
+        with self.assertRaises(ModelValidationError) as context_manager:
+            # Non-basic types cannot be used in from_dict, even if they are
+            # accepted in the constructor. This is by design, as the serialized
+            # form should only contain Python/json-schema basic types.
+            _ = Dog.from_dict({'barking': sympy.Symbol('woof')})
+        self.assertIn('barking', str(context_manager.exception))