Repository: pydantic/pydantic

Issue:
Instances of generic types with bound=BaseModel are not serialised
### Initial Checks

- [X] I confirm that I'm using Pydantic V2

### Description

I have an `Error` class that optionally includes `details`, which should be a subclass of Pydantic's BaseModel. My aim is to specify the type of these details. Here's how I've implemented it:

````python
ErrorDataT = TypeVar("ErrorDataT", bound=BaseModel)

class Error(BaseModel, Generic[ErrorDataT]):
    message: str
    details: Optional[ErrorDataT]
````

With this, the `details` field is strongly typed and ensures that, if present, it's always an instance of Pydantic's BaseModel. However, when I call `error.model_dump()`, the `details` field serializes as an empty dictionary. Removing the bound constraint on `ErrorDataT` allows the model to serialize correctly, but at the expense of type validation, i.e., I can assign any value to `error.details`.

In the tests provided below, both should ideally pass. But currently, only one of them does based on the bound constraint:

* With the constraint:`test_can_not_use_non_base_models_as_error_details` passes, but`test_sample_error_is_correctly_serialized` fails.
* Without the constraint:`test_sample_error_is_correctly_serialized` passes, but`test_can_not_use_non_base_models_as_error_details` fails.

**NOTE:** This was working in v1 (using GenericModel). I apologize in advance if I didn't understand something while doing the migration from v1 to v2.

### Example Code

```Python
from pydantic import BaseModel, ValidationError
from typing import TypeVar, Generic, Optional
import pytest

ErrorDataT = TypeVar("ErrorDataT", bound=BaseModel)


class ErrorDetails(BaseModel):
    foo: str


class ErrorDetailsNonBaseModel:
    other: str

    def __init__(self, other) -> None:
        self.other = other


class Error(BaseModel, Generic[ErrorDataT]):
    message: str
    details: Optional[ErrorDataT]


sample_error = Error(
    message="We just had an error",
    details=ErrorDetails(foo="var"),
)


class TestApiErrorResponse:
    def test_sample_error_is_correctly_serialized(self):
        assert sample_error.model_dump() == {
            "message": "We just had an error",
            "details": {
                "foo": "var",
            },
        }

    def test_can_not_use_non_base_models_as_error_details(self):
        with pytest.raises(ValidationError):
            Error(
                message="We just had an error",
                details=ErrorDetailsNonBaseModel(other="some value"),
            )
```


### Python, Pydantic & OS Version

```Text
pydantic version: 2.3.0
        pydantic-core version: 2.6.3
          pydantic-core build: profile=release pgo=false
                 install path: /Users/carlos/.virtualenvs/pydantic-issue/lib/python3.10/site-packages/pydantic
               python version: 3.10.8 (v3.10.8:aaaf517424, Oct 11 2022, 10:14:40) [Clang 13.0.0 (clang-1300.0.29.30)]
                     platform: macOS-13.5.1-arm64-arm-64bit
     optional deps. installed: ['typing-extensions']
```



Discussion:

Resolving patch:
diff --git a/pydantic/_internal/_generate_schema.py b/pydantic/_internal/_generate_schema.py
--- a/pydantic/_internal/_generate_schema.py
+++ b/pydantic/_internal/_generate_schema.py
@@ -1449,10 +1449,26 @@ def _callable_schema(self, function: Callable[..., Any]) -> core_schema.CallSche
     def _unsubstituted_typevar_schema(self, typevar: typing.TypeVar) -> core_schema.CoreSchema:
         assert isinstance(typevar, typing.TypeVar)
 
-        if typevar.__bound__:
-            return self.generate_schema(typevar.__bound__)
-        elif typevar.__constraints__:
-            return self._union_schema(typing.Union[typevar.__constraints__])  # type: ignore
+        bound = typevar.__bound__
+        constraints = typevar.__constraints__
+        not_set = object()
+        default = getattr(typevar, '__default__', not_set)
+
+        if (bound is not None) + (len(constraints) != 0) + (default is not not_set) > 1:
+            raise NotImplementedError(
+                'Pydantic does not support mixing more than one of TypeVar bounds, constraints and defaults'
+            )
+
+        if default is not not_set:
+            return self.generate_schema(default)
+        elif constraints:
+            return self._union_schema(typing.Union[constraints])  # type: ignore
+        elif bound:
+            schema = self.generate_schema(bound)
+            schema['serialization'] = core_schema.wrap_serializer_function_ser_schema(
+                lambda x, h: h(x), schema=core_schema.any_schema()
+            )
+            return schema
         else:
             return core_schema.any_schema()
 


Test patch:
diff --git a/tests/test_generics.py b/tests/test_generics.py
--- a/tests/test_generics.py
+++ b/tests/test_generics.py
@@ -32,7 +32,18 @@
 import pytest
 from dirty_equals import HasRepr, IsStr
 from pydantic_core import CoreSchema, core_schema
-from typing_extensions import Annotated, Literal, OrderedDict, ParamSpec, TypeVarTuple, Unpack, get_args
+from typing_extensions import (
+    Annotated,
+    Literal,
+    OrderedDict,
+    ParamSpec,
+    TypeVarTuple,
+    Unpack,
+    get_args,
+)
+from typing_extensions import (
+    TypeVar as TypingExtensionsTypeVar,
+)
 
 from pydantic import (
     BaseModel,
@@ -2622,3 +2633,140 @@ class Model(Generic[T], BaseModel):
     m1 = Model[int](x=1)
     m2 = Model[int](x=1)
     assert len({m1, m2}) == 1
+
+
+def test_serialize_unsubstituted_typevars_bound() -> None:
+    class ErrorDetails(BaseModel):
+        foo: str
+
+    ErrorDataT = TypeVar('ErrorDataT', bound=ErrorDetails)
+
+    class Error(BaseModel, Generic[ErrorDataT]):
+        message: str
+        details: ErrorDataT
+
+    class MyErrorDetails(ErrorDetails):
+        bar: str
+
+    sample_error = Error(
+        message='We just had an error',
+        details=MyErrorDetails(foo='var', bar='baz'),
+    )
+    assert sample_error.details.model_dump() == {
+        'foo': 'var',
+        'bar': 'baz',
+    }
+    assert sample_error.model_dump() == {
+        'message': 'We just had an error',
+        'details': {
+            'foo': 'var',
+            'bar': 'baz',
+        },
+    }
+
+    sample_error = Error[ErrorDetails](
+        message='We just had an error',
+        details=MyErrorDetails(foo='var', bar='baz'),
+    )
+    assert sample_error.details.model_dump() == {
+        'foo': 'var',
+        'bar': 'baz',
+    }
+    assert sample_error.model_dump() == {
+        'message': 'We just had an error',
+        'details': {
+            'foo': 'var',
+        },
+    }
+
+    sample_error = Error[MyErrorDetails](
+        message='We just had an error',
+        details=MyErrorDetails(foo='var', bar='baz'),
+    )
+    assert sample_error.details.model_dump() == {
+        'foo': 'var',
+        'bar': 'baz',
+    }
+    assert sample_error.model_dump() == {
+        'message': 'We just had an error',
+        'details': {
+            'foo': 'var',
+            'bar': 'baz',
+        },
+    }
+
+
+@pytest.mark.parametrize(
+    'type_var',
+    [
+        TypingExtensionsTypeVar('ErrorDataT', default=BaseModel),
+        TypeVar('ErrorDataT', BaseModel, str),
+    ],
+    ids=['default', 'constraint'],
+)
+def test_serialize_unsubstituted_typevars_bound(
+    type_var: Type[BaseModel],
+) -> None:
+    class ErrorDetails(BaseModel):
+        foo: str
+
+    class Error(BaseModel, Generic[type_var]):  # type: ignore
+        message: str
+        details: type_var
+
+    class MyErrorDetails(ErrorDetails):
+        bar: str
+
+    sample_error = Error(
+        message='We just had an error',
+        details=MyErrorDetails(foo='var', bar='baz'),
+    )
+    assert sample_error.details.model_dump() == {
+        'foo': 'var',
+        'bar': 'baz',
+    }
+    assert sample_error.model_dump() == {
+        'message': 'We just had an error',
+        'details': {},
+    }
+
+    sample_error = Error[ErrorDetails](
+        message='We just had an error',
+        details=MyErrorDetails(foo='var', bar='baz'),
+    )
+    assert sample_error.details.model_dump() == {
+        'foo': 'var',
+        'bar': 'baz',
+    }
+    assert sample_error.model_dump() == {
+        'message': 'We just had an error',
+        'details': {
+            'foo': 'var',
+        },
+    }
+
+    sample_error = Error[MyErrorDetails](
+        message='We just had an error',
+        details=MyErrorDetails(foo='var', bar='baz'),
+    )
+    assert sample_error.details.model_dump() == {
+        'foo': 'var',
+        'bar': 'baz',
+    }
+    assert sample_error.model_dump() == {
+        'message': 'We just had an error',
+        'details': {
+            'foo': 'var',
+            'bar': 'baz',
+        },
+    }
+
+
+def test_mix_default_and_constraints() -> None:
+    T = TypingExtensionsTypeVar('T', str, int, default=str)
+
+    msg = 'Pydantic does not support mixing more than one of TypeVar bounds, constraints and defaults'
+    with pytest.raises(NotImplementedError, match=msg):
+
+        class _(BaseModel, Generic[T]):
+            x: T