Repository: marshmallow-code/apispec
Pull request #333 (2018-10-31 18:09:53)

Nested schema auto resolution as default and respect modifiers in references
Relates to #307 

This is a proof of concept on combining the logic in inspect_schema_for_auto_referencing with the nesting logic in field2property.  

1. Default schema_name_resolver now passed to MarshmallowPlugin
2. Logic for adding nested schmeas moved from
3. MarshmallowPlugin.inspect_schema_for_auto_referencing to
4. OpenAPIConverter.field2property
5. Removed using LazyDict in favor of regular Dict

All the tests are passing, but I presume that there are some minor differences in the specs that are not captured by the test.  Of potential concern would be cases like [this](https://github.com/marshmallow-code/apispec/blob/b0e1b214a0c295bcf2205cc0995c45a400f4f7e7/tests/test_ext_marshmallow.py#L576).  The second two definition adds would be unnecessary and if the schema name resolved to something other than what the user is putting in as the name there will be extra components in the model.  Not sure that is too big of a deal or not.

At this point the cyclomatic complexity of field2property has increased, but there should be some simplifying that can be done.  For example I'm not sure that the unbound self referencing cases are valid and the concept of embedding the reference string in field metadata feels like a relic of not having a ref dictionary.  


lafrech: I think your recent commits make my comment above irrelevant (unless it was irrelevant from the beginning), as the resolving happens in a single place (https://github.com/marshmallow-code/apispec/pull/333/files#diff-daf6a711ac369a8d051b9606e0a9eca6R367) and there is no need to add specific cases for each container field. This is great.
lafrech: Current implementation of `spec.components.schemas` does not detect if a name is used twice. The second schema overrides the first. We should probably raise an Exception when trying to register a second schema with an already registered name. While this is not critical when registration is manual, it can be useful when registration is automatic as the resolving can lead to tricky cases.

I think we both have the same nominal use case in mind. All schemas are called `EntitySchema` and are registered as `Entity` in the spec. But there can be collisions if user doesn't follow this naming scheme. Like `PetSchema` and `Pet`. And letting the user specify another resolver opens the door to all sorts of tricky issues.

I don't think this should be addressed here, but it pushes for a simple check-raise in spec.components.schema.

I opened #340 about this.
lafrech: On second thought, the auto-ref feature could be a reason not to raise an Exception, because it might auto-register a Schema that was already registered manually...

We could add a test to check if the already registered schema with the same name is the same schema class, but then it becomes a bit smelly.
lafrech: Do we want to force the feature or just make it default?

In the current state of the PR, the only way to cancel the feature is to pass a no-op resolver to override the default. This is due to my change request of not passing the resolver default in the signature. I realized that afterwards. We could revert that change. It is not a friendly API.

We can go the other way and decide the feature is mandatory. This does not seem unreasonable. After all, when viewing the spec with ReDoc (and perhaps swagger-ui, I don't remember), it is done in the viewer frontend.

If we do that, we should provide a default name for when the schema name can't be resolved (when the resolver returns None). It can be `'Schema_{}'.format(some_incremented_counter)`.

Then we may write:

```py
        schema_cls = resolve_schema_cls(schema)
        if schema_cls not in self.refs:
            self.spec.components.schema(
                self.thin_layer_above_schema_name_resolver_providing_default_name(schema_cls),
                schema=schema,
                dump=dump,
                load=load,
            )
```

Or we don't try to think too much and we leave it as it is.

I think this thought was triggered by the self-nesting or circular nesting case. I still don't totally grasp the change you made w.r.t. the delayed resolution (`LazyDict`). It seems too nice to be true. I was wondering if it would work if the resolver returned `None`. Wouldn't we fall back on the old issue (before #44)? Making sure all schemas are registered, even with a dummy name, sounds like a simple way around this. Sorry if I'm still missing something.

Don't miss the "good" in "too good to be true". I like the code simplification brought by this PR.
Bangertm: I'm still trying to wrap my head around the implications of making this feature the default.  There are just so many assumptions that have been made along the way.  

Regarding collisions, my biggest concern is that a nested schema comes in once with `dump=False` and then gets referenced someplace where `dump=True`. That schema second reference will be missing fields.  

Part of the rational for excluding fields when `dump` or `load` is `False` is that the schema's parameters are listed in line, but if we have the general plan of using the `schema_name_resolver` to add the schema to the doc and insert a reference it might negate the need to have `dump` and `load` functionality at all.  

For example we could have a check early in `resolve_schema_dict` to use add schemas not currently in the spec using the resolved name and insert a ref instead of inlining the parameters.  

>  I was wondering if it would work if the resolver returned None. Wouldn't we fall back on the old issue (before #44)?

With the current implementation there is a check on whether `schema_name_resolver` returned a value and if not then the schema is not added to the spec.  For circular references, this will revert the the problem before #44.  But we could also remove the check.  But that would also remove a convenient way for users to opt out of this functionality.  
lafrech: > Regarding collisions, my biggest concern is that a nested schema comes in once with dump=False and then gets referenced someplace where dump=True. That schema second reference will be missing fields.

Exactly.

> Part of the rational for excluding fields when dump or load is False is that the schema's parameters are listed in line, but if we have the general plan of using the schema_name_resolver to add the schema to the doc and insert a reference it might negate the need to have dump and load functionality at all.

Yeah, what I thought, too. We should double-check that. I wouldn't mind dropping the dump/load logic since schema used as reference should have the dumpOnly/loadOnly attributes correctly set.

> For circular references, this will revert the the problem before #44.

OK, I thought so. We can't afford to break the circular reference use case so it looks like we're in the middle of the bridge: either we keep things as they are, either we make the auto-ref mandatory.

Keeping things as they are means back to https://github.com/marshmallow-code/apispec/issues/307. Either keep, remove or deprecate current implementation (which can be easily introduced by subclassing MarshmallowPlugin).

If we choose to make the feature mandatory, we need to sort out a few things:
- ensure the name defaults to something unique
- think of a nice way to manage name collisions (is an exception acceptable?)
- remove dump/load logic if it has become useless

I wrote:

> On second thought, the auto-ref feature could be a reason not to raise an Exception, because it might auto-register a Schema that was already registered manually...

This shouldn't be an issue since we check if the schema is already registered. The issue is when registering manually a schema that has been auto-registered before. We could skip that second registration, but that means the auto-name will be used instead of the user-specified name. Doing this silently sounds wrong, so an exception might be acceptable. If the user wants to specify the name manually, he should register the schema before using it.

This logic could be a blocker for users wanting to specify names manually in case of circular references. 

And if not a blocker, it could be an annoyance for users wanting to register manual names as they'd need to ensure they register the schemas in the right order. In practice, I'm not sure it is such an issue. I think I already do that in my code.

We may postpone this to 2.0 if we can't sort it out easily. We'd still need to decide what to do in 1.0.
Bangertm: After thinking about this for a few days I think this is in a pretty good state.

Out of the box: 
- Whenever a nested schema is encountered it is added to the spec and referenced
- Whenever a schema is found in request body or in a response body it is added to the spec and referenced (this eliminates the need for `dump` and `load`
- Circular referencing schemas are automatically resolved - users who are upgrading will get a warning that they are entering a schema twice (once automatically and then again manually) and will likely want to make some changes to their code to remove the manual entry of the schema and to change the schema's name if they don't like the auto one

With a custom `schema_name_resolver` function:
- Users can provide whatever names they want - easiest would be with a dictionary mapping schema class to a name string
- If users really want to have nested schema properties listed inline instead of referenced they can have their resolver return `None` for a given schema
- If the custom name resolver returns `None` for a schema in a circular referencing chain they get a helpful exception telling them not to do that

I think this seems like a decent mix of desired out of the box behavior with enough customization for users who want that.  

The nice thing about this from my perspective is that out of the box every schema in the spec will have a name that matches what I call the schema in my code.  I don't have to make sure to add nested schemas to the spec before adding the schemas that nest them and I don't have to provide a name resolver.  It also makes the resolving schemas from a doc string much more convenient because those schemas get automatically named as well.  I can probably eliminate adding schemas manually altogether can just add the paths.  
lafrech: Great!

I still need to review the code changes more thoroughly but I agree on the principle.

> Users can provide whatever names they want - easiest would be with a dictionary mapping schema class to a name string

Yeah, or an access to a class attribute.

```py
class MySchema(Schema):
    schema_name = 'MySuperSchema'
    ...

def resolver(schema):
    return schema.schema_name
```

lafrech: I left a few comments.

Icing on the cake, it I may suggest: cleaning history (squashing a few commits together to only have a few meaningful commits) would help reviewing and digging history in the future.
Bangertm: Thanks for the feedback.  All should be addressed.  I didn't know you could match exception and warning messages so easily in pytest - super convenient.  

Everything is squashed down to one commit.  I was trying for two - one for moving the schema name resolution functionality and one for using references for schemas found in request and response bodies but I couldn't resolve the conflicts.  
Bangertm: One more thing... Noticed that `use_instances` isn't used anymore so I removed it from the function signature. That diff should probably be squashed too, but wanted to give you a chance to review.
lafrech: `use_instances` was added in https://github.com/marshmallow-code/apispec/pull/144 to support partial schemas. This PR breaks the partial schema use case.

There is a wider discussion about this in https://github.com/marshmallow-code/apispec/issues/169. All fields modifiers are concerned (`partial`, `exclude`,...). Basically, resolving on classes ignores modifiers, resolving on instances would make user code unrealistically cumbersome. A third way could be to resolve on instances and also compare modifiers, if that is feasible. Until this is done, user should refrain form using modifiers.

I don't really mind the regression on `partial` if it is documented. It would be nice to have in mind a long-term solution to this. Like modifying `resolve_nested_schema` to resolve not only on the class but also on modifiers. I don't see any blocker right now.

Also, some `partial` logic was added by @sloria in https://github.com/marshmallow-code/apispec/commit/09e3febc9997d27d90a892f94b06d29cc98de995 before #144. I guess it should be removed, shouldn't it?
Bangertm: The way things are implemented now has the effect that is somewhat similar to `use_instances` being hard coded to `True`.  If `resolve_schema_dict` is called with a schema instance it is schema instance not the schema class that will be added to the spec.  The `partial` modifier will be applied to that that schema.  

The difference is that the previous implementation would always return an inline definition and never a reference when `use_instances` is `Tue` because the schema instance would never been in the `ref` dictionary.  Now most likely `resolve_schema_dict` will return a reference.  There wouldn't be a problem if only one version of the schema is in the spec, but there will be collisions if the schema is being used with different modifiers.  

I tend to think that using the schema class plus the schema's modifiers as the key in `ref` might be the way to go.
lafrech: > I tend to think that using the schema class plus the schema's modifiers as the key in ref might be the way to go.

@Bangertm, here's a draft of Schema comparison feature: https://github.com/marshmallow-code/marshmallow/pull/1062.
lafrech: Thinking of it, the `__eq__` function is not so helpful. Besides, it would only work for marshmallow 3. And we only want to compare the modifiers that affect the documentation. We're better off keeping the logic here.

We may ignore `'context'` and `'unknown'`.

```
MODIFIERS = {'only', 'exclude', 'many', 'load_only', 'dump_only', 'partial'}
```

And as you suggested, use class + modifiers as key.

And we need to add some more auto-naming logic to manage the case when multiple instances of the same class are provided. It doesn't have to be clever. People wanting nice names may pass them manually. All we need is to make sure each schema has a different name. So it can be an incremented counter. AFAIU, we already need this anyway for the pathological case of the resolver generating the same name for two different schema classes.
Bangertm: > And we need to add some more auto-naming logic to manage the case when multiple instances of the same class are provided. It doesn't have to be clever. People wanting nice names may pass them manually. All we need is to make sure each schema has a different name. So it can be an incremented counter. AFAIU, we already need this anyway for the pathological case of the resolver generating the same name for two different schema classes.

We might want to just warn the user of the name collision instead.  I can't image that people would be happy with Pet, Pet1, Pet2, etc in their spec.  If they have to take some action in this case anyway we might just want to warn them and tell them to either add the schema with a unique name or provide a custom schema name resolver.  
lafrech: Alright. Should we raise an exception then? If we don't offer a viable fallback, a warning might not be enough.
Bangertm: Yes I guess raising an exception is best.  I was thinking to use a warning so that they could complete the operation, inspect the spec, and figure out what to do.  But in this case, a name clash is overwriting an existing schema in the spec so we should stop them where that is happening because in all likelihood that is not what they want to happen.  

Where should we raise from?  Very specifically in `OpenAPIConverter.resolve_nested_schema` where the problem might happen via automatic name resolution or more generically in `MarshmallowPlugin.schema_helper` or even `Components.schema`?
Bangertm: Have now added some commits to include the instance modifiers in the key.  I'm pretty sure that we have to make this change if we go forward with the auto name resolver functionality.  Previously a schema like:

```python
class MultiModifierSchema(Schema):
     pet_unmodified = Nested(PetSchema)
     pet_exclude = Nested(PetSchema, exclude=('name',))
```
would have resolved properly with appropriate inline definitions for each field.  Without taking modifiers into account both fields will reference the last schema added even if they are both in the spec.

Also based on this case I added an ability to generate a unique name.  It would have been really annoying to get an exception when trying to add this schema to the spec and just warning the user would have yielded an incorrect spec.  So now the spec will be correct and the user will get an indication of how to get better names.  

Any thoughts on a better algorithm for generating the unique name?  I'm not super happy with it but can't think of something better right now.
lafrech: I think this is the way to go.

Do you think you could rebase and update docs?
Bangertm: Got the branch up to date and added some information in the docs on working with nested fields and modifiers.
Bangertm: @sloria changelog is updated.  Can you take a look to make sure it's clear.
lafrech: @Bangertm, sorry for the delay about this. I just reviewed it again and I think t is good to go once @sloria's comments are addressed.

When this is merged, we can release a RC version (maybe with other fixes from https://github.com/marshmallow-code/apispec/milestone/2 it you have something ready) and we're close to a final v1!

Patch:
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 60a79f7..76fc2ba 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -4,7 +4,7 @@ repos:
   hooks:
   - id: add-trailing-comma
 - repo: https://github.com/pre-commit/pre-commit-hooks
-  rev: v2.1.0
+  rev: v2.0.0
   hooks:
   - id: trailing-whitespace
   - id: double-quote-string-fixer
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 414af92..d2555c7 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -1,6 +1,18 @@
 Changelog
 ---------
 
+1.0.0 (unreleased)
+++++++++++++++++++
+
+Features:
+
+- Automatically generate references to nested schemas with a computed name, e.g.
+  ``fields.Nested(PetSchema())`` -> ``#components/schemas/Pet``.
+- Automatically generate references for ``requestBody`` using the above mechanism.
+- Ability to opt out of the above behavior by passing a ``schema_name_resolver``
+  function that returns ``None`` to ``api.ext.MarshmallowPlugin`` on initialization.
+- References now respect Schema initialization modifiers such as exclude.
+
 1.0.0b6 (2018-12-16)
 ++++++++++++++++++++
 
diff --git a/apispec/ext/marshmallow/__init__.py b/apispec/ext/marshmallow/__init__.py
index 70a0d9a..9c27f22 100644
--- a/apispec/ext/marshmallow/__init__.py
+++ b/apispec/ext/marshmallow/__init__.py
@@ -30,20 +30,31 @@ Requires marshmallow>=2.15.2.
 
 """
 from __future__ import absolute_import
+import warnings
 
 import marshmallow
 
 from apispec import BasePlugin
-from .common import resolve_schema_cls, resolve_schema_instance
+from .common import resolve_schema_cls, resolve_schema_instance, make_schema_key
 from .openapi import OpenAPIConverter
 
 
+def resolver(schema):
+    """Default implementation of a schema name resolver function
+    """
+    name = schema.__name__
+    if name.endswith('Schema'):
+        return name[:-6] or name
+    return name
+
+
 class MarshmallowPlugin(BasePlugin):
     """APISpec plugin handling marshmallow schemas
 
     :param callable schema_name_resolver: Callable to generate the schema definition name.
         Receives the `Schema` class and returns the name to be used in refs within
-        the generated spec.
+        the generated spec. When working with circular referencing this function
+        must must not return `None` for schemas in a circular reference chain.
 
         Example: ::
 
@@ -52,7 +63,7 @@ class MarshmallowPlugin(BasePlugin):
     """
     def __init__(self, schema_name_resolver=None):
         super(MarshmallowPlugin, self).__init__()
-        self.schema_name_resolver = schema_name_resolver
+        self.schema_name_resolver = schema_name_resolver or resolver
         self.spec = None
         self.openapi_version = None
         self.openapi = None
@@ -61,34 +72,11 @@ class MarshmallowPlugin(BasePlugin):
         super(MarshmallowPlugin, self).init_spec(spec)
         self.spec = spec
         self.openapi_version = spec.openapi_version
-        self.openapi = OpenAPIConverter(openapi_version=spec.openapi_version)
-
-    def inspect_schema_for_auto_referencing(self, original_schema_instance):
-        """Parse given schema instance and reference eventual nested schemas
-        :param original_schema_instance: schema to parse
-        """
-        # schema_name_resolver must be provided to use this function
-        assert self.schema_name_resolver
-
-        for field in original_schema_instance.fields.values():
-            nested_schema_class = None
-
-            if isinstance(field, marshmallow.fields.Nested):
-                nested_schema_class = self.openapi.resolve_schema_class(field.schema)
-
-            elif isinstance(field, marshmallow.fields.List) \
-                    and isinstance(field.container, marshmallow.fields.Nested):
-                nested_schema_class = self.openapi.resolve_schema_class(field.container.schema)
-
-            if nested_schema_class and nested_schema_class not in self.openapi.refs:
-                definition_name = self.schema_name_resolver(
-                    nested_schema_class,
-                )
-                if definition_name:
-                    self.spec.components.schema(
-                        definition_name,
-                        schema=nested_schema_class,
-                    )
+        self.openapi = OpenAPIConverter(
+            openapi_version=spec.openapi_version,
+            schema_name_resolver=self.schema_name_resolver,
+            spec=spec,
+        )
 
     def resolve_parameters(self, parameters):
         resolved = []
@@ -102,7 +90,7 @@ class MarshmallowPlugin(BasePlugin):
                         default_in=parameter.pop('in'), **parameter
                     )
                     continue
-            self.resolve_schema(parameter, dump=False, load=True)
+            self.resolve_schema(parameter)
             resolved.append(parameter)
         return resolved
 
@@ -113,26 +101,24 @@ class MarshmallowPlugin(BasePlugin):
         content = request_body['content']
         for content_type in content:
             schema = content[content_type]['schema']
-            content[content_type]['schema'] = self.openapi.resolve_schema_dict(schema, dump=False, load=True)
+            content[content_type]['schema'] = self.openapi.resolve_schema_dict(schema)
 
-    def resolve_schema(self, data, dump=True, load=True):
+    def resolve_schema(self, data):
         """Function to resolve a schema in a parameter or response - modifies the
         corresponding dict to convert Marshmallow Schema object or class into dict
 
         :param APISpec spec: `APISpec` containing refs.
         :param dict data: the parameter or response dictionary that may contain a schema
-        :param bool dump: Introspect dump logic.
-        :param bool load: Introspect load logic.
         """
         if self.openapi_version.major < 3:
             if 'schema' in data:
-                data['schema'] = self.openapi.resolve_schema_dict(data['schema'], dump=dump, load=load)
+                data['schema'] = self.openapi.resolve_schema_dict(data['schema'])
         else:
             if 'content' in data:
                 for content_type in data['content']:
                     schema = data['content'][content_type]['schema']
                     data['content'][content_type]['schema'] = self.openapi.resolve_schema_dict(
-                        schema, dump=dump, load=load,
+                        schema,
                     )
 
     def map_to_openapi_type(self, *args):
@@ -165,18 +151,14 @@ class MarshmallowPlugin(BasePlugin):
         if schema is None:
             return None
 
-        schema_cls = resolve_schema_cls(schema)
         schema_instance = resolve_schema_instance(schema)
 
-        # Store registered refs, keyed by Schema class
-        self.openapi.refs[schema_cls] = name
+        schema_key = make_schema_key(schema_instance)
+        self.warn_if_schema_already_in_spec(schema_key)
+        self.openapi.refs[schema_key] = name
 
         json_schema = self.openapi.schema2jsonschema(schema_instance, name=name)
 
-        # Auto reference schema if schema_name_resolver
-        if self.schema_name_resolver:
-            self.inspect_schema_for_auto_referencing(schema_instance)
-
         return json_schema
 
     def parameter_helper(self, **kwargs):
@@ -186,7 +168,7 @@ class MarshmallowPlugin(BasePlugin):
         :param type|Schema schema: A marshmallow Schema class or instance.
         """
         # In OpenAPIv3, this only works when using the complex form using "content"
-        self.resolve_schema(kwargs, dump=False, load=True)
+        self.resolve_schema(kwargs)
         return kwargs
 
     def response_helper(self, **kwargs):
@@ -195,7 +177,7 @@ class MarshmallowPlugin(BasePlugin):
 
         :param type|Schema schema: A marshmallow Schema class or instance.
         """
-        self.resolve_schema(kwargs, dump=True, load=False)
+        self.resolve_schema(kwargs)
         return kwargs
 
     def operation_helper(self, operations, **kwargs):
@@ -208,4 +190,15 @@ class MarshmallowPlugin(BasePlugin):
                 if 'requestBody' in operation:
                     self.resolve_schema_in_request_body(operation['requestBody'])
             for response in operation.get('responses', {}).values():
-                self.resolve_schema(response, dump=True, load=False)
+                self.resolve_schema(response)
+
+    def warn_if_schema_already_in_spec(self, schema_key):
+        """Method to warn the user if the schema has already been added to the
+        spec.
+        """
+        if schema_key in self.openapi.refs:
+            warnings.warn(
+                '{} has already been added to the spec. Adding it twice may '
+                'cause references to not resove properly'.format(schema_key[0]),
+                UserWarning,
+            )
diff --git a/apispec/ext/marshmallow/common.py b/apispec/ext/marshmallow/common.py
index e777bb8..2cbbf01 100644
--- a/apispec/ext/marshmallow/common.py
+++ b/apispec/ext/marshmallow/common.py
@@ -2,10 +2,15 @@
 """Utilities to get schema instances/classes"""
 
 import copy
+import warnings
+from collections import namedtuple
 
 import marshmallow
 
 
+MODIFIERS = ['only', 'exclude', 'load_only', 'dump_only', 'partial']
+
+
 def resolve_schema_instance(schema):
     """Return schema instance for given schema (instance or class)
 
@@ -39,3 +44,45 @@ def get_fields(schema):
     elif hasattr(schema, '_declared_fields'):
         return copy.deepcopy(schema._declared_fields)
     raise ValueError("{0!r} doesn't have either `fields` or `_declared_fields`".format(schema))
+
+
+def make_schema_key(schema):
+    if not isinstance(schema, marshmallow.Schema):
+        raise TypeError('can only make a schema key based on a Schema instance')
+    modifiers = []
+    for modifier in MODIFIERS:
+        attribute = getattr(schema, modifier)
+        try:
+            hash(attribute)
+        except TypeError:
+            attribute = tuple(attribute)
+        modifiers.append(attribute)
+    return SchemaKey(schema.__class__, *modifiers)
+
+
+SchemaKey = namedtuple('SchemaKey', ['SchemaClass'] + MODIFIERS)
+
+
+def get_unique_schema_name(components, name, counter=0):
+    """Function to generate a unique name based on the provided name and names
+    already in the spec.  Will append a number to the name to make it unique if
+    the name is already in the spec.
+
+    :param Components components: instance of the components of the spec
+    :param string name: the name to use as a basis for the unique name
+    :param int counter: the counter of the number of recursions
+    :return: the unique name
+    """
+    if name not in components._schemas:
+        return name
+    if not counter:  # first time time through recursion
+        warnings.warn(
+            'Multiple schemas resolved to the name {} - name has been modified '
+            'either manually add each of the schemas with a different name or '
+            'provide custom schema_name_resolver'.format(name),
+            UserWarning,
+        )
+    else:  # subsequent recursions
+        name = name[:-len(str(counter))]
+    counter += 1
+    return get_unique_schema_name(components, name + str(counter), counter)
diff --git a/apispec/ext/marshmallow/openapi.py b/apispec/ext/marshmallow/openapi.py
index 789d55e..c61310a 100644
--- a/apispec/ext/marshmallow/openapi.py
+++ b/apispec/ext/marshmallow/openapi.py
@@ -15,9 +15,12 @@ from marshmallow.utils import is_collection
 from marshmallow.compat import iteritems
 from marshmallow.orderedset import OrderedSet
 
-from apispec.lazy_dict import LazyDict
 from apispec.utils import OpenAPIVersion
-from .common import resolve_schema_cls, get_fields
+from .common import (
+    resolve_schema_cls, get_fields, make_schema_key, resolve_schema_instance,
+    get_unique_schema_name,
+)
+from apispec.exceptions import APISpecError
 
 
 MARSHMALLOW_VERSION_INFO = tuple(
@@ -94,18 +97,16 @@ _VALID_PROPERTIES = {
 _VALID_PREFIX = 'x-'
 
 
-class OrderedLazyDict(LazyDict, OrderedDict):
-    pass
-
-
 class OpenAPIConverter(object):
     """Converter generating OpenAPI specification from Marshmallow schemas and fields
 
     :param str|OpenAPIVersion openapi_version: The OpenAPI version to use.
         Should be in the form '2.x' or '3.x.x' to comply with the OpenAPI standard.
     """
-    def __init__(self, openapi_version):
+    def __init__(self, openapi_version, schema_name_resolver, spec):
         self.openapi_version = OpenAPIVersion(openapi_version)
+        self.schema_name_resolver = schema_name_resolver
+        self.spec = spec
         # Schema references
         self.refs = {}
         # Field mappings
@@ -291,7 +292,7 @@ class OpenAPIConverter(object):
                 attributes[max_attr] = validator.equal
         return attributes
 
-    def field2property(self, field, use_refs=True, dump=True, load=True, name=None):
+    def field2property(self, field, use_refs=True, name=None):
         """Return the JSON Schema property definition given a marshmallow
         :class:`Field <marshmallow.fields.Field>`.
 
@@ -302,8 +303,6 @@ class OpenAPIConverter(object):
 
         :param Field field: A marshmallow field.
         :param bool use_refs: Use JSONSchema ``refs``.
-        :param bool dump: Introspect dump logic.
-        :param bool load: Introspect load logic.
         :param str name: The definition name, if applicable, used to construct the $ref value.
         :rtype: dict, a Property Object
         """
@@ -362,20 +361,20 @@ class OpenAPIConverter(object):
                     else:
                         ret.update(ref_schema)
             else:
-                schema_dict = self.resolve_schema_dict(field.schema, dump=dump, load=load)
+                schema_dict = self.resolve_nested_schema(field.schema)
                 if ret and '$ref' in schema_dict:
                     ret.update({'allOf': [schema_dict]})
                 else:
                     ret.update(schema_dict)
         elif isinstance(field, marshmallow.fields.List):
             ret['items'] = self.field2property(
-                field.container, use_refs=use_refs, dump=dump, load=load,
+                field.container, use_refs=use_refs,
             )
         elif isinstance(field, marshmallow.fields.Dict):
             if MARSHMALLOW_VERSION_INFO[0] >= 3:
                 if field.value_container:
                     ret['additionalProperties'] = self.field2property(
-                        field.value_container, use_refs=use_refs, dump=dump, load=load,
+                        field.value_container, use_refs=use_refs,
                     )
 
         # Dasherize metadata that starts with x_
@@ -392,6 +391,44 @@ class OpenAPIConverter(object):
 
         return ret
 
+    def resolve_nested_schema(self, schema):
+        """Return the Open API representation of a marshmallow Schema.
+
+        Adds the schema to the spec if it isn't already present.
+
+        Typically will return a dictionary with the reference to the schema's
+        path in the spec unless the `schema_name_resolver` returns `None`, in
+        which case the returned dictoinary will contain a JSON Schema Object
+        representation of the schema.
+
+        :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:
+            name = get_unique_schema_name(self.spec.components, name)
+            self.spec.components.schema(
+                name,
+                schema=schema,
+            )
+        return self.get_ref_dict(schema_instance)
+
     def schema2parameters(self, schema, **kwargs):
         """Return an array of OpenAPI parameters given a given marshmallow
         :class:`Schema <marshmallow.Schema>`. If `default_in` is "body", then return an array
@@ -405,7 +442,7 @@ class OpenAPIConverter(object):
     def fields2parameters(
             self, fields, schema=None, use_refs=True,
             default_in='body', name='body', required=False,
-            use_instances=False, description=None, **kwargs
+            description=None, **kwargs
     ):
         """Return an array of OpenAPI parameters given a mapping between field names and
         :class:`Field <marshmallow.Field>` objects. If `default_in` is "body", then return an array
@@ -425,9 +462,9 @@ class OpenAPIConverter(object):
         openapi_default_in = __location_map__.get(default_in, default_in)
         if self.openapi_version.major < 3 and openapi_default_in == 'body':
             if schema is not None:
-                prop = self.resolve_schema_dict(schema, dump=False, load=True, use_instances=use_instances)
+                prop = self.resolve_schema_dict(schema)
             else:
-                prop = self.fields2jsonschema(fields, use_refs=use_refs, dump=False, load=True)
+                prop = self.fields2jsonschema(fields, use_refs=use_refs)
 
             param = {
                 'in': openapi_default_in,
@@ -476,7 +513,7 @@ class OpenAPIConverter(object):
         https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#parameterObject
         """
         location = field.metadata.get('location', None)
-        prop = self.field2property(field, use_refs=use_refs, dump=False, load=True)
+        prop = self.field2property(field, use_refs=use_refs)
         return self.property2parameter(
             prop,
             name=name,
@@ -537,7 +574,7 @@ class OpenAPIConverter(object):
     def schema2jsonschema(self, schema, **kwargs):
         return self.fields2jsonschema(get_fields(schema), schema, **kwargs)
 
-    def fields2jsonschema(self, fields, schema=None, use_refs=True, dump=True, load=True, name=None):
+    def fields2jsonschema(self, fields, schema=None, use_refs=True, name=None):
         """Return the JSON Schema Object for a given marshmallow
         :class:`Schema <marshmallow.Schema>`. Schema may optionally provide the ``title`` and
         ``description`` class Meta options.
@@ -590,26 +627,20 @@ class OpenAPIConverter(object):
 
         jsonschema = {
             'type': 'object',
-            'properties': OrderedLazyDict() if getattr(Meta, 'ordered', None) else LazyDict(),
+            'properties': OrderedDict() if getattr(Meta, 'ordered', None) else {},
         }
 
         exclude = set(getattr(Meta, 'exclude', []))
 
         for field_name, field_obj in iteritems(fields):
-            if (
-                    field_name in exclude or
-                    (field_obj.dump_only and not dump) or
-                    (field_obj.load_only and not load)
-            ):
+            if (field_name in exclude):
                 continue
 
-            def prop_func(field_obj=field_obj):
-                return self.field2property(
-                    field_obj, use_refs=use_refs, dump=dump, load=load, name=name,
-                )
-
             observed_field_name = self._observed_name(field_obj, field_name)
-            jsonschema['properties'][observed_field_name] = prop_func
+            property = self.field2property(
+                field_obj, use_refs=use_refs, name=name,
+            )
+            jsonschema['properties'][observed_field_name] = property
 
             partial = getattr(schema, 'partial', None)
             if field_obj.required:
@@ -633,6 +664,20 @@ class OpenAPIConverter(object):
 
         return jsonschema
 
+    def get_ref_dict(self, schema):
+        """Method to create a dictionary containing a JSON reference to the
+        schema in the spec
+        """
+        ref_path = self.get_ref_path()
+        schema_key = make_schema_key(schema)
+        ref_schema = {'$ref': '#/{0}/{1}'.format(ref_path, self.refs[schema_key])}
+        if getattr(schema, 'many', False):
+            return {
+                'type': 'array',
+                'items': ref_schema,
+            }
+        return ref_schema
+
     def get_ref_path(self):
         """Return the path for references based on the openapi version
 
@@ -645,35 +690,18 @@ class OpenAPIConverter(object):
         }
         return ref_paths[self.openapi_version.major]
 
-    def resolve_schema_dict(self, schema, dump=True, load=True, use_instances=False):
+    def resolve_schema_dict(self, schema):
         if isinstance(schema, dict):
             if schema.get('type') == 'array' and 'items' in schema:
-                schema['items'] = self.resolve_schema_dict(
-                    schema['items'], use_instances=use_instances,
-                )
+                schema['items'] = self.resolve_schema_dict(schema['items'])
             if schema.get('type') == 'object' and 'properties' in schema:
                 schema['properties'] = {
-                    k: self.resolve_schema_dict(v, dump=dump, load=load, use_instances=use_instances)
+                    k: self.resolve_schema_dict(v)
                     for k, v in schema['properties'].items()
                 }
             return schema
-        if isinstance(schema, marshmallow.Schema) and use_instances:
-            schema_cls = schema
-        else:
-            schema_cls = self.resolve_schema_class(schema)
-
-        if schema_cls in self.refs:
-            ref_path = self.get_ref_path()
-            ref_schema = {'$ref': '#/{0}/{1}'.format(ref_path, self.refs[schema_cls])}
-            if getattr(schema, 'many', False):
-                return {
-                    'type': 'array',
-                    'items': ref_schema,
-                }
-            return ref_schema
-        if not isinstance(schema, marshmallow.Schema):
-            schema = schema_cls
-        return self.schema2jsonschema(schema, dump=dump, load=load)
+
+        return self.resolve_nested_schema(schema)
 
     def resolve_schema_class(self, schema):
         """Return schema class for given schema (instance or class)
diff --git a/apispec/lazy_dict.py b/apispec/lazy_dict.py
deleted file mode 100644
index f9cddf9..0000000
--- a/apispec/lazy_dict.py
+++ /dev/null
@@ -1,50 +0,0 @@
-class LazyDict(dict):
-    """Simple implementation of a lazy dictionary. If a value set is callable
-    without parameters, the value will be called the first time is requested
-    and stored for future requests.
-    E.g:
-        dict = LazyDict()
-        dict['foo'] = lambda: return 123
-        dict['bar'] = dict
-        dict['chu'] = dict() # you can also pass non-callable values
-        assert dict['foo'] == 123
-        assert dict['bar'] == {}
-        assert dict['chu'] == {}
-
-    For Internal use only."""
-
-    def values(self):
-        return [self.__getitem__(key) for key in self.keys()]
-
-    def items(self):
-        return [(key, self.__getitem__(key)) for key in self.keys()]
-
-    def iteritems(self):
-        return [(key, self.__getitem__(key)) for key in self.keys()]
-
-    def itervalues(self):
-        return [self.__getitem__(key) for key in self.keys()]
-
-    def get(self, k, d=None):
-        return self.__getitem__(k) if k in self else d
-
-    def __getitem__(self, key):
-        item = super(LazyDict, self).__getitem__(key)
-        try:
-            self[key] = item = item()
-        except TypeError:
-            pass
-        return item
-
-    def __repr__(self):
-        # make dictionaries evaluate
-        self.values()
-        return super(LazyDict, self).__repr__()
-
-    def __eq__(self, other):
-        # make dictionaries evaluate
-        self.values()
-        if isinstance(other, LazyDict):
-            other.values()
-
-        return super(LazyDict, self).__eq__(other)
diff --git a/apispec/yaml_utils.py b/apispec/yaml_utils.py
index 6ceb527..53cbb30 100644
--- a/apispec/yaml_utils.py
+++ b/apispec/yaml_utils.py
@@ -4,7 +4,6 @@
 from collections import OrderedDict
 import yaml
 
-from apispec.lazy_dict import LazyDict
 from apispec.compat import PY2, unicode, iteritems
 from apispec.utils import trim_docstring, dedent
 
@@ -24,7 +23,6 @@ class YAMLDumper(yaml.Dumper):
 if PY2:
     yaml.add_representer(unicode, YAMLDumper._represent_unicode, Dumper=YAMLDumper)
 yaml.add_representer(OrderedDict, YAMLDumper._represent_dict, Dumper=YAMLDumper)
-yaml.add_representer(LazyDict, YAMLDumper._represent_dict, Dumper=YAMLDumper)
 
 
 def dict_to_yaml(dic):
diff --git a/docs/using_plugins.rst b/docs/using_plugins.rst
index c86d887..db8fcad 100644
--- a/docs/using_plugins.rst
+++ b/docs/using_plugins.rst
@@ -197,6 +197,45 @@ If your API uses `method-based dispatching <http://flask.pocoo.org/docs/0.12/vie
     #            'post': {}}}
     #
 
+
+Marshmallow Plugin
+------------------
+
+Nesting Schemas
+***************
+
+By default, Marshmallow `Nested` fields are represented by a `JSON Reference object
+<https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#referenceObject>`_.
+If the schema has been added to the spec via `spec.components.schema <apispec.core.Components.schema>`,
+the user-supplied name will be used in the reference. Otherwise apispec will
+add the nested schema to the spec using an automatically resolved name for the
+nested schema. The default `schema_name_resolver <apispec.ext.marshmallow.resolver>`
+function will resolve a name based on the schema's class `__name__`, dropping a
+trailing "Schema" so that `class PetSchema(Schema)` resolves to "Pet".
+
+To change the behavior of the name resolution simply pass an alternative
+function accepting a `Schema` class and returning a string to the plugin's
+constructor. If the `schema_name_resolver` function returns a value that
+evaluates to `False` in a boolean context the nested schema will not be added to
+the spec and instead defined in-line.
+
+Note: Circular-referencing schemas cannot be defined in-line due to infinite
+recursion so a `schema_name_resolver` function must return a string name when
+working with circular-referencing schemas.
+
+Schema Modifiers
+****************
+
+`Schema` instances can be initialized with modifier parameters to exclude
+fields or ignore the absence of required fields. apispec will respect
+schema modifiers in the generated schema definition. If a particular schema is
+initialized in an application with modifiers, it may be added to the spec with
+each set of modifiers and apispec will treat each unique set of modifiers --
+including no modifiers - as a unique schema definition.
+
+Custom Fields
+***************
+
 By default, apispec only knows how to set the type of
 built-in marshmallow fields. If you want to generate definitions for
 schemas with custom fields, use the


Test patch:
diff --git a/tests/conftest.py b/tests/conftest.py
index b6d254a..2a23e0b 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -30,3 +30,9 @@ def spec_fixture(request):
 @pytest.fixture(params=('2.0', '3.0.0'))
 def spec(request):
     return make_spec(request.param).spec
+
+
+@pytest.fixture(params=('2.0', '3.0.0'))
+def openapi(request):
+    spec = make_spec(request.param)
+    return spec.openapi
diff --git a/tests/schemas.py b/tests/schemas.py
index 0b9f253..105daed 100644
--- a/tests/schemas.py
+++ b/tests/schemas.py
@@ -9,13 +9,13 @@ class PetSchema(Schema):
 
 
 class SampleSchema(Schema):
-    runs = fields.Nested('RunSchema', many=True, exclude=('sample',))
+    runs = fields.Nested('RunSchema', many=True)
 
     count = fields.Int()
 
 
 class RunSchema(Schema):
-    sample = fields.Nested(SampleSchema, exclude=('runs',))
+    sample = fields.Nested(SampleSchema)
 
 
 class AnalysisSchema(Schema):
diff --git a/tests/test_ext_marshmallow.py b/tests/test_ext_marshmallow.py
index 4cdb845..d8421f8 100644
--- a/tests/test_ext_marshmallow.py
+++ b/tests/test_ext_marshmallow.py
@@ -4,19 +4,20 @@ import json
 
 import pytest
 
-from marshmallow.fields import Field, DateTime, Dict, String
+from marshmallow.fields import Field, DateTime, Dict, String, Nested, List
 from marshmallow import Schema
 
 from apispec import APISpec
 from apispec.ext.marshmallow import MarshmallowPlugin
 from apispec.ext.marshmallow.openapi import MARSHMALLOW_VERSION_INFO
 from .schemas import (
-    PetSchema, AnalysisSchema, SampleSchema, RunSchema,
+    PetSchema, AnalysisSchema, RunSchema,
     SelfReferencingSchema, OrderedSchema, PatternedObjectSchema,
     DefaultValuesSchema, AnalysisWithListSchema,
 )
 
 from .utils import get_definitions, get_parameters, get_responses, get_paths
+from apispec.exceptions import APISpecError
 
 
 def ref_path(spec):
@@ -126,31 +127,47 @@ class TestDefinitionHelper:
         )
         assert {} == get_definitions(spec)
 
-        spec.components.schema('analysis', schema=schema)
-        spec.path(
-            '/test', operations={
-                'get': {
-                    'responses': {
-                        '200': {
-                            'schema': {
-                                '$ref': '#/definitions/analysis',
-                            },
-                        },
-                    },
-                },
-            },
-        )
-        # Other shemas not yet referenced
+        with pytest.raises(APISpecError, match='Name resolver returned None for schema'):
+            spec.components.schema('analysis', schema=schema)
+
+    @pytest.mark.parametrize('schema', [AnalysisSchema, AnalysisSchema()])
+    def test_warning_when_schema_added_twice(self, spec, schema):
+        spec.components.schema('Analysis', schema=schema)
+        with pytest.warns(UserWarning, match='has already been added to the spec'):
+            spec.components.schema('Analysis', schema=schema)
+
+    def test_schema_instances_with_different_modifiers_added(self, spec):
+        class MultiModifierSchema(Schema):
+            pet_unmodified = Nested(PetSchema)
+            pet_exclude = Nested(PetSchema, exclude=('name',))
+
+        spec.components.schema('Pet', schema=PetSchema())
+        spec.components.schema('Pet_Exclude', schema=PetSchema(exclude=('name',)))
+
+        spec.components.schema('MultiModifierSchema', schema=MultiModifierSchema)
+
         definitions = get_definitions(spec)
-        assert 1 == len(definitions)
-        assert 'analysis' in definitions
-        # Inspect/Read objects will not auto reference because resolver func
-        # return None
-        json.dumps(spec.to_dict())
-        # Other shema still not referenced
+        pet_unmodified_ref = definitions['MultiModifierSchema']['properties']['pet_unmodified']
+        assert pet_unmodified_ref == {'$ref': ref_path(spec) + 'Pet'}
+
+        pet_exclude = definitions['MultiModifierSchema']['properties']['pet_exclude']
+        assert pet_exclude == {'$ref': ref_path(spec) + 'Pet_Exclude'}
+
+    def test_schema_with_clashing_names(self, spec):
+        class Pet(PetSchema):
+            another_field = String()
+
+        class NameClashSchema(Schema):
+            pet_1 = Nested(PetSchema)
+            pet_2 = Nested(Pet)
+
+        with pytest.warns(UserWarning, match='Multiple schemas resolved to the name Pet'):
+            spec.components.schema('NameClashSchema', schema=NameClashSchema)
+
         definitions = get_definitions(spec)
-        assert 1 == len(definitions)
 
+        assert 'Pet' in definitions
+        assert 'Pet1' in definitions
 
 class TestComponentParameterHelper:
 
@@ -164,14 +181,16 @@ class TestComponentParameterHelper:
         parameter = get_parameters(spec)['Pet']
         assert parameter['in'] == 'body'
         if spec.openapi_version.major < 3:
-            schema = parameter['schema']['properties']
+            reference = parameter['schema']
         else:
-            schema = parameter['content']['application/json']['schema']['properties']
+            reference = parameter['content']['application/json']['schema']
+
+        assert reference == {'$ref': ref_path(spec) + 'Pet'}
 
-        assert schema['name']['type'] == 'string'
-        assert schema['password']['type'] == 'string'
-        # dump_only field is ignored
-        assert 'id' not in schema
+        resolved_schema = spec.components._schemas['Pet']
+        assert resolved_schema['properties']['name']['type'] == 'string'
+        assert resolved_schema['properties']['password']['type'] == 'string'
+        assert resolved_schema['properties']['id']['type'] == 'integer'
 
 
 class TestComponentResponseHelper:
@@ -185,14 +204,16 @@ class TestComponentResponseHelper:
         spec.components.response('GetPetOk', **kwargs)
         response = get_responses(spec)['GetPetOk']
         if spec.openapi_version.major < 3:
-            schema = response['schema']['properties']
+            reference = response['schema']
         else:
-            schema = response['content']['application/json']['schema']['properties']
+            reference = response['content']['application/json']['schema']
 
-        assert schema['id']['type'] == 'integer'
-        assert schema['name']['type'] == 'string'
-        # load_only field is ignored
-        assert 'password' not in schema
+        assert reference == {'$ref': ref_path(spec) + 'Pet'}
+
+        resolved_schema = spec.components._schemas['Pet']
+        assert resolved_schema['properties']['id']['type'] == 'integer'
+        assert resolved_schema['properties']['name']['type'] == 'string'
+        assert resolved_schema['properties']['password']['type'] == 'string'
 
 
 class TestCustomField:
@@ -261,8 +282,12 @@ class TestOperationHelper:
             },
         )
         get = get_paths(spec_fixture.spec)['/pet']['get']
-        assert get['responses'][200]['schema'] == spec_fixture.openapi.schema2jsonschema(
-            PetSchema, dump=True, load=False,
+        reference = get['responses'][200]['schema']
+        assert reference == {'$ref': self.ref_path(spec_fixture.spec) + 'Pet'}
+        assert len(spec_fixture.spec.components._schemas) == 1
+        resolved_schema = spec_fixture.spec.components._schemas['Pet']
+        assert resolved_schema == spec_fixture.openapi.schema2jsonschema(
+            PetSchema,
         )
         assert get['responses'][200]['description'] == 'successful operation'
 
@@ -287,9 +312,13 @@ class TestOperationHelper:
             },
         )
         get = get_paths(spec_fixture.spec)['/pet']['get']
-        resolved_schema = get['responses'][200]['content']['application/json']['schema']
+        reference = get['responses'][200]['content']['application/json']['schema']
+
+        assert reference == {'$ref': self.ref_path(spec_fixture.spec) + 'Pet'}
+        assert len(spec_fixture.spec.components._schemas) == 1
+        resolved_schema = spec_fixture.spec.components._schemas['Pet']
         assert resolved_schema == spec_fixture.openapi.schema2jsonschema(
-            PetSchema, dump=True, load=False,
+            PetSchema,
         )
         assert get['responses'][200]['description'] == 'successful operation'
 
@@ -361,7 +390,7 @@ class TestOperationHelper:
             name = parameter['name']
             assert description == PetSchema.description[name]
         post = p['post']
-        post_schema = spec_fixture.openapi.resolve_schema_dict(PetSchema, dump=False, load=True)
+        post_schema = spec_fixture.openapi.resolve_schema_dict(PetSchema)
         assert post['requestBody']['content']['application/json']['schema'] == post_schema
         assert post['requestBody']['description'] == 'a pet schema'
         assert post['requestBody']['required']
@@ -622,8 +651,6 @@ class TestCircularReference:
 
     def test_circular_referencing_schemas(self, spec):
         spec.components.schema('Analysis', schema=AnalysisSchema)
-        spec.components.schema('Sample', schema=SampleSchema)
-        spec.components.schema('Run', schema=RunSchema)
         definitions = get_definitions(spec)
         ref = definitions['Analysis']['properties']['sample']['$ref']
         assert ref == ref_path(spec) + 'Sample'
@@ -716,3 +743,35 @@ class TestDictValues:
         spec.components.schema('SchemaWithDict', schema=SchemaWithDict)
         result = get_definitions(spec)['SchemaWithDict']['properties']['dict_field']
         assert result == {'type': 'object'}
+
+    def test_dict_with_nested(self, spec):
+
+        class SchemaWithDict(Schema):
+            dict_field = Dict(values=Nested(PetSchema))
+
+        spec.components.schema('SchemaWithDict', schema=SchemaWithDict)
+
+        assert len(get_definitions(spec)) == 2
+
+        result = get_definitions(spec)['SchemaWithDict']['properties']['dict_field']
+        assert result == {
+            'additionalProperties': {'$ref': ref_path(spec) + 'Pet'},
+            'type': 'object',
+        }
+
+
+class TestList:
+    def test_list_with_nested(self, spec):
+
+        class SchemaWithList(Schema):
+            list_field = List(Nested(PetSchema))
+
+        spec.components.schema('SchemaWithList', schema=SchemaWithList)
+
+        assert len(get_definitions(spec)) == 2
+
+        result = get_definitions(spec)['SchemaWithList']['properties']['list_field']
+        assert result == {
+            'items': {'$ref': ref_path(spec) + 'Pet'},
+            'type': 'array',
+        }
diff --git a/tests/test_ext_marshmallow_common.py b/tests/test_ext_marshmallow_common.py
new file mode 100644
index 0000000..050404e
--- /dev/null
+++ b/tests/test_ext_marshmallow_common.py
@@ -0,0 +1,41 @@
+# -*- coding: utf-8 -*-
+import pytest
+
+from apispec.ext.marshmallow.common import make_schema_key, get_unique_schema_name
+from .schemas import PetSchema, SampleSchema
+
+
+class TestMakeSchemaKey:
+    def test_raise_if_schema_class_passed(self):
+        with pytest.raises(TypeError, match='based on a Schema instance'):
+            make_schema_key(PetSchema)
+
+    def test_same_schemas_instances_equal(self):
+        assert make_schema_key(PetSchema()) == make_schema_key(PetSchema())
+
+    def test_different_schemas_not_equal(self):
+        assert make_schema_key(PetSchema()) != make_schema_key(SampleSchema())
+
+    def test_instances_with_different_modifiers_not_equal(self):
+        assert make_schema_key(PetSchema()) != make_schema_key(PetSchema(partial=True))
+
+
+class TestUniqueName:
+    def test_unique_name(self, spec):
+        properties = {
+            'id': {'type': 'integer', 'format': 'int64'},
+            'name': {'type': 'string', 'example': 'doggie'},
+        }
+
+        name = get_unique_schema_name(spec.components, 'Pet')
+        assert name == 'Pet'
+
+        spec.components.schema('Pet', properties=properties)
+        with pytest.warns(UserWarning, match='Multiple schemas resolved to the name Pet'):
+            name_1 = get_unique_schema_name(spec.components, 'Pet')
+        assert name_1 == 'Pet1'
+
+        spec.components.schema('Pet1', properties=properties)
+        with pytest.warns(UserWarning, match='Multiple schemas resolved to the name Pet'):
+            name_2 = get_unique_schema_name(spec.components, 'Pet')
+        assert name_2 == 'Pet2'
diff --git a/tests/test_openapi.py b/tests/test_openapi.py
index cb05d68..dd9b8e4 100644
--- a/tests/test_openapi.py
+++ b/tests/test_openapi.py
@@ -8,15 +8,10 @@ from pytest import mark
 from marshmallow import fields, Schema, validate
 
 from apispec.ext.marshmallow import MarshmallowPlugin
-from apispec.ext.marshmallow.openapi import OpenAPIConverter, MARSHMALLOW_VERSION_INFO
+from apispec.ext.marshmallow.openapi import MARSHMALLOW_VERSION_INFO
 from apispec import exceptions, utils, APISpec
 
-from .utils import get_definitions
-
-
-@pytest.fixture(params=('2.0', '3.0.0'))
-def openapi(request):
-    return OpenAPIConverter(openapi_version=request.param)
+from .utils import get_definitions, ref_path
 
 
 class TestMarshmallowFieldToOpenAPI:
@@ -90,12 +85,12 @@ class TestMarshmallowFieldToOpenAPI:
 
     def test_field_with_missing_load(self, openapi):
         field = fields.Str(default='foo', missing='bar')
-        res = openapi.field2property(field, dump=False)
+        res = openapi.field2property(field)
         assert res['default'] == 'bar'
 
     def test_field_with_boolean_false_missing_load(self, openapi):
         field = fields.Boolean(default=None, missing=False)
-        res = openapi.field2property(field, dump=False)
+        res = openapi.field2property(field)
         assert res['default'] is False
 
     def test_field_with_missing_callable(self, openapi):
@@ -423,36 +418,6 @@ class TestMarshmallowSchemaToModelDefinition:
         assert excinfo.value.args[0] == ("{0!r} doesn't have either `fields` "
                                          'or `_declared_fields`'.format(NotASchema))
 
-    def test_dump_only_load_only_fields(self, openapi):
-        class UserSchema(Schema):
-            _id = fields.Str(dump_only=True)
-            name = fields.Str()
-            password = fields.Str(load_only=True)
-
-        res = openapi.schema2jsonschema(UserSchema())
-        props = res['properties']
-        assert 'name' in props
-        # dump_only field appears with readOnly attribute
-        assert '_id' in props
-        assert 'readOnly' in props['_id']
-        # load_only field appears (writeOnly attribute does not exist)
-        assert 'password' in props
-        if openapi.openapi_version.major < 3:
-            assert 'writeOnly' not in props['password']
-        else:
-            assert 'writeOnly' in props['password']
-
-        res = openapi.schema2jsonschema(UserSchema(), dump=False)
-        props = res['properties']
-        assert 'name' in props
-        assert '_id' not in props
-        assert 'password' in props
-
-        res = openapi.schema2jsonschema(UserSchema(), load=False)
-        props = res['properties']
-        assert 'name' in props
-        assert '_id' in props
-        assert 'password' not in props
 
 class TestMarshmallowSchemaToParameters:
 
@@ -490,8 +455,10 @@ class TestMarshmallowSchemaToParameters:
         assert len(res) == 1
         param = res[0]
         assert param['in'] == 'body'
-        assert param['schema'] == openapi.schema2jsonschema(UserSchema)
+        assert param['schema'] == {'$ref': '#/definitions/User'}
 
+    # json/body is invalid for OpenAPI 3
+    @pytest.mark.parametrize('openapi', ('2.0', ), indirect=True)
     def test_schema_body_with_dump_only(self, openapi):
         class UserSchema(Schema):
             name = fields.Str()
@@ -501,8 +468,7 @@ class TestMarshmallowSchemaToParameters:
         assert len(res_nodump) == 1
         param = res_nodump[0]
         assert param['in'] == 'body'
-        assert param['schema'] == openapi.schema2jsonschema(UserSchema, dump=False)
-        assert set(param['schema']['properties'].keys()) == {'name'}
+        assert param['schema'] == {'$ref': ref_path(openapi.spec) + 'User'}
 
     # json/body is invalid for OpenAPI 3
     @pytest.mark.parametrize('openapi', ('2.0', ), indirect=True)
@@ -516,8 +482,7 @@ class TestMarshmallowSchemaToParameters:
         param = res[0]
         assert param['in'] == 'body'
         assert param['schema']['type'] == 'array'
-        assert param['schema']['items']['type'] == 'object'
-        assert param['schema']['items'] == openapi.schema2jsonschema(UserSchema)
+        assert param['schema']['items'] == {'$ref': '#/definitions/User'}
 
     def test_schema_query(self, openapi):
         class UserSchema(Schema):
@@ -637,30 +602,19 @@ class TestNesting:
         assert ret['items'] == {'$ref': self.ref_path(spec_fixture.spec) + 'Category'}
 
     def test_field2property_nested_ref(self, openapi):
-        category = fields.Nested(CategorySchema)
-        assert openapi.field2property(category) == openapi.schema2jsonschema(CategorySchema)
-
         cat_with_ref = fields.Nested(CategorySchema, ref='Category')
         assert openapi.field2property(cat_with_ref) == {'$ref': 'Category'}
 
     def test_field2property_nested_ref_with_meta(self, openapi):
-        category = fields.Nested(CategorySchema)
-        assert openapi.field2property(category) == openapi.schema2jsonschema(CategorySchema)
-
         cat_with_ref = fields.Nested(CategorySchema, ref='Category', description='A category')
         result = openapi.field2property(cat_with_ref)
         assert result == {'$ref': 'Category', 'description': 'A category'}
 
-    def test_field2property_nested_many(self, openapi):
+    def test_field2property_nested_many(self, spec_fixture):
         categories = fields.Nested(CategorySchema, many=True)
-        res = openapi.field2property(categories)
-        assert res['type'] == 'array'
-        assert res['items'] == openapi.schema2jsonschema(CategorySchema)
-
-        cats_with_ref = fields.Nested(CategorySchema, many=True, ref='Category')
-        res = openapi.field2property(cats_with_ref)
+        res = spec_fixture.openapi.field2property(categories)
         assert res['type'] == 'array'
-        assert res['items'] == {'$ref': 'Category'}
+        assert res['items'] == {'$ref': ref_path(spec_fixture.spec) + 'Category'}
 
     def test_field2property_nested_self_without_name_raises_error(self, openapi):
         self_nesting = fields.Nested('self')
@@ -693,47 +647,38 @@ class TestNesting:
         res = openapi.field2property(self_nesting2, name='Foo')
         assert res == {'$ref': '#/definitions/Bar'}
 
-    def test_field2property_nested_dump_only(self, openapi):
-        category = fields.Nested(CategorySchema)
-        res = openapi.field2property(category, name='Foo', dump=False)
-        props = res['properties']
-        assert 'breed' not in props
-
-    def test_field2property_nested_dump_only_with_spec(self, openapi):
-        category = fields.Nested(CategorySchema)
-        res = openapi.field2property(category, name='Foo', dump=False)
+    def test_schema2jsonschema_with_nested_fields(self, spec_fixture):
+        res = spec_fixture.openapi.schema2jsonschema(PetSchema, use_refs=False)
         props = res['properties']
-        assert 'breed' not in props
 
-    def test_schema2jsonschema_with_nested_fields(self, openapi):
-        res = openapi.schema2jsonschema(PetSchema, use_refs=False)
-        props = res['properties']
-        assert props['category']['items'] == openapi.schema2jsonschema(CategorySchema)
+        assert props['category']['items'] == {'$ref': ref_path(spec_fixture.spec) + 'Category'}
 
-    def test_schema2jsonschema_with_nested_fields_with_adhoc_changes(self, openapi):
+    def test_schema2jsonschema_with_nested_fields_with_adhoc_changes(self, spec_fixture):
         category_schema = CategorySchema(many=True)
         category_schema.fields['id'].required = True
 
         class PetSchema(Schema):
-            category = fields.Nested(category_schema, many=True, ref='#/definitions/Category')
+            category = fields.Nested(category_schema, many=True)
             name = fields.Str()
 
-        res = openapi.schema2jsonschema(PetSchema(), use_refs=False)
-        props = res['properties']
-        assert props['category'] == openapi.schema2jsonschema(category_schema)
-        assert set(props['category']['items']['required']) == {'id', 'name'}
+        spec_fixture.spec.components.schema('Pet', schema=PetSchema)
+        props = get_definitions(spec_fixture.spec)
 
-        props['category']['items']['required'] = ['name']
-        assert props['category']['items'] == openapi.schema2jsonschema(CategorySchema)
+        assert props['Category'] == spec_fixture.openapi.schema2jsonschema(category_schema)
+        assert set(props['Category']['items']['required']) == {'id', 'name'}
 
-    def test_schema2jsonschema_with_nested_excluded_fields(self, openapi):
+        props['Category']['items']['required'] = ['name']
+        assert props['Category']['items'] == spec_fixture.openapi.schema2jsonschema(CategorySchema)
+
+    def test_schema2jsonschema_with_nested_excluded_fields(self, spec):
         category_schema = CategorySchema(exclude=('breed', ))
 
         class PetSchema(Schema):
             category = fields.Nested(category_schema)
 
-        res = openapi.schema2jsonschema(PetSchema(), use_refs=False)
-        category_props = res['properties']['category']['properties']
+        spec.components.schema('Pet', schema=PetSchema)
+
+        category_props = get_definitions(spec)['Category']['properties']
         assert 'breed' not in category_props
 
     def test_nested_field_with_property(self, spec_fixture):
diff --git a/tests/utils.py b/tests/utils.py
index 70350ff..1a56bcf 100644
--- a/tests/utils.py
+++ b/tests/utils.py
@@ -18,3 +18,8 @@ def get_responses(spec):
 
 def get_paths(spec):
     return spec.to_dict()['paths']
+
+def ref_path(spec):
+    if spec.openapi_version.version[0] < 3:
+        return '#/definitions/'
+    return '#/components/schemas/'