Skip to content

Commit 84a03be

Browse files
authored
Merge pull request #386 from marshmallow-code/383_fix_nested_many
Don't document many=True schemas as array
2 parents bf3faf4 + 753c923 commit 84a03be

File tree

4 files changed

+57
-25
lines changed

4 files changed

+57
-25
lines changed

CHANGELOG.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,12 @@ Features:
1515
from ``Regexp`` validators (:pr:`364`).
1616
Thanks :user:`DStape` for the PR.
1717

18+
Bug fixes:
19+
20+
- [apispec.ext.marshmallow]: Fix automatic documentation of schemas when
21+
using ``Nested(MySchema, many==True)`` (:issue:`383`). Thanks
22+
:user:`whoiswes` for reporting.
23+
1824
Other changes:
1925

2026
- *Backwards-incompatible*: Components properties are now passed as dictionaries rather than keyword arguments (:pr:`381`).

apispec/ext/marshmallow/openapi.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -450,7 +450,7 @@ def resolve_nested_schema(self, schema):
450450
name = self.schema_name_resolver(schema_cls)
451451
if not name:
452452
try:
453-
return self.schema2jsonschema(schema)
453+
json_schema = self.schema2jsonschema(schema)
454454
except RuntimeError:
455455
raise APISpecError(
456456
"Name resolver returned None for schema {schema} which is "
@@ -459,6 +459,9 @@ def resolve_nested_schema(self, schema):
459459
" MarshmallowPlugin returns a string for all circular"
460460
" referencing schemas.".format(schema=schema)
461461
)
462+
if getattr(schema, "many", False):
463+
return {"type": "array", "items": json_schema}
464+
return json_schema
462465
name = get_unique_schema_name(self.spec.components, name)
463466
self.spec.components.schema(name, schema=schema)
464467
return self.get_ref_dict(schema_instance)
@@ -649,9 +652,6 @@ class Meta:
649652
if hasattr(Meta, "description"):
650653
jsonschema["description"] = Meta.description
651654

652-
if getattr(schema, "many", False):
653-
jsonschema = {"type": "array", "items": jsonschema}
654-
655655
return jsonschema
656656

657657
def fields2jsonschema(self, fields, ordered=False, partial=None):

tests/test_ext_marshmallow.py

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,6 @@ def resolver(schema):
102102

103103
@pytest.mark.parametrize("schema", [AnalysisSchema, AnalysisSchema()])
104104
def test_resolve_schema_dict_auto_reference_return_none(self, schema):
105-
# this resolver return None
106105
def resolver(schema):
107106
return None
108107

@@ -162,6 +161,27 @@ class NameClashSchema(Schema):
162161
assert "Pet" in definitions
163162
assert "Pet1" in definitions
164163

164+
def test_resolve_nested_schema_many_true_resolver_return_none(self):
165+
def resolver(schema):
166+
return None
167+
168+
class PetFamilySchema(Schema):
169+
pets_1 = Nested(PetSchema, many=True)
170+
pets_2 = List(Nested(PetSchema))
171+
172+
spec = APISpec(
173+
title="Test auto-reference",
174+
version="0.1",
175+
openapi_version="2.0",
176+
plugins=(MarshmallowPlugin(schema_name_resolver=resolver),),
177+
)
178+
179+
spec.components.schema("PetFamily", schema=PetFamilySchema)
180+
props = get_schemas(spec)["PetFamily"]["properties"]
181+
pets_1 = props["pets_1"]
182+
pets_2 = props["pets_2"]
183+
assert pets_1["type"] == pets_2["type"] == "array"
184+
165185

166186
class TestComponentParameterHelper:
167187
@pytest.mark.parametrize("schema", [PetSchema, PetSchema()])
@@ -250,7 +270,8 @@ class CustomPetBSchema(PetSchema):
250270

251271
class TestOperationHelper:
252272
@pytest.mark.parametrize(
253-
"pet_schema", (PetSchema, PetSchema(), "tests.schemas.PetSchema")
273+
"pet_schema",
274+
(PetSchema, PetSchema(), PetSchema(many=True), "tests.schemas.PetSchema"),
254275
)
255276
@pytest.mark.parametrize("spec_fixture", ("2.0",), indirect=True)
256277
def test_schema_v2(self, spec_fixture, pet_schema):
@@ -268,15 +289,20 @@ def test_schema_v2(self, spec_fixture, pet_schema):
268289
},
269290
)
270291
get = get_paths(spec_fixture.spec)["/pet"]["get"]
271-
reference = get["responses"][200]["schema"]
292+
if isinstance(pet_schema, Schema) and pet_schema.many is True:
293+
assert get["responses"][200]["schema"]["type"] == "array"
294+
reference = get["responses"][200]["schema"]["items"]
295+
else:
296+
reference = get["responses"][200]["schema"]
272297
assert reference == {"$ref": ref_path(spec_fixture.spec) + "Pet"}
273298
assert len(spec_fixture.spec.components._schemas) == 1
274299
resolved_schema = spec_fixture.spec.components._schemas["Pet"]
275300
assert resolved_schema == spec_fixture.openapi.schema2jsonschema(PetSchema)
276301
assert get["responses"][200]["description"] == "successful operation"
277302

278303
@pytest.mark.parametrize(
279-
"pet_schema", (PetSchema, PetSchema(), "tests.schemas.PetSchema")
304+
"pet_schema",
305+
(PetSchema, PetSchema(), PetSchema(many=True), "tests.schemas.PetSchema"),
280306
)
281307
@pytest.mark.parametrize("spec_fixture", ("3.0.0",), indirect=True)
282308
def test_schema_v3(self, spec_fixture, pet_schema):
@@ -294,7 +320,16 @@ def test_schema_v3(self, spec_fixture, pet_schema):
294320
},
295321
)
296322
get = get_paths(spec_fixture.spec)["/pet"]["get"]
297-
reference = get["responses"][200]["content"]["application/json"]["schema"]
323+
if isinstance(pet_schema, Schema) and pet_schema.many is True:
324+
assert (
325+
get["responses"][200]["content"]["application/json"]["schema"]["type"]
326+
== "array"
327+
)
328+
reference = get["responses"][200]["content"]["application/json"]["schema"][
329+
"items"
330+
]
331+
else:
332+
reference = get["responses"][200]["content"]["application/json"]["schema"]
298333

299334
assert reference == {"$ref": ref_path(spec_fixture.spec) + "Pet"}
300335
assert len(spec_fixture.spec.components._schemas) == 1

tests/test_openapi.py

Lines changed: 7 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -413,25 +413,16 @@ def test_observed_field_name_for_required_field(self, openapi):
413413
res = openapi.fields2jsonschema(fields_dict)
414414
assert res["required"] == ["id"]
415415

416-
def test_schema_instance_inspection(self, openapi):
416+
@pytest.mark.parametrize("many", (True, False))
417+
def test_schema_instance_inspection(self, openapi, many):
417418
class UserSchema(Schema):
418419
_id = fields.Int()
419420

420-
res = openapi.schema2jsonschema(UserSchema())
421+
res = openapi.schema2jsonschema(UserSchema(many=many))
421422
assert res["type"] == "object"
422423
props = res["properties"]
423424
assert "_id" in props
424425

425-
def test_schema_instance_inspection_with_many(self, openapi):
426-
class UserSchema(Schema):
427-
_id = fields.Int()
428-
429-
res = openapi.schema2jsonschema(UserSchema(many=True))
430-
assert res["type"] == "array"
431-
assert "items" in res
432-
props = res["items"]["properties"]
433-
assert "_id" in props
434-
435426
def test_raises_error_if_no_declared_fields(self, openapi):
436427
class NotASchema(object):
437428
pass
@@ -654,7 +645,7 @@ class Parent(Schema):
654645
def test_schema2jsonschema_with_nested_fields_with_adhoc_changes(
655646
self, spec_fixture
656647
):
657-
category_schema = CategorySchema(many=True)
648+
category_schema = CategorySchema()
658649
category_schema.fields["id"].required = True
659650

660651
class PetSchema(Schema):
@@ -667,10 +658,10 @@ class PetSchema(Schema):
667658
assert props["Category"] == spec_fixture.openapi.schema2jsonschema(
668659
category_schema
669660
)
670-
assert set(props["Category"]["items"]["required"]) == {"id", "name"}
661+
assert set(props["Category"]["required"]) == {"id", "name"}
671662

672-
props["Category"]["items"]["required"] = ["name"]
673-
assert props["Category"]["items"] == spec_fixture.openapi.schema2jsonschema(
663+
props["Category"]["required"] = ["name"]
664+
assert props["Category"] == spec_fixture.openapi.schema2jsonschema(
674665
CategorySchema
675666
)
676667

0 commit comments

Comments
 (0)