Repository: pydantic/pydantic

Issue:
Model name replaced by drive letter in JSON schema on Windows
### Initial Checks

- [X] I confirm that I'm using Pydantic V2

### Description

In the JSON schema generated by pydantic the name of the model is not used, rather the drive letter is used on Windows, like `D` from `D:` instead of `MyModel` if the model is imported from an absolute filesystem path using importlib. The JSON schema thus looks something like this:

```json
{
  "$defs": {
    "D": {
      "properties": {
        "my_int": {
          "title": "My Int",
          "type": "integer"
        }
      },
      "required": ["my_int"],
      "title": "MyModel",
      "type": "object"
    }
  },
  "title": "My Schema"
}
```

If the model is imported using a relative path or on a system that does not use drive letters the JOSN schema has the expected name of the model (`MyModel`) in it:

```json
{
  "$defs": {
    "MyModel": {
      "properties": {
        "my_int": {
          "title": "My Int",
          "type": "integer"
        }
      },
      "required": ["my_int"],
      "title": "MyModel",
      "type": "object"
    }
  },
  "title": "My Schema"
}
```

I believe the cause of this is the `get_defs_ref` function in [pydantic/json_schema.py](https://github.com/pydantic/pydantic/blob/v2.4.2/pydantic/json_schema.py#L1847) which tries to remove an ID by splitting at a colon `:` character and thus ends up removing everything after the drive letter, so `C:\Users\...` becomes just `C`.

A more detailed description and example that you can run can be found at https://github.com/joakimnordling/fastapi-pydantic-bug-poc Below is the same example for completeness:

### Example Code

Create two files.

main.py:

```Python
import importlib.util
import json
from pathlib import Path

from pydantic.json_schema import models_json_schema


def import_my_model():
    """
    Import a Pydantic model using importlib from an absolute filesystem path.
    """
    poc_dir = Path(__file__).parent.relative_to(Path.cwd())
    p = (poc_dir / "models.py").absolute()
    spec = importlib.util.spec_from_file_location(name=str(p), location=str(p))

    if not spec.loader:
        raise RuntimeError(f"Failed to import {p} module")

    module = spec.loader.load_module(str(p))

    return getattr(module, "MyModel")


def main():
    MyModel = import_my_model()

    _, top_level_schema = models_json_schema(
        [(MyModel, "validation")], title="My Schema"
    )
    print(json.dumps(top_level_schema, indent=2))


if __name__ == "__main__":
    main()
```

models.py:
```python
from pydantic import BaseModel, Field


class MyModel(BaseModel):
    my_int: int = Field(
        ...,
    )
```


### Python, Pydantic & OS Version

```Text
pydantic version: 2.4.2
        pydantic-core version: 2.10.1
          pydantic-core build: profile=release pgo=false
                 install path: C:\Users\runneradmin\AppData\Local\pypoetry\Cache\virtualenvs\fastapi-pydantic-bug-poc-y0CjNB0N-py3.10\Lib\site-packages\pydantic
               python version: 3.10.11 (tags/v3.10.11:7d4cc5a, Apr  5 2023, 00:38:17) [MSC v.1929 64 bit (AMD64)]
                     platform: Windows-10-10.0.20348-SP0
             related packages: typing_extensions-4.8.0
```



Discussion:
@davidhewitt, could you take a look at this tomorrow when you're back online? Thanks 😄 
It seems bizarre to me that a `C:\` would be showing up in the defs ref at all, I'm not sure how that's happening.

That said, @joakimnordling could you try changing [the line you identified](https://github.com/pydantic/pydantic/blob/d1634da7d9cce1f91ae6f76bd3379b776b7de6da/pydantic/json_schema.py):

```python
        components = [x.split(':')[0] for x in components]
```
to
```python
        components = [x.rsplit(':', 1)[0] for x in components]
```
? This should cause it to only remove the last item (which was the intent) as opposed to keeping the first item. (If you are right about this, I guess my assumption that the `core_ref` wouldn't have a colon was incorrect. Again, not sure where that's coming from, but this seems more correct anyway.)

If that fixes it for you I suspect we could quickly merge that change.

Resolving patch:
diff --git a/pydantic/json_schema.py b/pydantic/json_schema.py
--- a/pydantic/json_schema.py
+++ b/pydantic/json_schema.py
@@ -1844,7 +1844,7 @@ def get_defs_ref(self, core_mode_ref: CoreModeRef) -> DefsRef:
         core_ref, mode = core_mode_ref
         components = re.split(r'([\][,])', core_ref)
         # Remove IDs from each component
-        components = [x.split(':')[0] for x in components]
+        components = [x.rsplit(':', 1)[0] for x in components]
         core_ref_no_id = ''.join(components)
         # Remove everything before the last period from each "component"
         components = [re.sub(r'(?:[^.[\]]+\.)+((?:[^.[\]]+))', r'\1', x) for x in components]


Test patch:
diff --git a/tests/conftest.py b/tests/conftest.py
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -50,13 +50,14 @@ def disable_error_urls():
 
 @pytest.fixture
 def create_module(tmp_path, request):
-    def run(source_code_or_function, rewrite_assertions=True):
+    def run(source_code_or_function, rewrite_assertions=True, module_name_prefix=None):
         """
         Create module object, execute it and return
         Can be used as a decorator of the function from the source code of which the module will be constructed
 
         :param source_code_or_function string or function with body as a source code for created module
         :param rewrite_assertions: whether to rewrite assertions in module or not
+        :param module_name_prefix: string prefix to use in the name of the module, does not affect the name of the file.
 
         """
         if isinstance(source_code_or_function, FunctionType):
@@ -65,6 +66,8 @@ def run(source_code_or_function, rewrite_assertions=True):
             source_code = source_code_or_function
 
         module_name, filename = _create_module_file(source_code, tmp_path, request.node.name)
+        if module_name_prefix:
+            module_name = module_name_prefix + module_name
 
         if rewrite_assertions:
             loader = AssertionRewritingHook(config=request.config)
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
@@ -5702,3 +5702,29 @@ class Bar(BaseModel):
         },
         'allOf': [{'$ref': '#/$defs/Bar'}],
     }
+
+
+def test_module_with_colon_in_name(create_module):
+    module = create_module(
+        # language=Python
+        """
+from pydantic import BaseModel
+
+class Foo(BaseModel):
+    x: int
+        """,
+        module_name_prefix='C:\\',
+    )
+
+    foo_model = module.Foo
+    _, v_schema = models_json_schema([(foo_model, 'validation')])
+    assert v_schema == {
+        '$defs': {
+            'Foo': {
+                'properties': {'x': {'title': 'X', 'type': 'integer'}},
+                'required': ['x'],
+                'title': 'Foo',
+                'type': 'object',
+            }
+        }
+    }