Repository: pydantic/pydantic
Pull request: Allow arbitrary refs in JSON schema `examples`
Fix https://github.com/pydantic/pydantic/issues/9981

Skip ref counting and processing for the custom `examples` block in JSON schema.

Problem statement:
Using $ref" in examples raises KeyError on .model_json_schema() generation
### Initial Checks

- [X] I confirm that I'm using Pydantic V2

### Description

Using {"$ref": "value"} in Field example raises KeyError on `.model_json_schema()` generation.

I'm not sure if this is json-schema spec problem or pydantic problem (is it REQUIRED to resolve `$ref` in any place in schema, even in examples?)

### Example Code

```Python
from pydantic import BaseModel, Field

class Test(BaseModel):
   x: dict = Field(example={"$ref": "#/components/schemas/Pet"})

Test.model_json_schema()

# raises KeyError: '#/components/schemas/Pet'
```


### Python, Pydantic & OS Version

```Text
             pydantic version: 2.8.2
        pydantic-core version: 2.20.1
          pydantic-core build: profile=release pgo=true
                 install path: /home/vgavro/work/myproj/.venv/lib/python3.11/site-packages/pydantic
               python version: 3.11.9 (main, Apr 29 2024, 11:57:25) [GCC 13.2.1 20240417]
                     platform: Linux-6.8.7-arch1-2-x86_64-with-glibc2.39
             related packages: fastapi-0.109.2 typing_extensions-4.9.0 mypy-1.8.0 pydantic-settings-2.3.3
                       commit: unknown
```

Hints from issue discussion:
Yep, marking this as a dupe of https://github.com/pydantic/pydantic/issues/9783.

Thanks for your question @vgavro!
Hey here!

@sydney-runkle, I just tested a similar code at the beta pydantic version.
But I still have the `KeyError`, even though it's supposed to be resolved?
The example from #9783 works for me okay, but not this case.

**Example code**
```python
from pydantic import BaseModel, Field


class Test(BaseModel):
    x: dict = Field(
        examples=[
            # {"key": "val"},  # ok
            # {"$key": "val"},  # ok
            {"$ref": "val"},  # error
        ],
    )


Test.model_json_schema()
```

**Python, Pydantic & OS Version**
```
             pydantic version: 2.9.0b1
        pydantic-core version: 2.23.0
          pydantic-core build: profile=release pgo=false
                 install path: D:\Code\project\.venv\Lib\site-packages\pydantic
               python version: 3.12.3 (tags/v3.12.3:f6650f9, Apr  9 2024, 14:05:25) [MSC v.1938 64 bit (AMD64)]
                     platform: Windows-10-10.0.19045-SP0
             related packages: fastapi-0.112.1 mypy-1.11.1 pydantic-settings-2.4.0 typing_extensions-4.12.2
                       commit: unknown
```

Is this an expected behavior?

@Danipulok,

Thanks for bringing this to our attention! I'll take a look at fixing this for our next beta release.
Yeah, reopening this - looks like this isn't a dupe of the original issue. Not sure if we want to support this or not, but we can try in v2.10.
Alright, upon further investigation here - we don't plan to support this.

[`$ref`](https://json-schema.org/draft/2020-12/json-schema-core#name-direct-references-with-ref) is used to refer to a schema, and `examples` doesn't contain any schema, so support for this is out of scope.

This is a bit inconsistent with the above referenced change that added support for `http/s` valued refs, and I'd be willing to revert that in the name of consistency if desired.
Actually, reopening. We can skip the `examples` key and let folks inject whatever they want here.

Commit message: skip json schema val for custom ref

Patch to review:
diff --git a/pydantic/json_schema.py b/pydantic/json_schema.py
index c48e781ba3f..b0653e0318b 100644
--- a/pydantic/json_schema.py
+++ b/pydantic/json_schema.py
@@ -2172,7 +2172,9 @@ def _add_json_refs(schema: Any) -> None:
                         if not json_ref.startswith(('http://', 'https://')):
                             raise
 
-                for v in schema.values():
+                for k, v in schema.items():
+                    if k == 'examples':
+                        continue  # skip refs processing for examples, allow arbitrary values / refs
                     _add_json_refs(v)
             elif isinstance(schema, list):
                 for v in schema:
@@ -2484,6 +2486,8 @@ def _get_all_json_refs(item: Any) -> set[JsonRef]:
         current = stack.pop()
         if isinstance(current, dict):
             for key, value in current.items():
+                if key == 'examples':
+                    continue  # skip examples, allow arbitrary values / refs
                 if key == '$ref' and isinstance(value, str):
                     refs.add(JsonRef(value))
                 elif isinstance(value, dict):
diff --git a/tests/test_json_schema.py b/tests/test_json_schema.py
index 7ec70a20499..a9e84e9bb4d 100644
--- a/tests/test_json_schema.py
+++ b/tests/test_json_schema.py
@@ -6554,3 +6554,20 @@ class Model(BaseModel):
         some_field: str = Field(alias='_some_field')
 
     assert Model.model_json_schema()['properties']['_some_field']['title'] == 'Some Field'
+
+
+@pytest.mark.skip_json_schema_validation(reason='Custom ref used.')
+def test_arbitrary_ref_in_json_schema() -> None:
+    """See https://github.com/pydantic/pydantic/issues/9981."""
+
+    class Test(BaseModel):
+        x: dict = Field(examples={'$ref': '#/components/schemas/Pet'})
+
+    assert Test.model_json_schema() == {
+        'properties': {'x': {'examples': {'$ref': '#/components/schemas/Pet'}, 'title': 'X', 'type': 'object'}},
+        'required': ['x'],
+        'title': 'Test',
+        'type': 'object',
+    }
+
+    # raises KeyError: '#/components/schemas/Pet'


Code review comments:
diff_hunk: @@ -6554,3 +6554,20 @@ class Model(BaseModel):
         some_field: str = Field(alias='_some_field')
 
     assert Model.model_json_schema()['properties']['_some_field']['title'] == 'Some Field'
+
+
+@pytest.mark.skip_json_schema_validation(reason='Custom ref used.')
+def test_arbitrary_ref_in_json_schema() -> None:
+    """See https://github.com/pydantic/pydantic/issues/9981."""
+
+    class Test(BaseModel):
+        x: dict = Field(examples={'$ref': '#/components/schemas/Pet'})
path: tests/test_json_schema.py
text: @user1:
```suggestion
        x: dict = Field(examples=[{'$ref': '#/components/schemas/Pet'}])
```

Even though the test works because `skip_json_schema_validation` is used, still good to have valid JSON Schemas generated.

@user1:
Or maybe using a dict as an example was a requirement to have the added logic in `get_json_ref_counts` covered by this test?

@author:
Good call - I've switched to a list, and removed the xfail. This still works, as we unpack a `list` of things in `get_json_ref_counts` and iterate over those :)