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!



Discussion:
lafrech: BTW, I didn't understand this part of the code. It sounded related but I left it unchanged.

```py
            # marshmallow>=2.7.0 compat
            field.metadata.pop("many", None)
```

Any idea what this is about?
sloria: Thanks! I'm off to do errands but I'll hopefully have some time later today or tomorrow to review.

> I'm gonna get pawned by black.

I really hope https://github.com/pre-commit/pre-commit.github.io/issues/211 becomes a reality...
sloria: > Any idea what this is about?

https://github.com/marshmallow-code/apispec/pull/64 is the PR that added that. I don't recall the rationale, though.. =/
lafrech: I don't see anything related in the 2.7.0 entry of the changelog.
https://github.com/marshmallow-code/marshmallow/blob/dev/CHANGELOG.rst#270-2016-04-04

Removing the line doesn't break any test.
sloria: My guess is it's tied to this change: https://github.com/marshmallow-code/marshmallow/pull/413 , which may result in `many` being added to `field.metadata` (?).
lafrech: Yes, probably.

But I don't think it makes any difference as we only pull a limited list of items from metadata (`VALID_PROPERTIES` + `x-*`).

I'd be tempted to remove it...
sloria: Sure, it's your call. If there's no way for `many` to make it incorrectly into the OAS doc, then it can be removed.
lafrech: One one hand, I'm pretty confident. On the other hand, I don't see what changed since the change was accepted, so if there was a good reason by the time, it should still be valid. I'm still in favor of removing. Happy if @Bangertm has an opinion about this and this PR.
Bangertm: Looks good to me.  Thanks.

Patch:
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 36548f9..e32ad35 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,6 +1,6 @@
 repos:
 - repo: https://github.com/asottile/pyupgrade
-  rev: v1.11.1
+  rev: v1.11.0
   hooks:
   - id: pyupgrade
     exclude: docopt.py
@@ -10,7 +10,7 @@ repos:
   - id: black
     language_version: python3.6
 - repo: https://gitlab.com/pycqa/flake8
-  rev: 3.7.5
+  rev: 3.7.4
   hooks:
   - id: flake8
     additional_dependencies: ['flake8-bugbear==18.8.0; python_version >= "3.5"']
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):
diff --git a/setup.cfg b/setup.cfg
index 9502291..e3ad939 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -12,7 +12,3 @@ ignore = E203, E266, E501, W503
 max-line-length = 110
 max-complexity = 18
 select = B,C,E,F,W,T4,B9
-
-[tool:pytest]
-filterwarnings =
-  ignore::marshmallow.warnings.ChangedInMarshmallow3Warning


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
         )