Repository: pydantic/pydantic
Pull request: Improve error message for improper `RootModel` subclasses
Fix https://github.com/pydantic/pydantic/issues/8712

Improve error message in the case where `RootModel` isn't parametrized properly with generics.

Alternative to https://github.com/pydantic/pydantic/pull/8804.

Selected Reviewer: @samuelcolvin

Problem statement:
Weird error message on creating a generic RootModel
### Initial Checks

- [X] I confirm that I'm using Pydantic V2

### Description

When creating a generic `RootModel` without providing the generic to the model in the class inheritance `RootModelRootType` is mentioned in the error message, which is somewhat misleading.

I would expect `RootModelRootType` to be stripped from suggested types and maybe if possible to mention the needed parametrization of the `RootModel` itself

### Example Code

```Python
from typing import Generic, TypeVar
from pydantic import BaseModel, RootModel

T_ = TypeVar("T_", bound=BaseModel)


class GenericRootModel(RootModel, Generic[T_]):
    root: T_ | int
```


### Python, Pydantic & OS Version

```Text
pydantic version: 2.6.0
        pydantic-core version: 2.16.1
          pydantic-core build: profile=release pgo=true
                 install path: $HOME/pyjcsidm/.venv/lib/python3.11/site-packages/pydantic
               python version: 3.11.6 (main, Oct  8 2023, 05:06:43) [GCC 13.2.0]
                     platform: Linux-6.2.0-26-generic-x86_64-with-glibc2.38
             related packages: mypy-1.8.0 email-validator-2.1.0.post1 typing_extensions-4.9.0 pydantic-settings-2.1.0
                       commit: unknown
```

Hints from issue discussion:
@JensHeinrich,

Thanks for pointing this out. PRs welcome to improve the error message here in the case of subclassing from a non-parametrized `RootModel` class.
Hii @sydney-runkle I'm very interested to work on this! What you suggest? I was thinking into introducing a condition to check if the missing parameter issue involves RootModel. If so, tailor the error message to suggest the correct form of parameterization. (Talking about the `_model_construction.py`  file)
@Massakera,

That sounds great! Feel free to open a PR and ping me when you'd like a review :).
@sydney-runkle 

```python
            else:
                parent_parameters = getattr(cls, '__pydantic_generic_metadata__', {}).get('parameters', ())
                parameters = getattr(cls, '__parameters__', None) or parent_parameters
                if parameters and parent_parameters and not all(x in parameters for x in parent_parameters):
                    combined_parameters = parent_parameters + tuple(x for x in parameters if x not in parent_parameters)
                    parameters_str = ', '.join([str(x) for x in combined_parameters])
                    generic_type_label = f'typing.Generic[{parameters_str}]'
                    if issubclass(cls, RootModel) and not parameters:
                        # Custom error message for RootModel subclassing issue
                        error_message = (
                            f'{cls_name} is subclassing RootModel without generic parameters. '
                            f'You should ensure that RootModel is correctly parameterized, '
                            f'e.g., `class MyGenericRootModel(RootModel[T]): ...`, '
                            f'where `T` is a TypeVar bound to a Pydantic model.'
                        )
                    else:
                        error_message = (
                            f'All parameters must be present on typing.Generic;'
                            f' you should inherit from {generic_type_label}.'
                        )
```

This is what I thought to do but on line 164 of `_model_construction.py` but I'm receiving those errors and I'm having a though time to understand. Can you help me?

Errors: 

```bash
  /home/massaki/Projects/pydantic/pydantic/_internal/_model_construction.py:188:21 - error: Cannot assign member "__pydantic_generic_metadata__" for type "type[RootModel[Unknown]]"
    Expression of type "dict[str, Unknown]" cannot be assigned to member "__pydantic_generic_metadata__" of class "RootModel[Unknown]"
      Member "__set__" is unknown
      "dict[str, Unknown]" is incompatible with "PydanticGenericMetadata" (reportGeneralTypeIssues)
  /home/massaki/Projects/pydantic/pydantic/_internal/_model_construction.py:188:21 - error: Cannot assign member "__pydantic_generic_metadata__" for type "type[BaseModel]"
    Expression of type "dict[str, Unknown]" cannot be assigned to member "__pydantic_generic_metadata__" of class "BaseModel"
      Member "__set__" is unknown
      "dict[str, Unknown]" is incompatible with "PydanticGenericMetadata" (reportGeneralTypeIssues)
2 errors, 0 warnings, 0 informations
```

@Massakera,

Huh, I'm not sure off the top of my head. Go ahead and open a PR with your changes and I'll take a look in more detail!
Here it is @sydney-runkle : #8804

Commit message: minor test fix

Patch to review:
diff --git a/pydantic/_internal/_model_construction.py b/pydantic/_internal/_model_construction.py
index e41d74ab4bd..bfa8ee88add 100644
--- a/pydantic/_internal/_model_construction.py
+++ b/pydantic/_internal/_model_construction.py
@@ -137,22 +137,34 @@ def wrapped_model_post_init(self: BaseModel, __context: Any) -> None:
                 parent_parameters = getattr(cls, '__pydantic_generic_metadata__', {}).get('parameters', ())
                 parameters = getattr(cls, '__parameters__', None) or parent_parameters
                 if parameters and parent_parameters and not all(x in parameters for x in parent_parameters):
-                    combined_parameters = parent_parameters + tuple(x for x in parameters if x not in parent_parameters)
-                    parameters_str = ', '.join([str(x) for x in combined_parameters])
-                    generic_type_label = f'typing.Generic[{parameters_str}]'
-                    error_message = (
-                        f'All parameters must be present on typing.Generic;'
-                        f' you should inherit from {generic_type_label}.'
-                    )
-                    if Generic not in bases:  # pragma: no cover
-                        # We raise an error here not because it is desirable, but because some cases are mishandled.
-                        # It would be nice to remove this error and still have things behave as expected, it's just
-                        # challenging because we are using a custom `__class_getitem__` to parametrize generic models,
-                        # and not returning a typing._GenericAlias from it.
-                        bases_str = ', '.join([x.__name__ for x in bases] + [generic_type_label])
-                        error_message += (
-                            f' Note: `typing.Generic` must go last: `class {cls.__name__}({bases_str}): ...`)'
+                    missing_parameters = tuple(x for x in parameters if x not in parent_parameters)
+                    if 'RootModelRootType' in tuple(
+                        x.__name__ for x in parent_parameters
+                    ) and 'RootModelRootType' not in tuple(x.__name__ for x in parameters):
+                        parameters_str = ', '.join([x.__name__ for x in missing_parameters])
+                        error_message = (
+                            f'{cls.__name__} is a subclass of `RootModel`, but does not include the generic type identifier(s) '
+                            f'{parameters_str} in its parameters. '
+                            f'You should ensure that RootModel is correctly parameterized, '
+                            f'e.g., `class {cls.__name__}(RootModel[{parameters_str}]): ...`.'
                         )
+                    else:
+                        combined_parameters = parent_parameters + missing_parameters
+                        parameters_str = ', '.join([str(x) for x in combined_parameters])
+                        generic_type_label = f'typing.Generic[{parameters_str}]'
+                        error_message = (
+                            f'All parameters must be present on typing.Generic;'
+                            f' you should inherit from {generic_type_label}.'
+                        )
+                        if Generic not in bases:  # pragma: no cover
+                            # We raise an error here not because it is desirable, but because some cases are mishandled.
+                            # It would be nice to remove this error and still have things behave as expected, it's just
+                            # challenging because we are using a custom `__class_getitem__` to parametrize generic models,
+                            # and not returning a typing._GenericAlias from it.
+                            bases_str = ', '.join([x.__name__ for x in bases] + [generic_type_label])
+                            error_message += (
+                                f' Note: `typing.Generic` must go last: `class {cls.__name__}({bases_str}): ...`)'
+                            )
                     raise TypeError(error_message)
 
                 cls.__pydantic_generic_metadata__ = {
diff --git a/tests/test_root_model.py b/tests/test_root_model.py
index d25a3706a80..fe3a397af2b 100644
--- a/tests/test_root_model.py
+++ b/tests/test_root_model.py
@@ -1,11 +1,11 @@
 import pickle
 from datetime import date, datetime
-from typing import Any, Dict, List, Optional, Union
+from typing import Any, Dict, Generic, List, Optional, Union
 
 import pytest
 from pydantic_core import CoreSchema
 from pydantic_core.core_schema import SerializerFunctionWrapHandler
-from typing_extensions import Annotated, Literal
+from typing_extensions import Annotated, Literal, TypeVar
 
 from pydantic import (
     Base64Str,
@@ -648,3 +648,12 @@ def test_model_validate_strings(root_type, input_value, expected, raises_match,
             Model.model_validate_strings(input_value, strict=strict)
     else:
         assert Model.model_validate_strings(input_value, strict=strict).root == expected
+
+
+def test_model_construction_with_invalid_generic_specification() -> None:
+    T_ = TypeVar('T_', bound=BaseModel)
+
+    with pytest.raises(TypeError, match='You should ensure that RootModel is correctly parameterized'):
+
+        class GenericRootModel(RootModel, Generic[T_]):
+            root: Union[T_, int]


Code review comments:
diff_hunk: @@ -137,22 +137,34 @@ def wrapped_model_post_init(self: BaseModel, __context: Any) -> None:
                 parent_parameters = getattr(cls, '__pydantic_generic_metadata__', {}).get('parameters', ())
                 parameters = getattr(cls, '__parameters__', None) or parent_parameters
                 if parameters and parent_parameters and not all(x in parameters for x in parent_parameters):
-                    combined_parameters = parent_parameters + tuple(x for x in parameters if x not in parent_parameters)
-                    parameters_str = ', '.join([str(x) for x in combined_parameters])
-                    generic_type_label = f'typing.Generic[{parameters_str}]'
-                    error_message = (
-                        f'All parameters must be present on typing.Generic;'
-                        f' you should inherit from {generic_type_label}.'
-                    )
-                    if Generic not in bases:  # pragma: no cover
-                        # We raise an error here not because it is desirable, but because some cases are mishandled.
-                        # It would be nice to remove this error and still have things behave as expected, it's just
-                        # challenging because we are using a custom `__class_getitem__` to parametrize generic models,
-                        # and not returning a typing._GenericAlias from it.
-                        bases_str = ', '.join([x.__name__ for x in bases] + [generic_type_label])
-                        error_message += (
-                            f' Note: `typing.Generic` must go last: `class {cls.__name__}({bases_str}): ...`)'
+                    missing_parameters = tuple(x for x in parameters if x not in parent_parameters)
+                    if 'RootModelRootType' in tuple(
+                        x.__name__ for x in parent_parameters
+                    ) and 'RootModelRootType' not in tuple(x.__name__ for x in parameters):
path: pydantic/_internal/_model_construction.py
text: @user1:
Couldn't you do something like?
```suggestion
                    if (
                        RootModelRootType in parent_parameters
                        and RootModelRootType not in parameters
                    ):
```
where `RootModelRootType` is `pydantic.root_model.RootModelRootType`?

@user1:
Also, I think it would be good to add a comment describing what this condition is checking. I know the error message comes up below, but I feel like it could use a clearer plaintext description of what this condition indicates. Like, "Some parameters were replaced, but not the RootModelRootType, which means... "  ... ? (I'm not sure what it means lol, but I'm guessing you know?)