Repository: marshmallow-code/marshmallow

Issue:
Add a way to generate a Schema from a dictionary
Generating schemas at runtime is a fairly common use case that I've run into a handful of times at this point ([webargs](https://github.com/marshmallow-code/webargs/blob/de061e037285fd08a42d73be95bc779f2a4e3c47/src/webargs/core.py#L50-L63), [environs](https://github.com/sloria/environs/blob/91b32a0e4bdf40281fab3978368164b27ce65e39/environs.py#L83-L95) , form builders...). 

It might make sense to incorporate this into marshmallow core.

## Proposed API

```python
>>> MyDynamicSchema = Schema.from_dict({"foo": fields.Str()})
>>> MyDynamicSchema
<class 'marshmallow.schema.GeneratedSchema'>
```

```python
class AlbumSchema(Schema):
     artist = fields.Nested(Schema.from_dict({"name": fields.Str()})
```

Discussion:
sloria: I wanted to keep the API surface to a minimum here. If a user needs to customize `Meta`, they can declare a class.

Patch:
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 1784bcf0..86586ec4 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -8,6 +8,7 @@ Features:
 
 - Optimize ``List(Nested(...))`` (:issue:`779`).
 - Minor performance improvements and cleanup (:pr:`1328`).
+- Add ``Schema.from_dict`` (:issue:`1312`).
 
 Deprecations/Removals:
 
diff --git a/docs/quickstart.rst b/docs/quickstart.rst
index 43ff596e..5bd26d68 100644
--- a/docs/quickstart.rst
+++ b/docs/quickstart.rst
@@ -41,6 +41,20 @@ Create a schema by defining a class with variables mapping attribute names to :c
 
     For a full reference on the available field classes, see the :ref:`API Docs <api_fields>`.
 
+Creating Schemas From Dictionaries
+----------------------------------
+
+You can create a schema from a dictionary of fields using the `from_dict <marshmallow.Schema.from_dict>` method.
+
+.. code-block:: python
+
+    from marshmallow import Schema, fields
+
+    UserSchema = Schema.from_dict(
+        {"name": fields.Str(), "email": fields.Email(), "created_at": fields.DateTime()}
+    )
+
+`from_dict <marshmallow.Schema.from_dict>` is especially useful for generating schemas at runtime.
 
 Serializing Objects ("Dumping")
 -------------------------------
@@ -96,8 +110,8 @@ with a dictionary of validation errors, which we'll :ref:`revisit later <validat
 
     user_data = {
         "created_at": "2014-08-11T05:26:03.869245",
-        "email": u"ken@yahoo.com",
-        "name": u"Ken",
+        "email": "ken@yahoo.com",
+        "name": "Ken",
     }
     schema = UserSchema()
     result = schema.load(user_data)
diff --git a/src/marshmallow/schema.py b/src/marshmallow/schema.py
index fa1dd5a6..520ffb44 100644
--- a/src/marshmallow/schema.py
+++ b/src/marshmallow/schema.py
@@ -8,6 +8,7 @@ import decimal
 import copy
 import inspect
 import json
+import typing
 import warnings
 
 from marshmallow import base, fields as ma_fields, class_registry
@@ -409,6 +410,35 @@ class BaseSchema(base.SchemaABC):
     def set_class(self):
         return OrderedSet if self.ordered else set
 
+    @classmethod
+    def from_dict(
+        cls, fields: typing.Dict[str, ma_fields.Field], *, name: str = "GeneratedSchema"
+    ) -> typing.Type["Schema"]:
+        """Generate a `Schema` class given a dictionary of fields.
+
+        .. code-block:: python
+
+            from marshmallow import Schema, fields
+
+            PersonSchema = Schema.from_dict({"name": fields.Str()})
+            print(PersonSchema().load({"name": "David"}))  # => {'name': 'David'}
+
+        Generated schemas are not added to the class registry and therefore cannot
+        be referred to by name in `Nested` fields.
+
+        :param dict fields: Dictionary mapping field names to field instances.
+        :param str name: Optional name for the class, which will appear in
+            the ``repr`` for the class.
+
+        .. versionadded:: 3.0.0
+        """
+        attrs = fields.copy()
+        attrs["Meta"] = type(
+            "GeneratedMeta", (getattr(cls, "Meta", object),), {"register": False}
+        )
+        schema_cls = type(name, (cls,), attrs)
+        return schema_cls
+
     ##### Override-able methods #####
 
     def handle_error(self, error, data, *, many, **kwargs):


Test patch:
diff --git a/tests/test_schema.py b/tests/test_schema.py
index dde4d8ef..839c70e3 100644
--- a/tests/test_schema.py
+++ b/tests/test_schema.py
@@ -19,8 +19,13 @@ from marshmallow import (
     EXCLUDE,
     INCLUDE,
     RAISE,
+    class_registry,
+)
+from marshmallow.exceptions import (
+    ValidationError,
+    StringNotCollectionError,
+    RegistryError,
 )
-from marshmallow.exceptions import ValidationError, StringNotCollectionError
 
 from tests.base import (
     UserSchema,
@@ -2644,3 +2649,38 @@ class TestLoadOnly:
         data_with_no_top_level_domain = {"url": "marshmallow://app/discounts"}
         result = schema.load(data_with_no_top_level_domain)
         assert result == data_with_no_top_level_domain
+
+
+class TestFromDict:
+    def test_generates_schema(self):
+        MySchema = Schema.from_dict({"foo": fields.Str()})
+        assert issubclass(MySchema, Schema)
+
+    def test_name(self):
+        MySchema = Schema.from_dict({"foo": fields.Str()})
+        assert "GeneratedSchema" in repr(MySchema)
+        SchemaWithName = Schema.from_dict(
+            {"foo": fields.Int()}, name="MyGeneratedSchema"
+        )
+        assert "MyGeneratedSchema" in repr(SchemaWithName)
+
+    def test_generated_schemas_are_not_registered(self):
+        n_registry_entries = len(class_registry._registry)
+        Schema.from_dict({"foo": fields.Str()})
+        Schema.from_dict({"bar": fields.Str()}, name="MyGeneratedSchema")
+        assert len(class_registry._registry) == n_registry_entries
+        with pytest.raises(RegistryError):
+            class_registry.get_class("GeneratedSchema")
+        with pytest.raises(RegistryError):
+            class_registry.get_class("MyGeneratedSchema")
+
+    def test_meta_options_are_applied(self):
+        class OrderedSchema(Schema):
+            class Meta:
+                ordered = True
+                load_only = ("bar",)
+
+        OSchema = OrderedSchema.from_dict({"foo": fields.Int(), "bar": fields.Int()})
+        dumped = OSchema().dump({"foo": 42, "bar": 24})
+        assert isinstance(dumped, OrderedDict)
+        assert "bar" not in dumped