Repository: marshmallow-code/apispec

Issue:
Should marshmallow helpers respect `only`, `exclude`?
When introspecting `Nested` fields, we don't check for the `only` or `exclude` attributes. For example:

``` python
class FooSchema(Schema):
    bar = fields.Str()
    baz = fields.Str()

class BobSchema(Schema):
    foo = fields.Nested(FooSchema, only=('bar', ))
```

If we call `schema2jsonschema(BobSchema)`, the nested `foo` will include `bar` and `baz` fields, even though `baz` will never be included in this case. Which isn't necessarily a problem, unless `baz` is required:

``` python
class FooSchema(Schema):
    bar = fields.Str()
    baz = fields.Str(required=True)
```

In this case, users of apispec will likely return JSON that wouldn't validate against the definition schema, since there's going to be a missing required field. I haven't actually encountered this situation, and I don't know how often it's going to come up--just wanted to raise for discussion.


Comments:

Patch:
diff --git a/apispec/ext/marshmallow/openapi.py b/apispec/ext/marshmallow/openapi.py
index b18a21a..a254079 100644
--- a/apispec/ext/marshmallow/openapi.py
+++ b/apispec/ext/marshmallow/openapi.py
@@ -447,25 +447,22 @@ class OpenAPIConverter(object):
 
         :param schema: schema to add to the spec
         """
-
-        schema_cls = self.resolve_schema_class(schema)
-        name = self.schema_name_resolver(schema_cls)
-
-        if not name:
-            try:
-                return self.schema2jsonschema(schema)
-            except RuntimeError:
-                raise APISpecError(
-                    'Name resolver returned None for schema {schema} which is '
-                    'part of a chain of circular referencing schemas. Please'
-                    ' ensure that the schema_name_resolver passed to'
-                    ' MarshmallowPlugin returns a string for all circular'
-                    ' referencing schemas.'.format(schema=schema),
-                )
-
         schema_instance = resolve_schema_instance(schema)
         schema_key = make_schema_key(schema_instance)
         if schema_key not in self.refs:
+            schema_cls = self.resolve_schema_class(schema)
+            name = self.schema_name_resolver(schema_cls)
+            if not name:
+                try:
+                    return self.schema2jsonschema(schema)
+                except RuntimeError:
+                    raise APISpecError(
+                        'Name resolver returned None for schema {schema} which is '
+                        'part of a chain of circular referencing schemas. Please'
+                        ' ensure that the schema_name_resolver passed to'
+                        ' MarshmallowPlugin returns a string for all circular'
+                        ' referencing schemas.'.format(schema=schema),
+                    )
             name = get_unique_schema_name(self.spec.components, name)
             self.spec.components.schema(
                 name,


Test patch:
diff --git a/tests/test_ext_marshmallow.py b/tests/test_ext_marshmallow.py
index 72271ad..4437a29 100644
--- a/tests/test_ext_marshmallow.py
+++ b/tests/test_ext_marshmallow.py
@@ -425,6 +425,66 @@ class TestOperationHelper:
             spec_fixture.spec,
         ) + 'Pet'
 
+    def test_schema_uses_ref_if_available_name_resolver_returns_none_v2(self):
+        def resolver(schema):
+            return None
+        spec = APISpec(
+            title='Test auto-reference',
+            version='0.1',
+            openapi_version='2.0',
+            plugins=(
+                MarshmallowPlugin(schema_name_resolver=resolver,),
+            ),
+        )
+        spec.components.schema('Pet', schema=PetSchema)
+        spec.path(
+            path='/pet',
+            operations={
+                'get': {
+                    'responses': {
+                        200: {
+                            'schema': PetSchema,
+                        },
+                    },
+                },
+            },
+        )
+        get = get_paths(spec)['/pet']['get']
+        assert get['responses'][200]['schema']['$ref'] == ref_path(spec) + 'Pet'
+
+    def test_schema_uses_ref_if_available_name_resolver_returns_none_v3(self):
+        def resolver(schema):
+            return None
+        spec = APISpec(
+            title='Test auto-reference',
+            version='0.1',
+            openapi_version='3.0.0',
+            plugins=(
+                MarshmallowPlugin(schema_name_resolver=resolver,),
+            ),
+        )
+        spec.components.schema('Pet', schema=PetSchema)
+        spec.path(
+            path='/pet',
+            operations={
+                'get': {
+                    'responses': {
+                        200: {
+                            'content': {
+                                'application/json': {
+                                    'schema': PetSchema,
+                                },
+                            },
+                        },
+                    },
+                },
+            },
+        )
+        get = get_paths(spec)['/pet']['get']
+        assert get['responses'][200]['content']['application/json']['schema']['$ref'] == ref_path(
+            spec,
+        ) + 'Pet'
+
     @pytest.mark.parametrize('spec_fixture', ('2.0', ), indirect=True)
     def test_schema_uses_ref_in_parameters_and_request_body_if_available_v2(self, spec_fixture):
         spec_fixture.spec.components.schema('Pet', schema=PetSchema)
diff --git a/tests/test_openapi.py b/tests/test_openapi.py
index df990b3..a8b83b5 100644
--- a/tests/test_openapi.py
+++ b/tests/test_openapi.py
@@ -648,6 +648,20 @@ class TestNesting:
 
         assert props['category']['items'] == {'$ref': ref_path(spec_fixture.spec) + 'Category'}
 
+    @pytest.mark.parametrize('modifier', ('only', 'exclude'))
+    def test_schema2jsonschema_with_nested_fields_only_exclude(self, spec_fixture, modifier):
+        class Child(Schema):
+            i = fields.Int()
+            j = fields.Int()
+
+        class Parent(Schema):
+            child = fields.Nested(Child, **{modifier: ('i', )})
+
+        spec_fixture.openapi.schema2jsonschema(Parent)
+        props = get_definitions(spec_fixture.spec)['Child']['properties']
+        assert ('i' in props) == (modifier == 'only')
+        assert ('j' not in props) == (modifier == 'only')
+
     def test_schema2jsonschema_with_nested_fields_with_adhoc_changes(self, spec_fixture):
         category_schema = CategorySchema(many=True)
         category_schema.fields['id'].required = True