Repository: marshmallow-code/apispec

Issue:
Docstring-only schemas incorrectly referenced as arrays?
Tested with Marshmallow 3.0.0rc3 and APISpec 1.0.0rc1.

Took a copy of the APISpec quickstart code and removed the defintions, so that schemas were only being referenced from the docstrings.  I'm seeing that the nested schema is being picked up as an array (incorrect) but then is also referenced as an array from the parent schema (correct).

Example (as short as I can make it, ignoring imports):

```py
spec = APISpec(
    title="Swagger Petstore",
    version="1.0.0",
    openapi_version="3.0.2",
    plugins=[FlaskPlugin(), MarshmallowPlugin()],
)

app = Flask(__name__)

class CategorySchema(Schema):
    id = fields.Int()
    name = fields.Str(required=True)

class PetSchema(Schema):
    category = fields.Nested(CategorySchema, many=True)
    name = fields.Str()

@app.route("/pet")
def random_pet():
    """
    ---
    get:
      responses:
        200:
          content:
            application/json:
              schema: PetSchema
    """
    return jsonify({'foo':'bar'})

@app.route("/category")
def random_category():
    """
    ---
    get:
      responses:
        200:
          content:
            application/json:
              schema: CategorySchema
    """
    return jsonify({'foo':'bar'})

with app.test_request_context():
    spec.path(view=random_pet)
    spec.path(view=random_category)
    print(spec.to_dict()
```
Here's what gets output (note that both the nested reference and the child are both array types):

```py
"schemas": {                                                  
  "Category": {                                               
    "type": "array",                                          
    "items": {                                                
      "type": "object",                                       
      "properties": {                                         
        "id": {"type": "integer", "format": "int32"}                                                     
      }                                                       
    }                                                         
  },                                                          
  "Pet": {                                                    
    "type": "object",                                         
    "properties": {                                           
      "category": {                                           
        "type": "array",                                      
        "items": {                                            
          "$ref": "#/components/schemas/Category"             
        }                                                     
      }                                                       
    }                                                         
  }                                                           
} 
```

I think the issue stems from fields2jsonschema method [here](https://github.com/marshmallow-code/apispec/blob/dev/apispec/ext/marshmallow/openapi.py#L647-L648) where the schema attributes are checked for many=True.  I would expect the schemas themselves would always be referenced as singletons, then references to those schemas would be checked for many.  I certainly might be missing something though, but at least in my lab just commenting out those 2 lines fixed everything.

Alternately, the schemas could be registered as they are in the example, but we try to do everything in docstrings if possible.  So if this is a non-issue feel free to close.

Many thanks for some awesome libraries!



Comments:

Patch:
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index c8e3f96..4a5fbde 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -15,6 +15,12 @@ Features:
   from ``Regexp`` validators (:pr:`364`).
   Thanks :user:`DStape` for the PR.
 
+Bug fixes:
+
+- [apispec.ext.marshmallow]: Fix automatic documentation of schemas when
+  using ``Nested(MySchema, many==True)`` (:issue:`383`). Thanks
+  :user:`whoiswes` for reporting.
+
 Other changes:
 
 - *Backwards-incompatible*: Components properties are now passed as dictionaries rather than keyword arguments (:pr:`381`).
diff --git a/apispec/ext/marshmallow/openapi.py b/apispec/ext/marshmallow/openapi.py
index 4bffa36..259d2dd 100644
--- a/apispec/ext/marshmallow/openapi.py
+++ b/apispec/ext/marshmallow/openapi.py
@@ -450,7 +450,7 @@ class OpenAPIConverter(object):
             name = self.schema_name_resolver(schema_cls)
             if not name:
                 try:
-                    return self.schema2jsonschema(schema)
+                    json_schema = self.schema2jsonschema(schema)
                 except RuntimeError:
                     raise APISpecError(
                         "Name resolver returned None for schema {schema} which is "
@@ -459,6 +459,9 @@ class OpenAPIConverter(object):
                         " MarshmallowPlugin returns a string for all circular"
                         " referencing schemas.".format(schema=schema)
                     )
+                if getattr(schema, "many", False):
+                    return {"type": "array", "items": json_schema}
+                return json_schema
             name = get_unique_schema_name(self.spec.components, name)
             self.spec.components.schema(name, schema=schema)
         return self.get_ref_dict(schema_instance)
@@ -649,9 +652,6 @@ class OpenAPIConverter(object):
         if hasattr(Meta, "description"):
             jsonschema["description"] = Meta.description
 
-        if getattr(schema, "many", False):
-            jsonschema = {"type": "array", "items": jsonschema}
-
         return jsonschema
 
     def fields2jsonschema(self, fields, ordered=False, partial=None):


Test patch:
diff --git a/tests/test_ext_marshmallow.py b/tests/test_ext_marshmallow.py
index f474b5f..ce1083d 100644
--- a/tests/test_ext_marshmallow.py
+++ b/tests/test_ext_marshmallow.py
@@ -102,7 +102,6 @@ class TestDefinitionHelper:
 
     @pytest.mark.parametrize("schema", [AnalysisSchema, AnalysisSchema()])
     def test_resolve_schema_dict_auto_reference_return_none(self, schema):
-        # this resolver return None
         def resolver(schema):
             return None
 
@@ -162,6 +161,27 @@ class TestDefinitionHelper:
         assert "Pet" in definitions
         assert "Pet1" in definitions
 
+    def test_resolve_nested_schema_many_true_resolver_return_none(self):
+        def resolver(schema):
+            return None
+
+        class PetFamilySchema(Schema):
+            pets_1 = Nested(PetSchema, many=True)
+            pets_2 = List(Nested(PetSchema))
+
+        spec = APISpec(
+            title="Test auto-reference",
+            version="0.1",
+            openapi_version="2.0",
+            plugins=(MarshmallowPlugin(schema_name_resolver=resolver),),
+        )
+
+        spec.components.schema("PetFamily", schema=PetFamilySchema)
+        props = get_schemas(spec)["PetFamily"]["properties"]
+        pets_1 = props["pets_1"]
+        pets_2 = props["pets_2"]
+        assert pets_1["type"] == pets_2["type"] == "array"
+
 
 class TestComponentParameterHelper:
     @pytest.mark.parametrize("schema", [PetSchema, PetSchema()])
@@ -250,7 +270,8 @@ class TestCustomField:
 
 class TestOperationHelper:
     @pytest.mark.parametrize(
-        "pet_schema", (PetSchema, PetSchema(), "tests.schemas.PetSchema")
+        "pet_schema",
+        (PetSchema, PetSchema(), PetSchema(many=True), "tests.schemas.PetSchema"),
     )
     @pytest.mark.parametrize("spec_fixture", ("2.0",), indirect=True)
     def test_schema_v2(self, spec_fixture, pet_schema):
@@ -268,7 +289,11 @@ class TestOperationHelper:
             },
         )
         get = get_paths(spec_fixture.spec)["/pet"]["get"]
-        reference = get["responses"][200]["schema"]
+        if isinstance(pet_schema, Schema) and pet_schema.many is True:
+            assert get["responses"][200]["schema"]["type"] == "array"
+            reference = get["responses"][200]["schema"]["items"]
+        else:
+            reference = get["responses"][200]["schema"]
         assert reference == {"$ref": ref_path(spec_fixture.spec) + "Pet"}
         assert len(spec_fixture.spec.components._schemas) == 1
         resolved_schema = spec_fixture.spec.components._schemas["Pet"]
@@ -276,7 +301,8 @@ class TestOperationHelper:
         assert get["responses"][200]["description"] == "successful operation"
 
     @pytest.mark.parametrize(
-        "pet_schema", (PetSchema, PetSchema(), "tests.schemas.PetSchema")
+        "pet_schema",
+        (PetSchema, PetSchema(), PetSchema(many=True), "tests.schemas.PetSchema"),
     )
     @pytest.mark.parametrize("spec_fixture", ("3.0.0",), indirect=True)
     def test_schema_v3(self, spec_fixture, pet_schema):
@@ -294,7 +320,16 @@ class TestOperationHelper:
             },
         )
         get = get_paths(spec_fixture.spec)["/pet"]["get"]
-        reference = get["responses"][200]["content"]["application/json"]["schema"]
+        if isinstance(pet_schema, Schema) and pet_schema.many is True:
+            assert (
+                get["responses"][200]["content"]["application/json"]["schema"]["type"]
+                == "array"
+            )
+            reference = get["responses"][200]["content"]["application/json"]["schema"][
+                "items"
+            ]
+        else:
+            reference = get["responses"][200]["content"]["application/json"]["schema"]
 
         assert reference == {"$ref": ref_path(spec_fixture.spec) + "Pet"}
         assert len(spec_fixture.spec.components._schemas) == 1
diff --git a/tests/test_openapi.py b/tests/test_openapi.py
index 41b2298..3a64ef3 100644
--- a/tests/test_openapi.py
+++ b/tests/test_openapi.py
@@ -413,25 +413,16 @@ class TestMarshmallowSchemaToModelDefinition:
         res = openapi.fields2jsonschema(fields_dict)
         assert res["required"] == ["id"]
 
-    def test_schema_instance_inspection(self, openapi):
+    @pytest.mark.parametrize("many", (True, False))
+    def test_schema_instance_inspection(self, openapi, many):
         class UserSchema(Schema):
             _id = fields.Int()
 
-        res = openapi.schema2jsonschema(UserSchema())
+        res = openapi.schema2jsonschema(UserSchema(many=many))
         assert res["type"] == "object"
         props = res["properties"]
         assert "_id" in props
 
-    def test_schema_instance_inspection_with_many(self, openapi):
-        class UserSchema(Schema):
-            _id = fields.Int()
-
-        res = openapi.schema2jsonschema(UserSchema(many=True))
-        assert res["type"] == "array"
-        assert "items" in res
-        props = res["items"]["properties"]
-        assert "_id" in props
-
     def test_raises_error_if_no_declared_fields(self, openapi):
         class NotASchema(object):
             pass
@@ -654,7 +645,7 @@ class TestNesting:
     def test_schema2jsonschema_with_nested_fields_with_adhoc_changes(
         self, spec_fixture
     ):
-        category_schema = CategorySchema(many=True)
+        category_schema = CategorySchema()
         category_schema.fields["id"].required = True
 
         class PetSchema(Schema):
@@ -667,10 +658,10 @@ class TestNesting:
         assert props["Category"] == spec_fixture.openapi.schema2jsonschema(
             category_schema
         )
-        assert set(props["Category"]["items"]["required"]) == {"id", "name"}
+        assert set(props["Category"]["required"]) == {"id", "name"}
 
-        props["Category"]["items"]["required"] = ["name"]
-        assert props["Category"]["items"] == spec_fixture.openapi.schema2jsonschema(
+        props["Category"]["required"] = ["name"]
+        assert props["Category"] == spec_fixture.openapi.schema2jsonschema(
             CategorySchema
         )