Repository: pydantic/pydantic

Issue:
Leak in `pydantic_js_functions` leading to `RecursionError`s with V2 generic model json schema handling
### Initial Checks

- [X] I confirm that I'm using Pydantic V2

### Description

There seems to be a leak related to items in `pydantic_js_functions` which also leads to an `RecursionError` with `model_json_schema` handling. Seems `pydantic_js_functions` is getting lot of entries to the list with certain types of generic models. Below I tried to isolate the problem as best as possible but there might be even a simpler example of the failure.

### Example Code

```Python
from typing import Generic, Optional, TypeVar

from pydantic import BaseModel

try:
    # v2
    from pydantic._internal._generics import _GENERIC_TYPES_CACHE
    from pydantic import BaseModel as PydanticModel
    GenericModel = BaseModel
except ImportError:
    # v1
    from pydantic import BaseModel
    from pydantic.generics import GenericModel as GenericModel


T = TypeVar("T")


def test_leak():
    class Inner(BaseModel):
        a: int

    class Outer(GenericModel, Generic[T]):
        b: Optional[T]  # Works when not Optional...

    for i in range(1000):
        class ModelTest(BaseModel):
            c: Outer[Inner]

        if core_schema := getattr(ModelTest, "__pydantic_core_schema__", None):
            # v2
            # pydantic_js_functions on "Inner" keeps on growing at each iteration: 5, 6, 7, 8, 9, 10...
            inner = next(d for d in core_schema["definitions"] if d["cls"].__name__ == "Inner")
            inner_pydantic_js_functions = inner["metadata"]["pydantic_js_functions"]
            print(f"Run {i}: len(pydantic_js_functions)={len(inner_pydantic_js_functions)}")
            # ... and then RecursionError at "Run 228: len(pydantic_js_functions)=232"
            ModelTest.model_json_schema()
        else:
            # v1
            # It works without issues
            ModelTest.schema()
```


### Python, Pydantic & OS Version

```Text
pydantic version: 2.0.3
        pydantic-core version: 2.3.0 release build profile
                 install path: /Users/markussintonen/Library/Caches/pypoetry/virtualenvs/leak-test-VvUvdXxD-py3.11/lib/python3.11/site-packages/pydantic
               python version: 3.11.1 (main, Dec 23 2022, 09:40:27) [Clang 14.0.0 (clang-1400.0.29.202)]
                     platform: macOS-13.4.1-x86_64-i386-64bit
     optional deps. installed: ['typing-extensions']
```


Selected Assignee: @dmontagu


Discussion:
And here is part of the stack trace with recursion error:
```
../../Library/Caches/pypoetry/virtualenvs/leak-test-VvUvdXxD-py3.11/lib/python3.11/site-packages/pydantic/main.py:372: in model_json_schema
    return model_json_schema(
../../Library/Caches/pypoetry/virtualenvs/leak-test-VvUvdXxD-py3.11/lib/python3.11/site-packages/pydantic/json_schema.py:2073: in model_json_schema
    return schema_generator_instance.generate(cls.__pydantic_core_schema__, mode=mode)
../../Library/Caches/pypoetry/virtualenvs/leak-test-VvUvdXxD-py3.11/lib/python3.11/site-packages/pydantic/json_schema.py:381: in generate
    json_schema: JsonSchemaValue = self.generate_inner(schema)
../../Library/Caches/pypoetry/virtualenvs/leak-test-VvUvdXxD-py3.11/lib/python3.11/site-packages/pydantic/json_schema.py:520: in generate_inner
    json_schema = current_handler(schema)
../../Library/Caches/pypoetry/virtualenvs/leak-test-VvUvdXxD-py3.11/lib/python3.11/site-packages/pydantic/_internal/_schema_generation_shared.py:36: in __call__
    return self.handler(__core_schema)
../../Library/Caches/pypoetry/virtualenvs/leak-test-VvUvdXxD-py3.11/lib/python3.11/site-packages/pydantic/json_schema.py:477: in handler_func
    json_schema = generate_for_schema_type(schema_or_field)
../../Library/Caches/pypoetry/virtualenvs/leak-test-VvUvdXxD-py3.11/lib/python3.11/site-packages/pydantic/json_schema.py:1658: in definitions_schema
    self.generate_inner(definition)
../../Library/Caches/pypoetry/virtualenvs/leak-test-VvUvdXxD-py3.11/lib/python3.11/site-packages/pydantic/json_schema.py:520: in generate_inner
    json_schema = current_handler(schema)
../../Library/Caches/pypoetry/virtualenvs/leak-test-VvUvdXxD-py3.11/lib/python3.11/site-packages/pydantic/_internal/_schema_generation_shared.py:36: in __call__
    return self.handler(__core_schema)
../../Library/Caches/pypoetry/virtualenvs/leak-test-VvUvdXxD-py3.11/lib/python3.11/site-packages/pydantic/json_schema.py:494: in new_handler_func
    json_schema = js_modify_function(schema_or_field, current_handler)
../../Library/Caches/pypoetry/virtualenvs/leak-test-VvUvdXxD-py3.11/lib/python3.11/site-packages/pydantic/main.py:558: in __get_pydantic_json_schema__
    return __handler(__core_schema)
../../Library/Caches/pypoetry/virtualenvs/leak-test-VvUvdXxD-py3.11/lib/python3.11/site-packages/pydantic/_internal/_schema_generation_shared.py:36: in __call__
    return self.handler(__core_schema)
../../Library/Caches/pypoetry/virtualenvs/leak-test-VvUvdXxD-py3.11/lib/python3.11/site-packages/pydantic/json_schema.py:494: in new_handler_func
    json_schema = js_modify_function(schema_or_field, current_handler)
../../Library/Caches/pypoetry/virtualenvs/leak-test-VvUvdXxD-py3.11/lib/python3.11/site-packages/pydantic/main.py:558: in __get_pydantic_json_schema__
    return __handler(__core_schema)
../../Library/Caches/pypoetry/virtualenvs/leak-test-VvUvdXxD-py3.11/lib/python3.11/site-packages/pydantic/_internal/_schema_generation_shared.py:36: in __call__
    return self.handler(__core_schema)
../../Library/Caches/pypoetry/virtualenvs/leak-test-VvUvdXxD-py3.11/lib/python3.11/site-packages/pydantic/json_schema.py:494: in new_handler_func
    json_schema = js_modify_function(schema_or_field, current_handler)
../../Library/Caches/pypoetry/virtualenvs/leak-test-VvUvdXxD-py3.11/lib/python3.11/site-packages/pydantic/main.py:558: in __get_pydantic_json_schema__
    return __handler(__core_schema)
../../Library/Caches/pypoetry/virtualenvs/leak-test-VvUvdXxD-py3.11/lib/python3.11/site-packages/pydantic/_internal/_schema_generation_shared.py:36: in __call__
    return self.handler(__core_schema)
../../Library/Caches/pypoetry/virtualenvs/leak-test-VvUvdXxD-py3.11/lib/python3.11/site-packages/pydantic/json_schema.py:494: in new_handler_func
    json_schema = js_modify_function(schema_or_field, current_handler)
../../Library/Caches/pypoetry/virtualenvs/leak-test-VvUvdXxD-py3.11/lib/python3.11/site-packages/pydantic/main.py:558: in __get_pydantic_json_schema__
    return __handler(__core_schema)
../../Library/Caches/pypoetry/virtualenvs/leak-test-VvUvdXxD-py3.11/lib/python3.11/site-packages/pydantic/_internal/_schema_generation_shared.py:36: in __call__
    return self.handler(__core_schema)
../../Library/Caches/pypoetry/virtualenvs/leak-test-VvUvdXxD-py3.11/lib/python3.11/site-packages/pydantic/json_schema.py:494: in new_handler_func
    json_schema = js_modify_function(schema_or_field, current_handler)
../../Library/Caches/pypoetry/virtualenvs/leak-test-VvUvdXxD-py3.11/lib/python3.11/site-packages/pydantic/main.py:558: in __get_pydantic_json_schema__
    return __handler(__core_schema)
../../Library/Caches/pypoetry/virtualenvs/leak-test-VvUvdXxD-py3.11/lib/python3.11/site-packages/pydantic/_internal/_schema_generation_shared.py:36: in __call__
    return self.handler(__core_schema)
../../Library/Caches/pypoetry/virtualenvs/leak-test-VvUvdXxD-py3.11/lib/python3.11/site-packages/pydantic/json_schema.py:494: in new_handler_func
    json_schema = js_modify_function(schema_or_field, current_handler)
../../Library/Caches/pypoetry/virtualenvs/leak-test-VvUvdXxD-py3.11/lib/python3.11/site-packages/pydantic/main.py:558: in __get_pydantic_json_schema__
    return __handler(__core_schema)
../../Library/Caches/pypoetry/virtualenvs/leak-test-VvUvdXxD-py3.11/lib/python3.11/site-packages/pydantic/_internal/_schema_generation_shared.py:36: in __call__
    return self.handler(__core_schema)
../../Library/Caches/pypoetry/virtualenvs/leak-test-VvUvdXxD-py3.11/lib/python3.11/site-packages/pydantic/json_schema.py:494: in new_handler_func
    json_schema = js_modify_function(schema_or_field, current_handler)
../../Library/Caches/pypoetry/virtualenvs/leak-test-VvUvdXxD-py3.11/lib/python3.11/site-packages/pydantic/main.py:558: in __get_pydantic_json_schema__
    return __handler(__core_schema)
../../Library/Caches/pypoetry/virtualenvs/leak-test-VvUvdXxD-py3.11/lib/python3.11/site-packages/pydantic/_internal/_schema_generation_shared.py:36: in __call__
    return self.handler(__core_schema)
../../Library/Caches/pypoetry/virtualenvs/leak-test-VvUvdXxD-py3.11/lib/python3.11/site-packages/pydantic/json_schema.py:494: in new_handler_func
    json_schema = js_modify_function(schema_or_field, current_handler)
../../Library/Caches/pypoetry/virtualenvs/leak-test-VvUvdXxD-py3.11/lib/python3.11/site-packages/pydantic/main.py:558: in __get_pydantic_json_schema__
    return __handler(__core_schema)
... same repeats ...
RecursionError: maximum recursion depth exceeded
```
I tested against branch fixing previous leak: https://github.com/pydantic/pydantic/pull/6681 But it doesnt fix this issue
Makes sense. @adriangb I think we'll probably both need to look at this to get to the bottom of it 😩 

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
@@ -281,6 +281,17 @@ def generate_schema(
             obj, from_dunder_get_core_schema=from_dunder_get_core_schema, from_prepare_args=from_prepare_args
         )
 
+    def _add_js_function(self, metadata_schema: CoreSchema, js_function: Callable[..., Any]) -> None:
+        metadata = CoreMetadataHandler(metadata_schema).metadata
+        pydantic_js_functions = metadata.setdefault('pydantic_js_functions', [])
+        # because of how we generate core schemas for nested generic models
+        # we can end up adding `BaseModel.__get_pydantic_json_schema__` multiple times
+        # this check may fail to catch duplicates if the function is a `functools.partial`
+        # or something like that
+        # but if it does it'll fail by inserting the duplicate
+        if js_function not in pydantic_js_functions:
+            pydantic_js_functions.append(js_function)
+
     def _generate_schema_for_type(
         self,
         obj: Any,
@@ -304,8 +315,7 @@ def _generate_schema_for_type(
         if metadata_js_function is not None:
             metadata_schema = resolve_original_schema(schema, self.defs.definitions)
             if metadata_schema:
-                metadata = CoreMetadataHandler(metadata_schema).metadata
-                metadata.setdefault('pydantic_js_functions', []).append(metadata_js_function)
+                self._add_js_function(metadata_schema, metadata_js_function)
 
         return schema
 
@@ -1370,10 +1380,7 @@ def inner_handler(obj: Any) -> CoreSchema:
                 if metadata_js_function is not None:
                     metadata_schema = resolve_original_schema(schema, self.defs.definitions)
                     if metadata_schema is not None:
-                        metadata = CoreMetadataHandler(metadata_schema).metadata
-                        pydantic_js_functions = metadata.setdefault('pydantic_js_functions', [])
-                        if metadata_js_function not in pydantic_js_functions:
-                            pydantic_js_functions.append(metadata_js_function)
+                        self._add_js_function(metadata_schema, metadata_js_function)
             return transform_inner_schema(schema)
 
         get_inner_schema = CallbackGetCoreSchemaHandler(inner_handler, self)


Test patch:
diff --git a/tests/test_json_schema.py b/tests/test_json_schema.py
--- a/tests/test_json_schema.py
+++ b/tests/test_json_schema.py
@@ -5072,3 +5072,41 @@ class MyModel(BaseModel):
         'title': 'MyModel',
         'type': 'object',
     }
+
+
+def test_multiple_parametrization_of_generic_model() -> None:
+    """https://github.com/pydantic/pydantic/issues/6708"""
+    T = TypeVar('T')
+
+    calls = 0
+
+    class Inner(BaseModel):
+        a: int
+
+        @classmethod
+        def __get_pydantic_json_schema__(
+            cls, core_schema: CoreSchema, handler: GetJsonSchemaHandler
+        ) -> JsonSchemaValue:
+            nonlocal calls
+            calls += 1
+            json_schema = handler(core_schema)
+            return json_schema
+
+    class Outer(BaseModel, Generic[T]):
+        b: Optional[T]
+
+    class ModelTest(BaseModel):
+        c: Outer[Inner]
+
+    for _ in range(sys.getrecursionlimit() + 1):
+
+        class ModelTest(BaseModel):  # noqa: F811
+            c: Outer[Inner]
+
+    ModelTest.model_json_schema()
+
+    # this is not necessarily a promise we make
+    # (in fact, we've had bugs in the past where this was not the case and we'd
+    # call the __get_pydantic_json_schema__ method multiple times)
+    # but it's much easier to test for than absence of a recursion limit
+    assert calls == 1