Repository: pydantic/pydantic
Pull request: Added schema generation for Generic fields
<!-- Thank you for your contribution! -->
<!-- Unless your change is trivial, please create an issue to discuss the change before creating a PR -->
<!-- See https://pydantic-docs.helpmanual.io/contributing/ for help on Contributing -->

## Change Summary

Generating schema for generic fields hasn't been implemented. Now it should work.

## Related issue number

Fixes #1578. 

## Checklist

* [x] Unit tests for the changes exist
* [x] Tests pass on CI and coverage remains at 100%
* [x] Documentation reflects the changes where applicable
* [x] `changes/<pull request or issue id>-<github username>.md` file added describing change
  (see [changes/README.md](https://github.com/samuelcolvin/pydantic/blob/master/changes/README.md) for details)


Problem statement:
BaseModel.schema() fails when using generic types
# Bug
## Version
Output of `python -c "import pydantic.utils; print(pydantic.utils.version_info())"`:
```
  pydantic version: 1.5.1
            pydantic compiled: True
                 install path: C:\Users\adrian\miniconda3\envs\sangl-butler\Lib\site-packages\pydantic
               python version: 3.8.1 (default, Mar  2 2020, 13:06:26) [MSC v.1916 64 bit (AMD64)]
                     platform: Windows-10-10.0.18362-SP0
     optional deps. installed: ['typing-extensions', 'email-validator']

```
## Description
Hi there!

I want to write a generic type, that behaves like the type it extends, but injects fields into the model with ``__modify_schema__``. So my approach was to use a generic class to use it like that:

```py
from pydantic import BaseModel

class MyModel(BaseModel):
   my_field: FieldThatModifiesModel[str] = "Default value"

````
The field should completely behave like the type var.

However, when I use a generic type in my model, I can't retrieve the model anymore.
See the code below (taken from the docs):
## Example
```py
from pydantic import BaseModel, ValidationError
from pydantic.fields import ModelField
from typing import TypeVar, Generic

AgedType = TypeVar('AgedType')
QualityType = TypeVar('QualityType')

# This is not a pydantic model, it's an arbitrary generic class
class TastingModel(Generic[AgedType, QualityType]):
    def __init__(self, name: str, aged: AgedType, quality: QualityType):
        self.name = name
        self.aged = aged
        self.quality = quality

    @classmethod
    def __get_validators__(cls):
        yield cls.validate

    @classmethod
    # You don't need to add the "ModelField", but it will help your
    # editor give you completion and catch errors
    def validate(cls, v, field: ModelField):
        if not isinstance(v, cls):
            # The value is not even a TastingModel
            raise TypeError('Invalid value')
        if not field.sub_fields:
            # Generic parameters were not provided so we don't try to validate
            # them and just return the value as is
            return v
        aged_f = field.sub_fields[0]
        quality_f = field.sub_fields[1]
        errors = []
        # Here we don't need the validated value, but we want the errors
        valid_value, error = aged_f.validate(v.aged, {}, loc='aged')
        if error:
            errors.append(error)
        # Here we don't need the validated value, but we want the errors
        valid_value, error = quality_f.validate(v.quality, {}, loc='quality')
        if error:
            errors.append(error)
        if errors:
            raise ValidationError(errors, cls)
        # Validation passed without errors, return the same instance received
        return v

class Model(BaseModel):
    # for wine, "aged" is an int with years, "quality" is a float
    wine: TastingModel[int, float]
    # for cheese, "aged" is a bool, "quality" is a str
    cheese: TastingModel[bool, str]
    # for thing, "aged" is a Any, "quality" is Any
    thing: TastingModel

print(Model.schema())
"""
Traceback (most recent call last):
  File ".../generic_type.py", line 54, in <module>
    print(Model.schema())
  File "pydantic\main.py", line 556, in pydantic.main.BaseModel.schema
  File "pydantic\schema.py", line 132, in pydantic.schema.model_schema
  File "pydantic\schema.py", line 455, in pydantic.schema.model_process_schema
  File "pydantic\schema.py", line 491, in pydantic.schema.model_type_schema
  File "pydantic\schema.py", line 185, in pydantic.schema.field_schema
  File "pydantic\schema.py", line 411, in pydantic.schema.field_type_schema
AssertionError: 10
"""
```

Hints from issue discussion:
Looks to me like generating schema for generic fields just hasn't been implemented yet.

Happy to review and accept a PR to implement this.
I've done some poking around and reduced this down to some test cases.

These all succeed:

```python
    def test_with_generic(self):
        class Model(BaseModel, Generic[T]):
            """No generic types, only generic methods."""

            key: str

            def method(self) -> T:
                ...

        Model.schema()
        Model[str].schema()
        Model[Model].schema()

    def test_with_generic_model(self):
        class Model(GenericModel, Generic[T]):
            key: T

        Model.schema()
        Model[str].schema()
        Model[Model[str]].schema()

    def test_with_nested_generic(self):
        class Ref(BaseModel, Generic[T]):
            uuid: str

            def resolve(self) -> T:
                ...

        class Model(BaseModel):
            ref: Ref

        Model.schema()
```

This is the simplest case that fails (note this is not a GenericModel, it does not have generic types, only generic methods). It appears the problem is when you reference a specific implementation of a generic type (that isn't List, Dict, Union, etc.).

```python
    def test_with_nested_generic(self):
        class Ref(BaseModel, Generic[T]):
            uuid: str

            def resolve(self) -> T:
                ...

        class Model(BaseModel):
            ref: Ref[str]

        Model.schema()
```

A more realistic case (also fails):

```python
    def test_with_nested_generic_unions(self):
        class Ref(BaseModel, Generic[T]):
            uuid: str

            def resolve(self) -> T:
                ...

        class Child(BaseModel):
            uuid: str

            def resolve(self) -> "Child":
                return self

        class Model(BaseModel):
            refs: Union[Ref[Child], Child]

        Model.schema()
```

Commit message: remove redundant len

Patch to review:
diff --git a/changes/2262-maximberg.md b/changes/2262-maximberg.md
new file mode 100644
index 00000000000..0911c71d0a4
--- /dev/null
+++ b/changes/2262-maximberg.md
@@ -0,0 +1 @@
+Support generating schema for Generic fields. 
\ No newline at end of file
diff --git a/pydantic/schema.py b/pydantic/schema.py
index cf4dc4c7b88..2b5fe8f1e83 100644
--- a/pydantic/schema.py
+++ b/pydantic/schema.py
@@ -11,6 +11,7 @@
     Callable,
     Dict,
     FrozenSet,
+    Generic,
     Iterable,
     List,
     Optional,
@@ -27,6 +28,7 @@
 
 from .fields import (
     SHAPE_FROZENSET,
+    SHAPE_GENERIC,
     SHAPE_ITERABLE,
     SHAPE_LIST,
     SHAPE_MAPPING,
@@ -481,7 +483,7 @@ def field_type_schema(
             sub_schema = sub_schema[0]  # type: ignore
         f_schema = {'type': 'array', 'items': sub_schema}
     else:
-        assert field.shape == SHAPE_SINGLETON, field.shape
+        assert field.shape in {SHAPE_SINGLETON, SHAPE_GENERIC}, field.shape
         f_schema, f_definitions, f_nested_models = field_singleton_schema(
             field,
             by_alias=by_alias,
@@ -496,7 +498,10 @@ def field_type_schema(
 
     # check field type to avoid repeated calls to the same __modify_schema__ method
     if field.type_ != field.outer_type_:
-        modify_schema = getattr(field.outer_type_, '__modify_schema__', None)
+        if field.shape == SHAPE_GENERIC:
+            modify_schema = getattr(field.type_, '__modify_schema__', None)
+        else:
+            modify_schema = getattr(field.outer_type_, '__modify_schema__', None)
         if modify_schema:
             modify_schema(f_schema)
     return f_schema, definitions, nested_models
@@ -828,6 +833,11 @@ def field_singleton_schema(  # noqa: C901 (ignore complexity)
         schema_ref = get_schema_ref(model_name, ref_prefix, ref_template, schema_overrides)
         return schema_ref, definitions, nested_models
 
+    # For generics with no args
+    args = get_args(field_type)
+    if args is not None and not args and Generic in field_type.__bases__:
+        return f_schema, definitions, nested_models
+
     raise ValueError(f'Value not declarable with JSON Schema, field: {field}')
 
 
diff --git a/pydantic/typing.py b/pydantic/typing.py
index 8f8cbac927c..241c3bf36ba 100644
--- a/pydantic/typing.py
+++ b/pydantic/typing.py
@@ -92,7 +92,7 @@ def evaluate_forwardref(type_: ForwardRef, globalns: Any, localns: Any) -> Any:
     if sys.version_info < (3, 7):
 
         def get_args(t: Type[Any]) -> Tuple[Any, ...]:
-            """Simplest get_args compatability layer possible.
+            """Simplest get_args compatibility layer possible.
 
             The Python 3.6 typing module does not have `_GenericAlias` so
             this won't work for everything. In particular this will not
@@ -106,7 +106,7 @@ def get_args(t: Type[Any]) -> Tuple[Any, ...]:
         from typing import _GenericAlias
 
         def get_args(t: Type[Any]) -> Tuple[Any, ...]:
-            """Compatability version of get_args for python 3.7.
+            """Compatibility version of get_args for python 3.7.
 
             Mostly compatible with the python 3.8 `typing` module version
             and able to handle almost all use cases.
diff --git a/tests/test_schema.py b/tests/test_schema.py
index 3566716233e..f4f4b5b5d31 100644
--- a/tests/test_schema.py
+++ b/tests/test_schema.py
@@ -7,7 +7,21 @@
 from enum import Enum, IntEnum
 from ipaddress import IPv4Address, IPv4Interface, IPv4Network, IPv6Address, IPv6Interface, IPv6Network
 from pathlib import Path
-from typing import Any, Callable, Dict, FrozenSet, Iterable, List, NewType, Optional, Set, Tuple, Union
+from typing import (
+    Any,
+    Callable,
+    Dict,
+    FrozenSet,
+    Generic,
+    Iterable,
+    List,
+    NewType,
+    Optional,
+    Set,
+    Tuple,
+    TypeVar,
+    Union,
+)
 from uuid import UUID
 
 import pytest
@@ -2149,3 +2163,58 @@ class Model(BaseModel):
         f'{module_2.__name__}__MyEnum',
         f'{module_2.__name__}__MyModel',
     }
+
+
+@pytest.mark.skipif(
+    sys.version_info < (3, 7), reason='schema generation for generic fields is not available in python < 3.7'
+)
+def test_schema_for_generic_field():
+    T = TypeVar('T')
+
+    class GenModel(Generic[T]):
+        def __init__(self, data: Any):
+            self.data = data
+
+        @classmethod
+        def __get_validators__(cls):
+            yield cls.validate
+
+        @classmethod
+        def validate(cls, v: Any):
+            return v
+
+    class Model(BaseModel):
+        data: GenModel[str]
+        data1: GenModel
+
+    assert Model.schema() == {
+        'title': 'Model',
+        'type': 'object',
+        'properties': {
+            'data': {'title': 'Data', 'type': 'string'},
+            'data1': {
+                'title': 'Data1',
+            },
+        },
+        'required': ['data', 'data1'],
+    }
+
+    class GenModelModified(GenModel, Generic[T]):
+        @classmethod
+        def __modify_schema__(cls, field_schema):
+            field_schema.pop('type', None)
+            field_schema.update(anyOf=[{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}])
+
+    class ModelModified(BaseModel):
+        data: GenModelModified[str]
+        data1: GenModelModified
+
+    assert ModelModified.schema() == {
+        'title': 'ModelModified',
+        'type': 'object',
+        'properties': {
+            'data': {'title': 'Data', 'anyOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]},
+            'data1': {'title': 'Data1', 'anyOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]},
+        },
+        'required': ['data', 'data1'],
+    }


Code review comments:
diff_hunk: @@ -496,7 +498,10 @@ def field_type_schema(
 
     # check field type to avoid repeated calls to the same __modify_schema__ method
     if field.type_ != field.outer_type_:
-        modify_schema = getattr(field.outer_type_, '__modify_schema__', None)
+        if field.shape == SHAPE_GENERIC:
+            modify_schema = getattr(field.type_, '__modify_schema__', None)
+        else:
+            modify_schema = getattr(field.outer_type_, '__modify_schema__', None)
path: pydantic/schema.py
text: @user1:
suggestion for DRYness

```suggestion
        if field.shape == SHAPE_GENERIC:
            field_type = field.type_
        else:
            field_type = field.outer_type_
        modify_schema = getattr(field_type, '__modify_schema__', None)
```