diff --git a/fastapi/_compat.py b/fastapi/_compat.py
index 0e3afa8e..cd89bba3 100644
--- a/fastapi/_compat.py
+++ b/fastapi/_compat.py
@@ -9,8 +9,97 @@ from pydantic import BaseModel, create_model
from pydantic.version import VERSION as P_VERSION
from starlette.datastructures import UploadFile
from typing_extensions import Annotated, Literal, get_args, get_origin
+from pydantic.v1.utils import update_not_none
PYDANTIC_VERSION = P_VERSION
PYDANTIC_V2 = PYDANTIC_VERSION.startswith('2.')
+
+def _model_rebuild(cls: Type[BaseModel]) -> None:
+ """Rebuild a pydantic model, updating its configuration.
+
+ This is a compatibility function for Pydantic v2 that mimics the behavior of
+ the original _model_rebuild function from Pydantic v1.
+ """
+ if not PYDANTIC_V2:
+ from pydantic.main import _model_rebuild as v1_model_rebuild
+ return v1_model_rebuild(cls)
+
+ # For Pydantic v2, we need to force model rebuild by clearing caches
+ if hasattr(cls, '__pydantic_validator__'):
+ delattr(cls, '__pydantic_validator__')
+ if hasattr(cls, '__pydantic_serializer__'):
+ delattr(cls, '__pydantic_serializer__')
+ if hasattr(cls, '__pydantic_core_schema__'):
+ delattr(cls, '__pydantic_core_schema__')
+ cls.__pydantic_complete__ = False
+
+ # Use Pydantic v1 model rebuild to handle circular references
+ from pydantic.v1.main import _model_rebuild as v1_model_rebuild
+
+ # Create a new model with the same configuration
+ from pydantic.v1.main import create_model
+ new_model = create_model(
+ cls.__name__,
+ __base__=cls,
+ __module__=cls.__module__,
+ __validators__=cls.__dict__.get('__validators__', {}),
+ __cls_kwargs__=cls.__dict__.get('__cls_kwargs__', {}),
+ )
+
+ # Rebuild the model using Pydantic v1
+ v1_model_rebuild(new_model)
+
+ # Copy the rebuilt model's attributes back to the original class
+ for attr in ('__fields__', '__validators__', '__pre_root_validators__', '__post_root_validators__',
+ '__config__', '__schema_cache__', '__json_encoder__', '__custom_root_type__',
+ '__private_attributes__', '__slots__', '__class_vars__', '__fields_set__'):
+ if hasattr(new_model, attr):
+ setattr(cls, attr, getattr(new_model, attr))
+
+ # Add model validators to handle circular references
+ from pydantic import model_validator
+
+ @model_validator(mode='before')
+ def validate_refs(cls, values: Dict[str, Any]) -> Dict[str, Any]:
+ if not isinstance(values, dict):
+ return values
+ for field_name, field_value in values.items():
+ if isinstance(field_value, dict):
+ if '$ref' in field_value:
+ from fastapi.openapi.models import Reference
+ values[field_name] = Reference(**field_value)
+ else:
+ # Check if the field is a reference to another model
+ from fastapi.openapi.models import Schema, Operation, Encoding, RequestBody, Response, PathItem, Header
+ if field_name in ('schema', 'schema_', 'items', 'contains', 'additionalProperties', 'propertyNames', 'unevaluatedItems', 'unevaluatedProperties', 'contentSchema'):
+ values[field_name] = Schema(**field_value)
+ elif field_name == 'requestBody':
+ values[field_name] = RequestBody(**field_value)
+ elif field_name == 'responses':
+ values[field_name] = {k: Response(**v) for k, v in field_value.items()}
+ elif field_name == 'callbacks':
+ values[field_name] = {k: {url: PathItem(**item) for url, item in v.items()} for k, v in field_value.items()}
+ elif field_name == 'headers':
+ values[field_name] = {k: Header(**v) for k, v in field_value.items()}
+ else:
+ values[field_name] = cls(**field_value)
+ elif isinstance(field_value, list):
+ values[field_name] = [
+ Reference(**v) if isinstance(v, dict) and '$ref' in v else cls(**v) if isinstance(v, dict) else v
+ for v in field_value
+ ]
+ return values
+
+ # Add the validator to the model
+ if not hasattr(cls, 'model_validators'):
+ cls.model_validators = []
+ if validate_refs not in cls.model_validators:
+ cls.model_validators.append(validate_refs)
+
+ # Force model rebuild
+ from pydantic.v1.main import _model_rebuild as v1_model_rebuild
+ v1_model_rebuild(cls)
+
+ return cls
sequence_annotation_to_type = {Sequence: list, List: list, list: list, Tuple: tuple, tuple: tuple, Set: set, set: set, FrozenSet: frozenset, frozenset: frozenset, Deque: deque, deque: deque}
sequence_types = tuple(sequence_annotation_to_type.keys())
if PYDANTIC_V2:
@@ -20,6 +109,7 @@ if PYDANTIC_V2:
from pydantic._internal._schema_generation_shared import GetJsonSchemaHandler as GetJsonSchemaHandler
from pydantic._internal._typing_extra import eval_type_lenient
from pydantic._internal._utils import lenient_issubclass as lenient_issubclass
+ from pydantic._internal._model_construction import _model_rebuild
from pydantic.fields import FieldInfo
from pydantic.json_schema import GenerateJsonSchema as GenerateJsonSchema
from pydantic.json_schema import JsonSchemaValue as JsonSchemaValue
diff --git a/fastapi/openapi/models.py b/fastapi/openapi/models.py
index 1507fa7f..4bbd8590 100644
--- a/fastapi/openapi/models.py
+++ b/fastapi/openapi/models.py
@@ -1,10 +1,14 @@
from enum import Enum
from typing import Any, Callable, Dict, Iterable, List, Optional, Set, Type, Union
-from fastapi._compat import PYDANTIC_V2, CoreSchema, GetJsonSchemaHandler, JsonSchemaValue, _model_rebuild, with_info_plain_validator_function
+from fastapi._compat import PYDANTIC_V2, CoreSchema, GetJsonSchemaHandler, JsonSchemaValue, with_info_plain_validator_function
from fastapi.logger import logger
-from pydantic import AnyUrl, BaseModel, Field
+from pydantic import AnyUrl, BaseModel, Field, model_validator
from typing_extensions import Annotated, Literal, TypedDict
from typing_extensions import deprecated as typing_deprecated
+from pydantic.v1.utils import update_not_none
+from pydantic.v1.main import ModelMetaclass
+from pydantic.v1.main import create_model
+from pydantic.v1.main import _model_rebuild
try:
import email_validator
assert email_validator
@@ -330,6 +334,137 @@ class OpenAPI(BaseModelWithConfig):
security: Optional[List[Dict[str, List[str]]]] = None
tags: Optional[List[Tag]] = None
externalDocs: Optional[ExternalDocumentation] = None
-_model_rebuild(Schema)
-_model_rebuild(Operation)
-_model_rebuild(Encoding)
\ No newline at end of file
+# Handle circular references using model validators
+@model_validator(mode='before')
+def validate_schema_refs(cls, values: Dict[str, Any]) -> Dict[str, Any]:
+ if not isinstance(values, dict):
+ return values
+ if 'allOf' in values:
+ values['allOf'] = [v if isinstance(v, bool) else Schema(**v) for v in values['allOf']]
+ if 'anyOf' in values:
+ values['anyOf'] = [v if isinstance(v, bool) else Schema(**v) for v in values['anyOf']]
+ if 'oneOf' in values:
+ values['oneOf'] = [v if isinstance(v, bool) else Schema(**v) for v in values['oneOf']]
+ if 'not_' in values:
+ values['not_'] = values['not_'] if isinstance(values['not_'], bool) else Schema(**values['not_'])
+ if 'if_' in values:
+ values['if_'] = values['if_'] if isinstance(values['if_'], bool) else Schema(**values['if_'])
+ if 'then' in values:
+ values['then'] = values['then'] if isinstance(values['then'], bool) else Schema(**values['then'])
+ if 'else_' in values:
+ values['else_'] = values['else_'] if isinstance(values['else_'], bool) else Schema(**values['else_'])
+ if 'dependentSchemas' in values:
+ values['dependentSchemas'] = {k: v if isinstance(v, bool) else Schema(**v) for k, v in values['dependentSchemas'].items()}
+ if 'prefixItems' in values:
+ values['prefixItems'] = [v if isinstance(v, bool) else Schema(**v) for v in values['prefixItems']]
+ if 'items' in values:
+ if isinstance(values['items'], list):
+ values['items'] = [v if isinstance(v, bool) else Schema(**v) for v in values['items']]
+ else:
+ values['items'] = values['items'] if isinstance(values['items'], bool) else Schema(**values['items'])
+ if 'contains' in values:
+ values['contains'] = values['contains'] if isinstance(values['contains'], bool) else Schema(**values['contains'])
+ if 'properties' in values:
+ values['properties'] = {k: v if isinstance(v, bool) else Schema(**v) for k, v in values['properties'].items()}
+ if 'patternProperties' in values:
+ values['patternProperties'] = {k: v if isinstance(v, bool) else Schema(**v) for k, v in values['patternProperties'].items()}
+ if 'additionalProperties' in values:
+ values['additionalProperties'] = values['additionalProperties'] if isinstance(values['additionalProperties'], bool) else Schema(**values['additionalProperties'])
+ if 'propertyNames' in values:
+ values['propertyNames'] = values['propertyNames'] if isinstance(values['propertyNames'], bool) else Schema(**values['propertyNames'])
+ if 'unevaluatedItems' in values:
+ values['unevaluatedItems'] = values['unevaluatedItems'] if isinstance(values['unevaluatedItems'], bool) else Schema(**values['unevaluatedItems'])
+ if 'unevaluatedProperties' in values:
+ values['unevaluatedProperties'] = values['unevaluatedProperties'] if isinstance(values['unevaluatedProperties'], bool) else Schema(**values['unevaluatedProperties'])
+ if 'contentSchema' in values:
+ values['contentSchema'] = values['contentSchema'] if isinstance(values['contentSchema'], bool) else Schema(**values['contentSchema'])
+ return values
+
+@model_validator(mode='before')
+def validate_operation_refs(cls, values: Dict[str, Any]) -> Dict[str, Any]:
+ if not isinstance(values, dict):
+ return values
+ if 'requestBody' in values:
+ values['requestBody'] = Reference(**values['requestBody']) if '$ref' in values['requestBody'] else RequestBody(**values['requestBody'])
+ if 'responses' in values:
+ values['responses'] = {k: Reference(**v) if '$ref' in v else Response(**v) for k, v in values['responses'].items()}
+ if 'callbacks' in values:
+ values['callbacks'] = {k: Reference(**v) if '$ref' in v else {url: PathItem(**item) for url, item in v.items()} for k, v in values['callbacks'].items()}
+ return values
+
+@model_validator(mode='before')
+def validate_encoding_refs(cls, values: Dict[str, Any]) -> Dict[str, Any]:
+ if not isinstance(values, dict):
+ return values
+ if 'headers' in values:
+ values['headers'] = {k: Reference(**v) if '$ref' in v else Header(**v) for k, v in values['headers'].items()}
+ return values
+
+# Handle circular references using model validators
+if PYDANTIC_V2:
+ # Create new models with the same configuration
+ Schema = create_model(
+ 'Schema',
+ __base__=Schema,
+ __module__=Schema.__module__,
+ __validators__=Schema.__dict__.get('__validators__', {}),
+ __cls_kwargs__=Schema.__dict__.get('__cls_kwargs__', {}),
+ )
+ Operation = create_model(
+ 'Operation',
+ __base__=Operation,
+ __module__=Operation.__module__,
+ __validators__=Operation.__dict__.get('__validators__', {}),
+ __cls_kwargs__=Operation.__dict__.get('__cls_kwargs__', {}),
+ )
+ Encoding = create_model(
+ 'Encoding',
+ __base__=Encoding,
+ __module__=Encoding.__module__,
+ __validators__=Encoding.__dict__.get('__validators__', {}),
+ __cls_kwargs__=Encoding.__dict__.get('__cls_kwargs__', {}),
+ )
+
+ # Add model validators to handle circular references
+ @model_validator(mode='before')
+ def validate_refs(cls, values: Dict[str, Any]) -> Dict[str, Any]:
+ if not isinstance(values, dict):
+ return values
+ for field_name, field_value in values.items():
+ if isinstance(field_value, dict):
+ if '$ref' in field_value:
+ values[field_name] = Reference(**field_value)
+ else:
+ # Check if the field is a reference to another model
+ if field_name in ('schema', 'schema_', 'items', 'contains', 'additionalProperties', 'propertyNames', 'unevaluatedItems', 'unevaluatedProperties', 'contentSchema'):
+ values[field_name] = Schema(**field_value)
+ elif field_name == 'requestBody':
+ values[field_name] = RequestBody(**field_value)
+ elif field_name == 'responses':
+ values[field_name] = {k: Response(**v) for k, v in field_value.items()}
+ elif field_name == 'callbacks':
+ values[field_name] = {k: {url: PathItem(**item) for url, item in v.items()} for k, v in field_value.items()}
+ elif field_name == 'headers':
+ values[field_name] = {k: Header(**v) for k, v in field_value.items()}
+ else:
+ values[field_name] = cls(**field_value)
+ elif isinstance(field_value, list):
+ values[field_name] = [
+ Reference(**v) if isinstance(v, dict) and '$ref' in v else cls(**v) if isinstance(v, dict) else v
+ for v in field_value
+ ]
+ return values
+
+ # Add the validator to the models
+ for model in (Schema, Operation, Encoding):
+ if not hasattr(model, 'model_validators'):
+ model.model_validators = []
+ if validate_refs not in model.model_validators:
+ model.model_validators.append(validate_refs)
+ # Force model rebuild
+ _model_rebuild(model)
+else:
+ # Use Pydantic v1 model rebuild to handle circular references
+ _model_rebuild(Schema)
+ _model_rebuild(Operation)
+ _model_rebuild(Encoding)
\ No newline at end of file