Repository: marshmallow-code/marshmallow

Issue:
How to pass parameters to a nested schema which is referred by name
I would like to refer to a schema by its name when I use it as a nested one, but still I'd like to pass custom arguments to its constructor.

For example instead of doing this:
```python
class AuthorSchema(Schema):
    books = fields.Nested(BookSchema(enveloped=True, many=True))
    class Meta:
        fields = ('id', 'name', 'books')
```

I'd like to use something like this:

```python
class AuthorSchema(Schema):
    books = fields.Nested('BookSchema', enveloped=True, many=True)
    class Meta:
        fields = ('id', 'name', 'books')
```

Please note that in this case the `BookSchema`'s `__init__` method is looking for a custom parameter called `enveloped`.

Is there any official way that I can achieve this using only the native `Nested` field?

I'm using the following versions of marshmallow:
`marshmallow==2.16.3`
`flask-marshmallow==0.9.0`

Discussion:
sloria: Will this cause any problems for apispec? @lafrech 
taion: Oh this is neat. Do you think there's any value to supporting the SQLAlchemy-style syntax of `lambda: FooSchema` that doesn't construct the instance? I guess not, given the somewhat different semantics here.
sloria: It's not explicitly tested because I don't think we need to officially support that usage, but it should work anyway.
lafrech: > Will this cause any problems for apispec? @lafrech

I suppose it wouldn't since the nested field instance is resolved on first call to schema cached property.
sloria: > Deprecating only and exclude on Nested sort of deprecates the Nested('MySchema', only=.., exclude=...) usage, indirectly. If the string name usage is deprecated then removed, the class_registry becomes unused within marshmallow. Do we want to keep support for the class_registry moving forward, or should we drop it?

Maybe we hold off on deprecating the string name usage (as well as the `only` and `exclude` params, by extension). It's a convenient way to avoid circular imports.

It's worth noting that SQLAlchemy also supports string names to `relationship`.
sloria: We could just document `lambda` as the preferred way, with a sidenote about using string names for avoiding circular imports.
lafrech: So finally we'd have

- the string way to avoid circular imports but without the ability to pass constructor arguments
- the lambda way to pass constructor arguments but circular imports issues

That's double API surface with still unsupported cases.


|    | Standard case | Constructor args | Circular import | Constructor args + Circular import
| ----| ---- | ---- | ---- |  ---- | 
| string | X |  |  X | 
| lambda | X | X |  |  |

 The last case could be covered with string + a `constructor_(kw)args` parameter. Would we still need lambda if we did that?
sloria: The lambda/callable usage can actually handle the circular import, but in a clunky way.

```python
def get_artist_schema():
    from .artists import ArtistSchema
    return ArtistSchema()

class AlbumSchema(Schema):
    artist = fields.Nested(get_artist_schema)
```

In most cases, lambda is strictly better than string names because it doesn't break static analysis. The string usage is only provided for convenience to avoid the circular import cases. We could decide to deprecate/remove it later.

So it's a bit more like

|        | Standard case | Constructor args | Circular import |
| ------ | ------------- | ---------------- | --------------- |
| string | X (clunky)    |                  | X               |
| lambda | X             | X                | X (clunky)      |

Kamforka: Oh yeah, that was the reason behind my implementation of the string constructor and argument passing, to avoid circular imports, now I remember.

Regarding to static analysis / linting, I guess you are also breaking the import guidelines by not having imports at the top of your modules, but by lazily importing the schemas in your factory function, it's clunky indeed.

Unfortunately both ways have their lows and ups but I guess only one should be supported not both.
sloria: If I haaad to choose one way, it'd be the `lambda`. It meets all use cases and doesn't break go-to in my editor =P

@lafrech @Kamforka What do you think about

1. Removing the deprecations from this PR
1. Deprecating `Nested('self')`
1. Documenting `lambda` as the preferred way (updating all existing 2-way nesting examples), with a sidenote about the string way for circular import cases.
1. Deciding in a future 3.x release whether we want to deprecate string/many/exclude.
taion: For the lambda case, can you not do:

```python
from . import artists

class AlbumSchema(Schema):
    artist = fields.Nested(lambda: artists.ArtistSchema())
```

As long as the evaluation is delayed, I think this should be okay? You do get a circular import, but it should still work.
sloria: @taion That will work sometimes but will break in cases like

```python
# core.py
from . import artists


class EntityMixin:
    id = fields.UUID()

class AlbumSchema(EntityMixin, Schema):
    artist = fields.Nested(lambda: artists.ArtistSchema())
```

```python
# artists.py

from marshmallow import Schema, fields
from . import core


class ArtistSchema(core.EntityMixin, Schema):
    name = fields.Str()
```
sloria: Indeed, you can fix the above cases with some code re-org.

I still think we should iteratively move users toward the `lambda` usage, i.e. defer deprecation of string names until we decide all use cases can be met with `lambda` without too much finagling.
s16h: Do we know what the ETA on merging this is?
sloria: @lafrech @deckar01 Can you take a look at this?
lafrech: Sorry for the delay.

LGTM.
Kamforka: I'm a bit late on this but my original solution was introduced to solve the circular import issue with nested schemas, when both schema nests the other.
I guess this one will still crash with a circular import error in that case, or am I missing something?
sloria: @Kamforka See https://github.com/marshmallow-code/marshmallow/pull/1382#issuecomment-530352031 . It's technically possible to avoid circular imports but it's klunky. The proposal here is to keep support for string names in the short term, but `lambda: ...` is the preferred API.

Patch:
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 15993b2c..1a4e0ed8 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -1,12 +1,63 @@
 Changelog
 ---------
 
+3.3.0 (unreleased)
+******************
+
+Features:
+
+- ``fields.Nested`` may take a callable that returns a schema instance.
+  Use this to resolve order-of-declaration issues when schemas nest each other (:issue:`1146`).
+
+.. code-block:: python
+
+    # <3.3
+    class AlbumSchema(Schema):
+        title = fields.Str()
+        artist = fields.Nested("ArtistSchema", only=("name",))
+
+
+    class ArtistSchema(Schema):
+        name = fields.Str()
+        albums = fields.List(fields.Nested(AlbumSchema))
+
+
+    # >=3.3
+    class AlbumSchema(Schema):
+        title = fields.Str()
+        artist = fields.Nested(lambda: ArtistSchema(only=("name",)))
+
+
+    class ArtistSchema(Schema):
+        name = fields.Str()
+        albums = fields.List(fields.Nested(AlbumSchema))
+
+Deprecations:
+
+- Passing the string ``"self"`` to ``fields.Nested`` is deprecated.
+  Use a callable instead.
+
+.. code-block:: python
+
+    from marshmallow import Schema, fields
+
+    # <3.3
+    class PersonSchema(Schema):
+        partner = fields.Nested("self", exclude=("partner",))
+        friends = fields.List(fields.Nested("self"))
+
+
+    # >=3.3
+    class PersonSchema(Schema):
+        partner = fields.Nested(lambda: PersonSchema(exclude=("partner")))
+        friends = fields.List(fields.Nested(lambda: PersonSchema()))
+
 3.2.2 (2019-11-04)
 ******************
 
 Bug fixes:
 
-- Don't load fields for which ``load_only`` and ``dump_only`` are both ``True`` (:pr:`1448`). 
+- Don't load fields for which ``load_only`` and ``dump_only`` are both ``True`` (:pr:`1448`).
 - Fix types in ``marshmallow.validate`` (:pr:`1446`).
 
 Support:
diff --git a/docs/nesting.rst b/docs/nesting.rst
index 66da5df7..6ded19d7 100644
--- a/docs/nesting.rst
+++ b/docs/nesting.rst
@@ -22,7 +22,7 @@ Schemas can be nested to represent relationships between objects (e.g. foreign k
             self.title = title
             self.author = author  # A User object
 
-Use a :class:`Nested <marshmallow.fields.Nested>` field to represent the relationship, passing in a nested schema class.
+Use a :class:`Nested <marshmallow.fields.Nested>` field to represent the relationship, passing in a nested schema.
 
 .. code-block:: python
 
@@ -53,24 +53,24 @@ The serialized blog will have the nested user representation.
     #             'created_at': '2014-08-17T14:58:57.600623+00:00'}}
 
 .. note::
-    If the field is a collection of nested objects, you must set ``many=True``.
+    If the field is a collection of nested objects, pass the `Nested <marshmallow.fields.Nested>` field to `List <marshmallow.fields.List>`.
 
     .. code-block:: python
 
-        collaborators = fields.Nested(UserSchema, many=True)
+        collaborators = fields.List(fields.Nested(UserSchema))
 
 .. _specifying-nested-fields:
 
 Specifying Which Fields to Nest
 -------------------------------
 
-You can explicitly specify which attributes of the nested objects you want to serialize with the ``only`` argument.
+You can explicitly specify which attributes of the nested objects you want to (de)serialize with the ``only`` argument to the schema.
 
 .. code-block:: python
 
     class BlogSchema2(Schema):
         title = fields.String()
-        author = fields.Nested(UserSchema, only=["email"])
+        author = fields.Nested(UserSchema(only=("email",)))
 
 
     schema = BlogSchema2()
@@ -81,7 +81,7 @@ You can explicitly specify which attributes of the nested objects you want to se
     #     'author': {'email': u'monty@python.org'}
     # }
 
-You can represent the attributes of deeply nested objects using dot delimiters.
+Dotted paths may be passed to ``only`` and ``exclude`` to specify nested attributes.
 
 .. code-block:: python
 
@@ -89,7 +89,7 @@ You can represent the attributes of deeply nested objects using dot delimiters.
         blog = fields.Nested(BlogSchema2)
 
 
-    schema = SiteSchema(only=["blog.author.email"])
+    schema = SiteSchema(only=("blog.author.email",))
     result = schema.dump(site)
     pprint(result)
     # {
@@ -125,8 +125,6 @@ You can replace nested data with a single value (or flat list of values if ``man
     # }
 
 
-You can also exclude fields by passing in an ``exclude`` list. This argument also allows representing the attributes of deeply nested objects using dot delimiters.
-
 .. _partial-loading:
 
 Partial Loading
@@ -168,27 +166,29 @@ You can specify a subset of the fields to allow partial loading using dot delimi
 Two-way Nesting
 ---------------
 
-If you have two objects that nest each other, you can refer to a nested schema by its class name. This allows you to nest Schemas that have not yet been defined.
-
+If you have two objects that nest each other, you can pass a callable to `Nested <marshmallow.fields.Nested>`.
+This allows you to resolve order-of-declaration issues, such as when one schema nests a schema that is declared below it.
 
-For example, a representation of an ``Author`` model might include the books that have a foreign-key (many-to-one) relationship to it. Correspondingly, a representation of a ``Book`` will include its author representation.
+For example, a representation of an ``Author`` model might include the books that have a many-to-one relationship to it.
+Correspondingly, a representation of a ``Book`` will include its author representation.
 
 .. code-block:: python
 
-    class AuthorSchema(Schema):
-        # Make sure to use the 'only' or 'exclude' params
+    class BookSchema(Schema):
+        id = fields.Int(dump_only=True)
+        title = fields.Str()
+
+        # Make sure to use the 'only' or 'exclude'
         # to avoid infinite recursion
-        books = fields.Nested("BookSchema", many=True, exclude=("author",))
+        author = fields.Nested(lambda: AuthorSchema(only=("id", "title")))
 
-        class Meta:
-            fields = ("id", "name", "books")
 
+    class AuthorSchema(Schema):
+        id = fields.Int(dump_only=True)
+        title = fields.Str()
 
-    class BookSchema(Schema):
-        author = fields.Nested(AuthorSchema, only=("id", "name"))
+        books = fields.List(fields.Nested(BookSchema(exclude=("author",))))
 
-        class Meta:
-            fields = ("id", "title", "author")
 
 .. code-block:: python
 
@@ -221,27 +221,54 @@ For example, a representation of an ``Author`` model might include the books tha
     #   ]
     # }
 
+You can also pass a class name as a string to `Nested <marshmallow.fields.Nested>`.
+This is useful for avoiding circular imports when your schemas are located in different modules.
+
+.. code-block:: python
+
+    # books.py
+    from marshmallow import Schema, fields
+
+
+    class BookSchema(Schema):
+        id = fields.Int(dump_only=True)
+        title = fields.Str()
+
+        author = fields.Nested("AuthorSchema", only=("id", "title"))
+
+.. code-block:: python
+
+    # authors.py
+    from marshmallow import Schema, fields
+
+
+    class AuthorSchema(Schema):
+        id = fields.Int(dump_only=True)
+        title = fields.Str()
+
+        books = fields.List(fields.Nested("BookSchema", exclude=("author",)))
+
 .. note::
-    If you need to, you can also pass the full, module-qualified path to `fields.Nested`. ::
 
-        books = fields.Nested('path.to.BookSchema',
-                              many=True, exclude=('author', ))
+    If you have multiple schemas with the same class name, you must pass the full, module-qualified path. ::
+
+        author = fields.Nested("authors.BookSchema", only=("id", "title"))
 
 .. _self-nesting:
 
 Nesting A Schema Within Itself
 ------------------------------
 
-If the object to be marshalled has a relationship to an object of the same type, you can nest the `Schema` within itself by passing ``"self"`` (with quotes) to the :class:`Nested <marshmallow.fields.Nested>` constructor.
+If the object to be marshalled has a relationship to an object of the same type, you can nest the `Schema` within itself by passing a callable that returns an instance of the same schema.
 
 .. code-block:: python
 
     class UserSchema(Schema):
         name = fields.String()
         email = fields.Email()
-        friends = fields.Nested("self", many=True)
         # Use the 'exclude' argument to avoid infinite recursion
-        employer = fields.Nested("self", exclude=("employer",), default=None)
+        employer = fields.Nested(lambda: UserSchema(exclude=("employer",)))
+        friends = fields.List(fields.Nested(lambda: UserSchema()))
 
 
     user = User("Steve", "steve@example.com")
diff --git a/docs/upgrading.rst b/docs/upgrading.rst
index c01d13a9..ea7d6f9d 100644
--- a/docs/upgrading.rst
+++ b/docs/upgrading.rst
@@ -3,6 +3,55 @@ Upgrading to Newer Releases
 
 This section documents migration paths to new releases.
 
+Upgrading to 3.3
+++++++++++++++++
+
+In 3.3, `fields.Nested <marshmallow.fields.Nested>` may take a callable that returns a schema instance. 
+Use this to resolve order-of-declaration issues when schemas nest each other.
+
+.. code-block:: python
+
+    from marshmallow import Schema, fields
+
+    # <3.3
+    class AlbumSchema(Schema):
+        title = fields.Str()
+        artist = fields.Nested("ArtistSchema", only=("name",))
+
+
+    class ArtistSchema(Schema):
+        name = fields.Str()
+        albums = fields.List(fields.Nested(AlbumSchema))
+
+
+    # >=3.3
+    class AlbumSchema(Schema):
+        title = fields.Str()
+        artist = fields.Nested(lambda: ArtistSchema(only=("name",)))
+
+
+    class ArtistSchema(Schema):
+        name = fields.Str()
+        albums = fields.List(fields.Nested(AlbumSchema))
+
+A callable should also be used when nesting a schema within itself.
+Passing ``"self"`` is deprecated.
+
+.. code-block:: python
+
+    from marshmallow import Schema, fields
+
+    # <3.3
+    class PersonSchema(Schema):
+        partner = fields.Nested("self", exclude=("partner",))
+        friends = fields.List(fields.Nested("self"))
+
+
+    # >=3.3
+    class PersonSchema(Schema):
+        partner = fields.Nested(lambda: PersonSchema(exclude=("partner")))
+        friends = fields.List(fields.Nested(lambda: PersonSchema()))
+
 .. _upgrading_3_0:
 
 Upgrading to 3.0
diff --git a/docs/why.rst b/docs/why.rst
index 08444e30..9f86bbaa 100644
--- a/docs/why.rst
+++ b/docs/why.rst
@@ -33,8 +33,8 @@ As an example, you might have a JSON endpoint for retrieving all information abo
 
     class GameStateSchema(Schema):
         _id = fields.UUID(required=True)
-        players = fields.Nested(PlayerSchema, many=True)
         score = fields.Nested(ScoreSchema)
+        players = fields.List(fields.Nested(PlayerSchema))
         last_changed = fields.DateTime(format="rfc")
 
         class Meta:
diff --git a/examples/peewee_example.py b/examples/peewee_example.py
index a28c4fef..d4da8bf9 100644
--- a/examples/peewee_example.py
+++ b/examples/peewee_example.py
@@ -77,7 +77,7 @@ class UserSchema(Schema):
 class TodoSchema(Schema):
     id = fields.Int(dump_only=True)
     done = fields.Boolean(attribute="is_done", missing=False)
-    user = fields.Nested(UserSchema, exclude=("joined_on", "password"), dump_only=True)
+    user = fields.Nested(UserSchema(exclude=("joined_on", "password")), dump_only=True)
     content = fields.Str(required=True)
     posted_on = fields.DateTime(dump_only=True)
 
diff --git a/src/marshmallow/fields.py b/src/marshmallow/fields.py
index 003c33ae..95392e75 100644
--- a/src/marshmallow/fields.py
+++ b/src/marshmallow/fields.py
@@ -437,10 +437,19 @@ class Nested(Field):
 
     Examples: ::
 
-        user = fields.Nested(UserSchema)
-        user2 = fields.Nested('UserSchema')  # Equivalent to above
-        collaborators = fields.Nested(UserSchema, many=True, only=('id',))
-        parent = fields.Nested('self')
+        class ChildSchema(Schema):
+            id = fields.Str()
+            name = fields.Str()
+            # Use lambda functions when you need two-way nesting or self-nesting
+            parent = fields.Nested(lambda: ParentSchema(only=("id",)), dump_only=True)
+            siblings = fields.List(fields.Nested(lambda: ChildSchema(only=("id", "name"))))
+
+        class ParentSchema(Schema):
+            id = fields.Str()
+            children = fields.List(
+                fields.Nested(ChildSchema(only=("id", "parent", "siblings")))
+            )
+            spouse = fields.Nested(lambda: ParentSchema(only=("id",)))
 
     When passing a `Schema <marshmallow.Schema>` instance as the first argument,
     the instance's ``exclude``, ``only``, and ``many`` attributes will be respected.
@@ -471,7 +480,7 @@ class Nested(Field):
 
     def __init__(
         self,
-        nested: typing.Union[SchemaABC, type, str],
+        nested: typing.Union[SchemaABC, type, str, typing.Callable[[], SchemaABC]],
         *,
         default: typing.Any = missing_,
         only: types.StrSequenceOrSet = None,
@@ -487,6 +496,12 @@ class Nested(Field):
             raise StringNotCollectionError(
                 '"exclude" should be a collection of strings.'
             )
+        if nested == "self":
+            warnings.warn(
+                "Passing 'self' to `Nested` is deprecated. "
+                "Use `Nested(lambda: MySchema(...))` instead.",
+                DeprecationWarning,
+            )
         self.nested = nested
         self.only = only
         self.exclude = exclude
@@ -505,8 +520,13 @@ class Nested(Field):
         if not self._schema:
             # Inherit context from parent.
             context = getattr(self.parent, "context", {})
-            if isinstance(self.nested, SchemaABC):
-                self._schema = copy.copy(self.nested)
+            if callable(self.nested) and not isinstance(self.nested, type):
+                nested = self.nested()
+            else:
+                nested = self.nested
+
+            if isinstance(nested, SchemaABC):
+                self._schema = copy.copy(nested)
                 self._schema.context.update(context)
                 # Respect only and exclude passed from parent and re-initialize fields
                 set_class = self._schema.set_class
@@ -521,20 +541,17 @@ class Nested(Field):
                     self._schema.exclude = set_class(self.exclude).union(original)
                 self._schema._init_fields()
             else:
-                if isinstance(self.nested, type) and issubclass(self.nested, SchemaABC):
-                    schema_class = self.nested
-                elif not isinstance(self.nested, (str, bytes)):
+                if isinstance(nested, type) and issubclass(nested, SchemaABC):
+                    schema_class = nested
+                elif not isinstance(nested, (str, bytes)):
                     raise ValueError(
-                        "Nested fields must be passed a "
-                        "Schema, not {}.".format(self.nested.__class__)
+                        "`Nested` fields must be passed a "
+                        "`Schema`, not {}.".format(nested.__class__)
                     )
-                elif self.nested == "self":
-                    ret = self
-                    while not isinstance(ret, SchemaABC):
-                        ret = ret.parent
-                    schema_class = ret.__class__
+                elif nested == "self":
+                    schema_class = self.root.__class__
                 else:
-                    schema_class = class_registry.get_class(self.nested)
+                    schema_class = class_registry.get_class(nested)
                 self._schema = schema_class(
                     many=self.many,
                     only=self.only,


Test patch:
diff --git a/tests/base.py b/tests/base.py
index 43822166..b2ffa87c 100644
--- a/tests/base.py
+++ b/tests/base.py
@@ -275,21 +275,21 @@ class UserRelativeUrlSchema(UserSchema):
 class BlogSchema(Schema):
     title = fields.String()
     user = fields.Nested(UserSchema)
-    collaborators = fields.Nested(UserSchema, many=True)
+    collaborators = fields.List(fields.Nested(UserSchema()))
     categories = fields.List(fields.String)
     id = fields.String()
 
 
 class BlogUserMetaSchema(Schema):
     user = fields.Nested(UserMetaSchema())
-    collaborators = fields.Nested(UserMetaSchema, many=True)
+    collaborators = fields.List(fields.Nested(UserMetaSchema()))
 
 
 class BlogSchemaMeta(Schema):
     """Same as BlogSerializer but using ``fields`` options."""
 
     user = fields.Nested(UserSchema)
-    collaborators = fields.Nested(UserSchema, many=True)
+    collaborators = fields.List(fields.Nested(UserSchema()))
 
     class Meta:
         fields = ("title", "user", "collaborators", "categories", "id")
@@ -298,7 +298,7 @@ class BlogSchemaMeta(Schema):
 class BlogOnlySchema(Schema):
     title = fields.String()
     user = fields.Nested(UserSchema)
-    collaborators = fields.Nested(UserSchema, only=("id",), many=True)
+    collaborators = fields.List(fields.Nested(UserSchema(only=("id",))))
 
 
 class BlogSchemaExclude(BlogSchema):
diff --git a/tests/test_fields.py b/tests/test_fields.py
index 846e1611..8671be95 100644
--- a/tests/test_fields.py
+++ b/tests/test_fields.py
@@ -377,6 +377,34 @@ class TestListNested:
             "children": [{"name": "Lily"}]
         }
 
+    @pytest.mark.parametrize(
+        ("param", "expected_attribute", "expected_dump"),
+        (
+            ("only", {"name"}, {"children": [{"name": "Lily"}]}),
+            ("exclude", {"name", "surname", "age"}, {"children": [{}]}),
+        ),
+    )
+    def test_list_nested_lambda_only_and_exclude_merged_with_nested(
+        self, param, expected_attribute, expected_dump
+    ):
+        class Child(Schema):
+            name = fields.String()
+            surname = fields.String()
+            age = fields.Integer()
+
+        class Family(Schema):
+            children = fields.List(
+                fields.Nested(lambda: Child(**{param: ("name", "surname")}))
+            )
+
+        schema = Family(**{param: ["children.name", "children.age"]})
+        assert (
+            getattr(schema.fields["children"].inner.schema, param) == expected_attribute
+        )
+
+        family = {"children": [{"name": "Lily", "surname": "Martinez", "age": 15}]}
+        assert schema.dump(family) == expected_dump
+
     def test_list_nested_partial_propagated_to_nested(self):
         class Child(Schema):
             name = fields.String(required=True)
diff --git a/tests/test_schema.py b/tests/test_schema.py
index 2be35b77..2106e235 100644
--- a/tests/test_schema.py
+++ b/tests/test_schema.py
@@ -1136,11 +1136,9 @@ def test_meta_nested_exclude():
 
 
 def test_nested_custom_set_not_implementing_getitem():
-    """
-    This test checks that Marshmallow can serialize implementations of
-    :mod:`collections.abc.MutableSequence`, with ``__getitem__`` arguments
-    that are not integers.
-    """
+    # This test checks that marshmallow can serialize implementations of
+    # :mod:`collections.abc.MutableSequence`, with ``__getitem__`` arguments
+    # that are not integers.
 
     class ListLikeParent:
         """
@@ -1226,6 +1224,55 @@ def test_deeply_nested_only_and_exclude():
     assert "bah" not in grand_child
 
 
+def test_nested_lambda():
+    class ChildSchema(Schema):
+        id = fields.Str()
+        name = fields.Str()
+        parent = fields.Nested(lambda: ParentSchema(only=("id",)), dump_only=True)
+        siblings = fields.List(fields.Nested(lambda: ChildSchema(only=("id", "name"))))
+
+    class ParentSchema(Schema):
+        id = fields.Str()
+        spouse = fields.Nested(lambda: ParentSchema(only=("id",)))
+        children = fields.List(
+            fields.Nested(lambda: ChildSchema(only=("id", "parent", "siblings")))
+        )
+
+    sch = ParentSchema()
+    data_to_load = {
+        "id": "p1",
+        "spouse": {"id": "p2"},
+        "children": [{"id": "c1", "siblings": [{"id": "c2", "name": "sis"}]}],
+    }
+    loaded = sch.load(data_to_load)
+    assert loaded == data_to_load
+
+    data_to_dump = dict(
+        id="p2",
+        spouse=dict(id="p2"),
+        children=[
+            dict(
+                id="c1",
+                name="bar",
+                parent=dict(id="p2"),
+                siblings=[dict(id="c2", name="sis")],
+            )
+        ],
+    )
+    dumped = sch.dump(data_to_dump)
+    assert dumped == {
+        "id": "p2",
+        "spouse": {"id": "p2"},
+        "children": [
+            {
+                "id": "c1",
+                "parent": {"id": "p2"},
+                "siblings": [{"id": "c2", "name": "sis"}],
+            }
+        ],
+    }
+
+
 @pytest.mark.parametrize("data_key", ("f1", "f5", None))
 def test_data_key_collision(data_key):
     class MySchema(Schema):
@@ -2163,13 +2210,15 @@ class TestSelfReference:
     def user(self, employer):
         return User(name="Tom", employer=employer, age=28)
 
-    def test_nesting_schema_within_itself(self, user, employer):
-        class SelfSchema(Schema):
-            name = fields.String()
-            age = fields.Integer()
-            employer = fields.Nested("self", exclude=("employer",))
+    def test_nesting_schema_by_passing_lambda(self, user, employer):
+        class SelfReferencingSchema(Schema):
+            name = fields.Str()
+            age = fields.Int()
+            employer = fields.Nested(
+                lambda: SelfReferencingSchema(exclude=("employer",))
+            )
 
-        data = SelfSchema().dump(user)
+        data = SelfReferencingSchema().dump(user)
         assert data["name"] == user.name
         assert data["employer"]["name"] == employer.name
         assert data["employer"]["age"] == employer.age
@@ -2185,9 +2234,24 @@ class TestSelfReference:
         assert data["employer"]["name"] == employer.name
         assert data["employer"]["age"] == employer.age
 
+    def test_nesting_schema_self_string(self, user, employer):
+        with pytest.warns(
+            DeprecationWarning, match="Passing 'self' to `Nested` is deprecated"
+        ):
+
+            class SelfSchema(Schema):
+                name = fields.String()
+                age = fields.Integer()
+                employer = fields.Nested("self", exclude=("employer",))
+
+        data = SelfSchema().dump(user)
+        assert data["name"] == user.name
+        assert data["employer"]["name"] == employer.name
+        assert data["employer"]["age"] == employer.age
+
     def test_nesting_within_itself_meta(self, user, employer):
         class SelfSchema(Schema):
-            employer = fields.Nested("self", exclude=("employer",))
+            employer = fields.Nested(lambda: SelfSchema(exclude=("employer",)))
 
             class Meta:
                 additional = ("name", "age")
@@ -2200,7 +2264,7 @@ class TestSelfReference:
 
     def test_nested_self_with_only_param(self, user, employer):
         class SelfSchema(Schema):
-            employer = fields.Nested("self", only=("name",))
+            employer = fields.Nested(lambda: SelfSchema(only=("name",)))
 
             class Meta:
                 fields = ("name", "employer")
@@ -2210,10 +2274,14 @@ class TestSelfReference:
         assert data["employer"]["name"] == employer.name
         assert "age" not in data["employer"]
 
-    def test_multiple_pluck_self_field(self, user):
+    def test_multiple_pluck_self_lambda(self, user):
         class MultipleSelfSchema(Schema):
-            emp = fields.Pluck("self", "name", attribute="employer")
-            rels = fields.Pluck("self", "name", many=True, attribute="relatives")
+            emp = fields.Pluck(
+                lambda: MultipleSelfSchema(), "name", attribute="employer"
+            )
+            rels = fields.Pluck(
+                lambda: MultipleSelfSchema(), "name", many=True, attribute="relatives"
+            )
 
             class Meta:
                 fields = ("name", "emp", "rels")
@@ -2225,9 +2293,28 @@ class TestSelfReference:
         relative = data["rels"][0]
         assert relative == user.relatives[0].name
 
-    def test_nested_self_many(self):
+    def test_multiple_pluck_self_string(self, user):
+        with pytest.warns(
+            DeprecationWarning, match="Passing 'self' to `Nested` is deprecated"
+        ):
+
+            class MultipleSelfSchema(Schema):
+                emp = fields.Pluck("self", "name", attribute="employer")
+                rels = fields.Pluck("self", "name", many=True, attribute="relatives")
+
+                class Meta:
+                    fields = ("name", "emp", "rels")
+
+        schema = MultipleSelfSchema()
+        user.relatives = [User(name="Bar", age=12), User(name="Baz", age=34)]
+        data = schema.dump(user)
+        assert len(data["rels"]) == len(user.relatives)
+        relative = data["rels"][0]
+        assert relative == user.relatives[0].name
+
+    def test_nested_self_many_lambda(self):
         class SelfManySchema(Schema):
-            relatives = fields.Nested("self", many=True)
+            relatives = fields.Nested(lambda: SelfManySchema(), many=True)
 
             class Meta:
                 additional = ("name", "age")
@@ -2240,9 +2327,28 @@ class TestSelfReference:
         assert data["relatives"][0]["name"] == person.relatives[0].name
         assert data["relatives"][0]["age"] == person.relatives[0].age
 
+    def test_nested_self_many_string(self):
+        with pytest.warns(
+            DeprecationWarning, match="Passing 'self' to `Nested` is deprecated"
+        ):
+
+            class SelfManySchema(Schema):
+                relatives = fields.Nested("self", many=True)
+
+                class Meta:
+                    additional = ("name", "age")
+
+        person = User(name="Foo")
+        person.relatives = [User(name="Bar", age=12), User(name="Baz", age=34)]
+        data = SelfManySchema().dump(person)
+        assert data["name"] == person.name
+        assert len(data["relatives"]) == len(person.relatives)
+        assert data["relatives"][0]["name"] == person.relatives[0].name
+        assert data["relatives"][0]["age"] == person.relatives[0].age
+
     def test_nested_self_list(self):
         class SelfListSchema(Schema):
-            relatives = fields.List(fields.Nested("self"))
+            relatives = fields.List(fields.Nested(lambda: SelfListSchema()))
 
             class Meta:
                 additional = ("name", "age")
@@ -2255,6 +2361,25 @@ class TestSelfReference:
         assert data["relatives"][0]["name"] == person.relatives[0].name
         assert data["relatives"][0]["age"] == person.relatives[0].age
 
+    def test_nested_self_list_string(self):
+        with pytest.warns(
+            DeprecationWarning, match="Passing 'self' to `Nested` is deprecated"
+        ):
+
+            class SelfListSchema(Schema):
+                relatives = fields.List(fields.Nested("self"))
+
+                class Meta:
+                    additional = ("name", "age")
+
+        person = User(name="Foo")
+        person.relatives = [User(name="Bar", age=12), User(name="Baz", age=34)]
+        data = SelfListSchema().dump(person)
+        assert data["name"] == person.name
+        assert len(data["relatives"]) == len(person.relatives)
+        assert data["relatives"][0]["name"] == person.relatives[0].name
+        assert data["relatives"][0]["age"] == person.relatives[0].age
+
 
 class RequiredUserSchema(Schema):
     name = fields.Field(required=True)