Repository: cwacek/python-jsonschema-objects

Issue:
oneOf array of simple types bug - propose fix
```
basicSchemaDefn = {
  '$schema': 'http://json-schema.org/draft-04/schema#',
  'title': 'Test',
  'properties': {
    'SimpleArrayOfNumberOrString': {
      '$ref': '#/definitions/simparray'
    }
  },
  'required': [
    'SimpleArrayOfNumberOrString'
  ],
  'type': 'object',
  'definitions': {
    'simparray': {
      'oneOf': [{
          'type': 'array',
          'items': {
            'type': 'number'
          }
        },
        {
          'type': 'array',
          'items': {
            'type': 'string'
          }
        }
      ]
    }
  }
}

builder = pjs.ObjectBuilder(basicSchemaDefn)

ns = builder.build_classes()
ns.Test().from_json('{"SimpleArrayOfNumberOrString" : [0, 1]}')
```



Discussion:

Patch:
diff --git a/python_jsonschema_objects/classbuilder.py b/python_jsonschema_objects/classbuilder.py
index 8178397..809095e 100644
--- a/python_jsonschema_objects/classbuilder.py
+++ b/python_jsonschema_objects/classbuilder.py
@@ -399,9 +399,20 @@ class TypeRef(object):
 
 
 class TypeProxy(object):
-    def __init__(self, types):
+    slots = ("__title__", "_types")
+
+    def __init__(self, types, title=None):
+        self.__title__ = title
         self._types = types
 
+    def from_json(self, jsonmsg):
+        import json
+
+        msg = json.loads(jsonmsg)
+        obj = self(**msg)
+        obj.validate()
+        return obj
+
     def __call__(self, *a, **kw):
         validation_errors = []
         valid_types = self._types
@@ -434,25 +445,44 @@ class ClassBuilder(object):
         self.resolved = {}
         self.under_construction = set()
 
-    def resolve_classes(self, iterable):
+    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. 
+        """
         pp = []
         for elem in iterable:
             if "$ref" in elem:
-                ref = elem["$ref"]
-                uri = util.resolve_ref_uri(self.resolver.resolution_scope, ref)
-                if uri in self.resolved:
-                    pp.append(self.resolved[uri])
-                else:
-                    with self.resolver.resolving(ref) as resolved:
-                        self.resolved[uri] = self.construct(
-                            uri, resolved, (ProtocolBase,)
-                        )
-                        pp.append(self.resolved[uri])
+                pp.append(self.resolve_type(elem["$ref"], source_uri))
             else:
                 pp.append(elem)
 
         return pp
 
+    def resolve_type(self, ref, source):
+        """ Return a resolved type for a URI, potentially constructing one if necessary"""
+        uri = util.resolve_ref_uri(self.resolver.resolution_scope, ref)
+        if uri in self.resolved:
+            return self.resolved[uri]
+
+        elif uri in self.under_construction:
+            logger.debug(
+                util.lazy_format(
+                    "Using a TypeRef to avoid a cyclic reference for {0} -> {1} ",
+                    uri,
+                    source,
+                )
+            )
+            return TypeRef(uri, self.resolved)
+        else:
+            logger.debug(
+                util.lazy_format(
+                    "Resolving direct reference object {0} -> {1}", source, uri
+                )
+            )
+            with self.resolver.resolving(ref) as resolved:
+                self.resolved[uri] = self.construct(uri, resolved, (ProtocolBase,))
+                return self.resolved[uri]
+
     def construct(self, uri, *args, **kw):
         """ Wrapper to debug things """
         logger.debug(util.lazy_format("Constructing {0}", uri))
@@ -472,11 +502,19 @@ class ClassBuilder(object):
             raise NotImplementedError("anyOf is not supported as bare property")
 
         elif "oneOf" in clsdata:
-            self.resolved[uri] = self._build_object(uri, clsdata, parent, **kw)
+            """ If this object itself has a 'oneOf' designation, 
+            then construct a TypeProxy.
+            """
+            klasses = self.construct_objects(clsdata["oneOf"], uri)
+
+            logger.debug(
+                util.lazy_format("Designating {0} as TypeProxy for {1}", uri, klasses)
+            )
+            self.resolved[uri] = TypeProxy(klasses, title=clsdata.get("title"))
             return self.resolved[uri]
 
         elif "allOf" in clsdata:
-            potential_parents = self.resolve_classes(clsdata["allOf"])
+            potential_parents = self.expand_references(uri, clsdata["allOf"])
             parents = []
             for p in potential_parents:
                 if isinstance(p, dict):
@@ -509,25 +547,8 @@ class ClassBuilder(object):
                 )
             else:
                 ref = clsdata["$ref"]
-                refuri = util.resolve_ref_uri(self.resolver.resolution_scope, ref)
-                if refuri in self.under_construction:
-                    logger.debug(
-                        util.lazy_format(
-                            "Resolving cyclic reference from {0} to {1}.", uri, refuri
-                        )
-                    )
-                    return TypeRef(refuri, self.resolved)
-                else:
-                    logger.debug(
-                        util.lazy_format(
-                            "Resolving direct reference object for {0}: {1}",
-                            uri,
-                            refuri,
-                        )
-                    )
-
-                    with self.resolver.resolving(refuri) as resolved:
-                        self.resolved[uri] = self.construct(refuri, resolved, parent)
+                typ = self.resolve_type(ref, uri)
+                self.resolved[uri] = typ
 
             return self.resolved[uri]
 
@@ -639,23 +660,14 @@ class ClassBuilder(object):
 
             elif "type" not in detail and "$ref" in detail:
                 ref = detail["$ref"]
-                uri = util.resolve_ref_uri(self.resolver.resolution_scope, ref)
-                logger.debug(
-                    util.lazy_format(
-                        "Resolving reference {0} for {1}.{2}", ref, nm, prop
-                    )
-                )
-                if uri in self.resolved:
-                    typ = self.resolved[uri]
-                else:
-                    typ = self.construct(uri, detail, (ProtocolBase,))
+                typ = self.resolve_type(ref, ".".join([nm, prop]))
 
                 props[prop] = make_property(prop, {"type": typ}, typ.__doc__)
-                properties[prop]["$ref"] = uri
+                properties[prop]["$ref"] = ref
                 properties[prop]["type"] = typ
 
             elif "oneOf" in detail:
-                potential = self.resolve_classes(detail["oneOf"])
+                potential = self.expand_references(nm, detail["oneOf"])
                 logger.debug(
                     util.lazy_format("Designating {0} as oneOf {1}", prop, potential)
                 )
@@ -665,16 +677,13 @@ class ClassBuilder(object):
             elif "type" in detail and detail["type"] == "array":
                 if "items" in detail and isinstance(detail["items"], dict):
                     if "$ref" in detail["items"]:
-                        uri = util.resolve_ref_uri(
-                            self.resolver.resolution_scope, detail["items"]["$ref"]
-                        )
-                        typ = self.construct(uri, detail["items"])
+                        typ = self.resolve_type(detail["items"]["$ref"], nm)
                         constraints = copy.copy(detail)
                         constraints["strict"] = kw.get("strict")
                         propdata = {
                             "type": "array",
                             "validator": python_jsonschema_objects.wrapper_types.ArrayWrapper.create(
-                                uri, item_constraint=typ, **constraints
+                                nm, item_constraint=typ, **constraints
                             ),
                         }
 
@@ -683,20 +692,9 @@ class ClassBuilder(object):
                         try:
                             if "oneOf" in detail["items"]:
                                 typ = TypeProxy(
-                                    [
-                                        self.construct(uri + "_%s" % i, item_detail)
-                                        if "$ref" not in item_detail
-                                        else self.construct(
-                                            util.resolve_ref_uri(
-                                                self.resolver.resolution_scope,
-                                                item_detail["$ref"],
-                                            ),
-                                            item_detail,
-                                        )
-                                        for i, item_detail in enumerate(
-                                            detail["items"]["oneOf"]
-                                        )
-                                    ]
+                                    self.construct_objects(
+                                        detail["items"]["oneOf"], uri
+                                    )
                                 )
                             else:
                                 typ = self.construct(uri, detail["items"])
@@ -738,14 +736,6 @@ class ClassBuilder(object):
 
                 props[prop] = make_property(prop, {"type": typ}, desc)
 
-        """ If this object itself has a 'oneOf' designation, then
-        make the validation 'type' the list of potential objects.
-        """
-        if "oneOf" in clsdata:
-            klasses = self.resolve_classes(clsdata["oneOf"])
-            # Need a validation to check that it meets one of them
-            props["__validation__"] = {"type": klasses}
-
         props["__extensible__"] = pattern_properties.ExtensibleValidator(
             nm, clsdata, self
         )
@@ -777,6 +767,14 @@ class ClassBuilder(object):
 
         return cls
 
+    def construct_objects(self, oneOfList, uri):
+        return [
+            self.construct(uri + "_%s" % i, item_detail)
+            if "$ref" not in item_detail
+            else self.resolve_type(item_detail["$ref"], uri + "_%s" % i)
+            for i, item_detail in enumerate(oneOfList)
+        ]
+
 
 def make_property(prop, info, desc=""):
     from . import descriptors
diff --git a/python_jsonschema_objects/pattern_properties.py b/python_jsonschema_objects/pattern_properties.py
index 2ebdf1b..c4a07a6 100644
--- a/python_jsonschema_objects/pattern_properties.py
+++ b/python_jsonschema_objects/pattern_properties.py
@@ -30,7 +30,7 @@ class ExtensibleValidator(object):
             self._additional_type = True
         else:
             if "$ref" in addlProp:
-                refs = builder.resolve_classes([addlProp])
+                typ = builder.resolve_type(addlProp["$ref"], name)
             else:
                 uri = "{0}/{1}_{2}".format(
                     name, "<additionalProperties>", "<anonymous>"
@@ -38,23 +38,23 @@ class ExtensibleValidator(object):
                 builder.resolved[uri] = builder.construct(
                     uri, addlProp, (cb.ProtocolBase,)
                 )
-                refs = [builder.resolved[uri]]
+                typ = builder.resolved[uri]
 
-            self._additional_type = refs[0]
+            self._additional_type = typ
 
         for pattern, typedef in six.iteritems(schemadef.get("patternProperties", {})):
             if "$ref" in typedef:
-                refs = builder.resolve_classes([typedef])
+                typ = builder.resolve_type(typedef["$ref"], name)
             else:
                 uri = "{0}/{1}_{2}".format(name, "<patternProperties>", pattern)
 
                 builder.resolved[uri] = builder.construct(
                     uri, typedef, (cb.ProtocolBase,)
                 )
-                refs = [builder.resolved[uri]]
+                typ = builder.resolved[uri]
 
             self._pattern_types.append(
-                PatternDef(pattern=re.compile(pattern), schema_type=refs[0])
+                PatternDef(pattern=re.compile(pattern), schema_type=typ)
             )
 
     def _make_type(self, typ, val):
diff --git a/python_jsonschema_objects/wrapper_types.py b/python_jsonschema_objects/wrapper_types.py
index c2ce58f..4d7ea91 100644
--- a/python_jsonschema_objects/wrapper_types.py
+++ b/python_jsonschema_objects/wrapper_types.py
@@ -251,7 +251,9 @@ class ArrayWrapper(collections.MutableSequence):
         )
         from python_jsonschema_objects import classbuilder
 
-        klassbuilder = addl_constraints.pop("classbuilder", None)
+        klassbuilder = addl_constraints.pop(
+            "classbuilder", None
+        )  # type: python_jsonschema_objects.classbuilder.ClassBuilder
         props = {}
 
         if item_constraint is not None:
@@ -290,23 +292,9 @@ class ArrayWrapper(collections.MutableSequence):
                             )
                         )
 
-                    uri = item_constraint["$ref"]
-                    if uri in klassbuilder.resolved:
-                        logger.debug(
-                            util.lazy_format(
-                                "Using previously resolved object for {0}", uri
-                            )
-                        )
-                    else:
-                        logger.debug(util.lazy_format("Resolving object for {0}", uri))
-
-                        with klassbuilder.resolver.resolving(uri) as resolved:
-                            # Set incase there is a circular reference in schema definition
-                            klassbuilder.resolved[uri] = klassbuilder.construct(
-                                uri, resolved, (classbuilder.ProtocolBase,)
-                            )
-
-                    item_constraint = klassbuilder.resolved[uri]
+                    item_constraint = klassbuilder.resolve_type(
+                        item_constraint["$ref"], name
+                    )
 
                 elif isdict and item_constraint.get("type") == "array":
                     # We need to create a sub-array validator.
@@ -318,22 +306,9 @@ class ArrayWrapper(collections.MutableSequence):
                 elif isdict and "oneOf" in item_constraint:
                     # We need to create a TypeProxy validator
                     uri = "{0}_{1}".format(name, "<anonymous_list_type>")
-                    type_array = []
-                    for i, item_detail in enumerate(item_constraint["oneOf"]):
-                        if "$ref" in item_detail:
-                            subtype = klassbuilder.construct(
-                                util.resolve_ref_uri(
-                                    klassbuilder.resolver.resolution_scope,
-                                    item_detail["$ref"],
-                                ),
-                                item_detail,
-                            )
-                        else:
-                            subtype = klassbuilder.construct(
-                                uri + "_%s" % i, item_detail
-                            )
-
-                        type_array.append(subtype)
+                    type_array = klassbuilder.construct_objects(
+                        item_constraint["oneOf"], uri
+                    )
 
                     item_constraint = classbuilder.TypeProxy(type_array)
 


Test patch:
diff --git a/test/test_feature_151.py b/test/test_feature_151.py
new file mode 100644
index 0000000..3798287
--- /dev/null
+++ b/test/test_feature_151.py
@@ -0,0 +1,31 @@
+import pytest
+import python_jsonschema_objects as pjo
+
+
+def test_simple_array_oneOf():
+    basicSchemaDefn = {
+        "$schema": "http://json-schema.org/draft-04/schema#",
+        "title": "Test",
+        "properties": {
+            "SimpleArrayOfNumberOrString": {"$ref": "#/definitions/simparray"}
+        },
+        "required": ["SimpleArrayOfNumberOrString"],
+        "type": "object",
+        "definitions": {
+            "simparray": {
+                "oneOf": [
+                    {"type": "array", "items": {"type": "number"}},
+                    {"type": "array", "items": {"type": "string"}},
+                ]
+            }
+        },
+    }
+
+    builder = pjo.ObjectBuilder(basicSchemaDefn)
+
+    ns = builder.build_classes()
+    ns.Test().from_json('{"SimpleArrayOfNumberOrString" : [0, 1]}')
+    ns.Test().from_json('{"SimpleArrayOfNumberOrString" : ["Hi", "There"]}')
+
+    with pytest.raises(pjo.ValidationError):
+        ns.Test().from_json('{"SimpleArrayOfNumberOrString" : ["Hi", 0]}')
diff --git a/test/test_pytest.py b/test/test_pytest.py
index 2ae8a5e..b1106e1 100644
--- a/test/test_pytest.py
+++ b/test/test_pytest.py
@@ -529,3 +529,12 @@ def test_default_values(default):
 
     x = ns.Test()
     assert x.sample == default["default"]
+
+
+def test_justareference_example(markdown_examples):
+
+    builder = pjs.ObjectBuilder(
+        markdown_examples["Just a Reference"], resolved=markdown_examples
+    )
+    ns = builder.build_classes()
+    ns.JustAReference("Hello")
diff --git a/test/test_regression_156.py b/test/test_regression_156.py
index 138bbe0..48837bc 100644
--- a/test/test_regression_156.py
+++ b/test/test_regression_156.py
@@ -18,3 +18,11 @@ def test_regression_156(markdown_examples):
     # round-trip serialize-deserialize into class defined with `oneOf`
     classes.Multipleobjects.from_json(er.serialize())
     classes.Multipleobjects.from_json(vgr.serialize())
+
+
+def test_toplevel_oneof_gets_a_name(markdown_examples):
+    builder = pjo.ObjectBuilder(
+        markdown_examples["MultipleObjects"], resolved=markdown_examples
+    )
+    classes = builder.build_classes(named_only=True)
+    assert classes.Multipleobjects.__title__ is not None