Repository: cwacek/python-jsonschema-objects

Issue:
serialize and reload (roundtrip) failed with oneOf keyword
the reification of an object previously serialized with the library failed.
the particularity of the schema: is using 'oneOf' to switch between 2 object types.
The following code and schema aim to reproduce the bug. please notice that if you serialize an object containing 'parametersA' the roundtrip will work.


**Example Schema and code**

```python 

schema_uri = "roundtrip.json"
builder = pjs.ObjectBuilder(schema_uri)
namespace = builder.build_classes()

data_config = """
{
  "something": "a name",
  "another": "a value"
}
"""
paramsTypeB = namespace.Parametersb().from_json(data_config)
somethingInstance = namespace.Something(param1="toto", param2= "tata")
somethingInstance.parameters = paramsTypeB


json_object = somethingInstance.serialize()
print(json_object)

aNewsomething = namespace.Something().from_json(json_object)

json_object2 = aNewsomething.serialize()

```

```json
{
  "$schema": "http://json-schema.org/draft-04/schema#",
  "$id": "roundtrip.json",
  "type": "object",
  "properties":
  {
    "container": {"$ref": "#/definitions/config"}
  },
  "definitions": {
    "config": {
      "properties":{
        "something": {
          "allOf": [
            {
              "$ref": "#/definitions/params"
            },
            {
              "properties":{
                "parameters": {
                  "oneOf": [
                    {
                      "$ref": "#/definitions/parametersA"
                    },
                    {
                      "$ref": "#/definitions/parametersB"
                    }
                  ]
                }
              },
              "required": ["parameters"]
            }
          ]
        }
      }
    },
    "params": {
      "type": "object",
      "properties": {
        "param1": {
          "type": "string"
        },
        "param2": {
          "type": "string"
        }
      },
      "required": [
        "param1",
        "param2"
      ]
    },
    "parametersA": {
      "properties": {
        "name": {
          "type": "string"
        },
        "value": {
          "type": "string"
        }
      },
      "required": [
        "name",
        "value"
      ]
    },
    "parametersB": {
      "properties": {
        "something": {
          "type": "string"
        },
        "another": {
          "type": "string"
        }
      },
      "required": [
        "something",
        "another"
      ]
    }
  }
}
```

**Expected behavior**
I am able to recreate the python object from the serialized JSON object.


Discussion:

Patch:
diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml
index 56255e6..ab0dd74 100644
--- a/.github/workflows/pythonpackage.yml
+++ b/.github/workflows/pythonpackage.yml
@@ -21,7 +21,7 @@ jobs:
     - name: Install dependencies
       run: |
         python -m pip install --upgrade pip 
-        pip install black
+        pip install black==20.8b1
     - name: Lint with black
       run: | 
         git ls-files | grep '\.py' | grep -v versioneer.py
@@ -33,7 +33,7 @@ jobs:
     runs-on: ubuntu-latest
     strategy:
       matrix:
-        python-version: [3.5, 3.6, 3.7, 3.8]
+        python-version: [3.6, 3.7, 3.8]
 
     steps:
     - uses: actions/checkout@v2
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 4d42e6d..baaee68 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,6 +1,6 @@
 repos:
   - repo: https://github.com/ambv/black
-    rev: stable
+    rev: 20.8b1
     hooks:
     - id: black
       language_version: python3.7
diff --git a/python_jsonschema_objects/classbuilder.py b/python_jsonschema_objects/classbuilder.py
index 7510fe1..ee3ff1f 100644
--- a/python_jsonschema_objects/classbuilder.py
+++ b/python_jsonschema_objects/classbuilder.py
@@ -24,7 +24,7 @@ if sys.version_info > (3,):
 
 
 class ProtocolBase(collections.abc.MutableMapping):
-    """ An instance of a class generated from the provided
+    """An instance of a class generated from the provided
     schema. All properties will be validated according to
     the definitions provided. However, whether or not all required
     properties have been provide will *not* be validated.
@@ -46,7 +46,7 @@ class ProtocolBase(collections.abc.MutableMapping):
     __object_attr_list__ = set(["_properties", "_extended_properties"])
 
     def as_dict(self):
-        """ Return a dictionary containing the current values
+        """Return a dictionary containing the current values
         of the object.
 
         Returns:
@@ -104,7 +104,7 @@ class ProtocolBase(collections.abc.MutableMapping):
 
     @classmethod
     def from_json(cls, jsonmsg):
-        """ Create an object directly from a JSON string.
+        """Create an object directly from a JSON string.
 
         Applies general validation after creating the
         object to check whether all required fields are
@@ -131,9 +131,9 @@ class ProtocolBase(collections.abc.MutableMapping):
         return self.__class__(**self.as_dict())
 
     def __new__(cls, **props):
-        """ Overridden to support oneOf, where we need to
+        """Overridden to support oneOf, where we need to
         instantiate a different class depending on what
-        value we've seen """
+        value we've seen"""
         if getattr(cls, "__validation__", None) is None:
             new = super(ProtocolBase, cls).__new__
             if new is object.__new__:
@@ -298,7 +298,7 @@ class ProtocolBase(collections.abc.MutableMapping):
         return enc.encode(self)
 
     def validate(self):
-        """ Applies all defined validation to the current
+        """Applies all defined validation to the current
         state of the object, and raises an error if
         they are not all met.
 
@@ -449,8 +449,8 @@ class ClassBuilder(object):
         self.under_construction = set()
 
     def expand_references(self, source_uri, iterable):
-        """ Give an iterable of jsonschema descriptors, expands any
-        of them that are $ref objects, and otherwise leaves them alone. 
+        """Give an iterable of jsonschema descriptors, expands any
+        of them that are $ref objects, and otherwise leaves them alone.
         """
         pp = []
         for elem in iterable:
@@ -505,7 +505,7 @@ class ClassBuilder(object):
             raise NotImplementedError("anyOf is not supported as bare property")
 
         elif "oneOf" in clsdata:
-            """ If this object itself has a 'oneOf' designation, 
+            """If this object itself has a 'oneOf' designation,
             then construct a TypeProxy.
             """
             klasses = self.construct_objects(clsdata["oneOf"], uri)
@@ -605,11 +605,11 @@ class ClassBuilder(object):
     def _build_literal(self, nm, clsdata):
         """@todo: Docstring for _build_literal
 
-      :nm: @todo
-      :clsdata: @todo
-      :returns: @todo
+        :nm: @todo
+        :clsdata: @todo
+        :returns: @todo
 
-      """
+        """
         cls = type(
             str(nm),
             tuple((LiteralValue,)),
diff --git a/python_jsonschema_objects/descriptors.py b/python_jsonschema_objects/descriptors.py
index fde0ceb..a80fcf2 100644
--- a/python_jsonschema_objects/descriptors.py
+++ b/python_jsonschema_objects/descriptors.py
@@ -50,32 +50,32 @@ class AttributeDescriptor(object):
                 elif hasattr(typ, "isLiteralClass"):
                     try:
                         validator = typ(val)
+                        validator.validate()
                     except Exception as e:
                         errors.append("Failed to coerce to '{0}': {1}".format(typ, e))
                         pass
                     else:
-                        validator.validate()
                         ok = True
                         break
                 elif util.safe_issubclass(typ, ProtocolBase):
                     # force conversion- thus the val rather than validator assignment
                     try:
                         val = typ(**util.coerce_for_expansion(val))
+                        val.validate()
                     except Exception as e:
                         errors.append("Failed to coerce to '{0}': {1}".format(typ, e))
                         pass
                     else:
-                        val.validate()
                         ok = True
                         break
                 elif util.safe_issubclass(typ, wrapper_types.ArrayWrapper):
                     try:
                         val = typ(val)
+                        val.validate()
                     except Exception as e:
                         errors.append("Failed to coerce to '{0}': {1}".format(typ, e))
                         pass
                     else:
-                        val.validate()
                         ok = True
                         break
 
diff --git a/python_jsonschema_objects/literals.py b/python_jsonschema_objects/literals.py
index d0240cb..0c05a6e 100644
--- a/python_jsonschema_objects/literals.py
+++ b/python_jsonschema_objects/literals.py
@@ -30,9 +30,9 @@ class LiteralValue(object):
     def __init__(self, value, typ=None):
         """@todo: to be defined
 
-      :value: @todo
+        :value: @todo
 
-      """
+        """
         if isinstance(value, LiteralValue):
             self._value = value._value
         else:
diff --git a/python_jsonschema_objects/wrapper_types.py b/python_jsonschema_objects/wrapper_types.py
index ac4f0ee..f64817b 100644
--- a/python_jsonschema_objects/wrapper_types.py
+++ b/python_jsonschema_objects/wrapper_types.py
@@ -11,7 +11,7 @@ logger = logging.getLogger(__name__)
 
 
 class ArrayWrapper(collections.abc.MutableSequence):
-    """ A wrapper for array-like structures.
+    """A wrapper for array-like structures.
 
     This implements all of the array like behavior that one would want,
     with a dirty-tracking mechanism to avoid constant validation costs.
@@ -52,7 +52,7 @@ class ArrayWrapper(collections.abc.MutableSequence):
             return self.for_json() == other
 
     def __init__(self, ary):
-        """ Initialize a wrapper for the array
+        """Initialize a wrapper for the array
 
         Args:
             ary: (list-like, or ArrayWrapper)
@@ -147,7 +147,7 @@ class ArrayWrapper(collections.abc.MutableSequence):
                 )
 
     def validate_items(self):
-        """ Validates the items in the backing array, including
+        """Validates the items in the backing array, including
         performing type validation.
 
         Sets the _typed property and clears the dirty flag as a side effect
@@ -230,7 +230,7 @@ class ArrayWrapper(collections.abc.MutableSequence):
 
     @staticmethod
     def create(name, item_constraint=None, **addl_constraints):
-        """ Create an array validator based on the passed in constraints.
+        """Create an array validator based on the passed in constraints.
 
         If item_constraint is a tuple, it is assumed that tuple validation
         is being performed. If it is a class or dictionary, list validation


Test patch:
diff --git a/test/test_pattern_properties.py b/test/test_pattern_properties.py
index d4e3aae..60bddde 100644
--- a/test/test_pattern_properties.py
+++ b/test/test_pattern_properties.py
@@ -18,7 +18,7 @@ def base_schema():
 
 
 def test_standard_properties_take_precedence(base_schema):
-    """ foobar is a boolean, and it's a standard property,
+    """foobar is a boolean, and it's a standard property,
     so we expect it will validate properly as a boolean,
     not using the patternProperty that matches it.
     """
diff --git a/test/test_pytest.py b/test/test_pytest.py
index f993d79..810d6cf 100644
--- a/test/test_pytest.py
+++ b/test/test_pytest.py
@@ -35,8 +35,7 @@ def test_warnings_on_schema_version(version, warn):
 
 
 def test_schema_validation():
-    """ Test that the ObjectBuilder validates the schema itself.
-    """
+    """Test that the ObjectBuilder validates the schema itself."""
     schema = {
         "$schema": "http://json-schema.org/schema#",
         "$id": "test",
diff --git a/test/test_regression_208.py b/test/test_regression_208.py
new file mode 100644
index 0000000..6344c84
--- /dev/null
+++ b/test/test_regression_208.py
@@ -0,0 +1,114 @@
+import json
+import pytest
+import python_jsonschema_objects as pjo
+
+schema = """
+{
+  "$schema": "http://json-schema.org/draft-04/schema#",
+  "$id": "roundtrip.json",
+  "type": "object",
+  "properties":
+  {
+    "container": {"$ref": "#/definitions/config"}
+  },
+  "definitions": {
+    "config": {
+      "properties":{
+        "something": {
+          "allOf": [
+            {
+              "$ref": "#/definitions/params"
+            },
+            {
+              "properties":{
+                "parameters": {
+                  "oneOf": [
+                    {
+                      "$ref": "#/definitions/parametersA"
+                    },
+                    {
+                      "$ref": "#/definitions/parametersB"
+                    }
+                  ]
+                }
+              },
+              "required": ["parameters"]
+            }
+          ]
+        }
+      }
+    },
+    "params": {
+      "type": "object",
+      "properties": {
+        "param1": {
+          "type": "string"
+        },
+        "param2": {
+          "type": "string"
+        }
+      },
+      "required": [
+        "param1",
+        "param2"
+      ]
+    },
+    "parametersA": {
+      "properties": {
+        "name": {
+          "type": "string"
+        },
+        "value": {
+          "type": "string"
+        }
+      },
+      "required": [
+        "name",
+        "value"
+      ]
+    },
+    "parametersB": {
+      "properties": {
+        "something": {
+          "type": "string"
+        },
+        "another": {
+          "type": "string"
+        }
+      },
+      "required": [
+        "something",
+        "another"
+      ]
+    }
+  }
+}
+"""
+
+
+@pytest.fixture
+def schema_json():
+    return json.loads(schema)
+
+
+def test_unpack_arraywrapper_on_init(schema_json):
+    builder = pjo.ObjectBuilder(schema_json)
+    namespace = builder.build_classes()
+
+    data_config = """
+    {
+    "something": "a name",
+    "another": "a value"
+    }
+    """
+    paramsTypeB = namespace.Parametersb().from_json(data_config)
+    somethingInstance = namespace.Something(param1="toto", param2="tata")
+    somethingInstance.parameters = paramsTypeB
+
+    json_object = somethingInstance.serialize()
+    print(json_object)
+
+    aNewsomething = namespace.Something.from_json(json_object)
+
+    json_object2 = aNewsomething.serialize()
+    assert json_object == json_object2