Repository: pydantic/pydantic

Issue:
Support custom naming for GenericModel subclasses
# Feature Request

Right now, it is not easy to use a custom naming scheme for a generic models. But I think this could be useful in some cases.

------

I recently faced this problem when I realized that the generated schema for `List[Optional[T]]` types doesn't actually mark the value type as optional (strangely/sadly I think there isn't currently a well-supported way to do this under OpenAPI).

My workaround was to create a wrapper class for optionals:
```python
class OptionalWrapper(GenericModel, Generic[T]):
    value: Optional[T]
```
But I wanted to override the `cls.__name__` since that is used by FastAPI when generating the openapi spec. (For example, I wanted to see `OptionalInt` instead of `OptionalWrapper[int]`.)

Given how small a code change this requires, I think it should be reasonable, but I figured I'd create an issue for discussion just in case.


Comments:

Patch:
diff --git a/docs/examples/generics-naming.py b/docs/examples/generics-naming.py
new file mode 100644
--- /dev/null
+++ b/docs/examples/generics-naming.py
@@ -0,0 +1,17 @@
+from typing import Generic, TypeVar, Type, Any, Tuple
+
+from pydantic.generics import GenericModel
+
+DataT = TypeVar('DataT')
+
+class Response(GenericModel, Generic[DataT]):
+    data: DataT
+
+    @classmethod
+    def __concrete_name__(cls: Type[Any], params: Tuple[Type[Any], ...]) -> str:
+        return f'{params[0].__name__.title()}Response'
+
+print(Response[int](data=1))
+# IntResponse data=1
+print(Response[str](data='a'))
+# StrResponse data='a'
diff --git a/pydantic/generics.py b/pydantic/generics.py
--- a/pydantic/generics.py
+++ b/pydantic/generics.py
@@ -40,7 +40,7 @@ def __class_getitem__(  # type: ignore
             k: resolve_type_hint(v, typevars_map) for k, v in instance_type_hints.items()
         }
 
-        model_name = concrete_name(cls, params)
+        model_name = cls.__concrete_name__(params)
         validators = gather_all_validators(cls)
         fields: Dict[str, Tuple[Type[Any], Any]] = {
             k: (v, cls.__fields__[k].field_info) for k, v in concrete_type_hints.items() if k in cls.__fields__
@@ -60,11 +60,14 @@ def __class_getitem__(  # type: ignore
             _generic_types_cache[(cls, params[0])] = created_model
         return created_model
 
-
-def concrete_name(cls: Type[Any], params: Tuple[Type[Any], ...]) -> str:
-    param_names = [param.__name__ if hasattr(param, '__name__') else str(param) for param in params]
-    params_component = ', '.join(param_names)
-    return f'{cls.__name__}[{params_component}]'
+    @classmethod
+    def __concrete_name__(cls: Type[Any], params: Tuple[Type[Any], ...]) -> str:
+        """
+        This method can be overridden to achieve a custom naming scheme for GenericModels
+        """
+        param_names = [param.__name__ if hasattr(param, '__name__') else str(param) for param in params]
+        params_component = ', '.join(param_names)
+        return f'{cls.__name__}[{params_component}]'
 
 
 def resolve_type_hint(type_: Any, typevars_map: Dict[Any, Any]) -> Type[Any]:


Test patch:
diff --git a/tests/test_generics.py b/tests/test_generics.py
--- a/tests/test_generics.py
+++ b/tests/test_generics.py
@@ -1,6 +1,6 @@
 import sys
 from enum import Enum
-from typing import Any, ClassVar, Dict, Generic, List, Optional, TypeVar, Union
+from typing import Any, ClassVar, Dict, Generic, List, Optional, Tuple, Type, TypeVar, Union
 
 import pytest
 
@@ -421,3 +421,20 @@ class MyModel(GenericModel, Generic[T]):
 
     schema = MyModel[int].schema()
     assert schema['properties']['a'].get('description') == 'Custom'
+
+
+@skip_36
+def test_custom_generic_naming():
+    T = TypeVar('T')
+
+    class MyModel(GenericModel, Generic[T]):
+        value: Optional[T]
+
+        @classmethod
+        def __concrete_name__(cls: Type[Any], params: Tuple[Type[Any], ...]) -> str:
+            param_names = [param.__name__ if hasattr(param, '__name__') else str(param) for param in params]
+            title = param_names[0].title()
+            return f'Optional{title}Wrapper'
+
+    assert str(MyModel[int](value=1)) == 'OptionalIntWrapper value=1'
+    assert str(MyModel[str](value=None)) == 'OptionalStrWrapper value=None'