mirror of
https://github.com/fastapi/fastapi.git
synced 2026-03-04 15:07:18 -05:00
✨ Add support for OpenAPI 3.1.0 (#9770)
* ✨ Update OpenAPI models for JSON Schema 2020-12 and OpenAPI 3.1.0 * ✨ Add support for summary and webhooks * ✨ Update JSON Schema for UploadFiles * ⏪️ Revert making paths optional, to ensure always correctness * ⏪️ Keep UploadFile as format: binary for compatibility with the rest of Pydantic bytes fields in v1 * ✨ Update version of OpenAPI generated to 3.1.0 * ✨ Update the version of Swagger UI * 📝 Update docs about extending OpenAPI * 📝 Update docs and links to refer to OpenAPI 3.1.0 * ✨ Update logic for handling webhooks * ♻️ Update parameter functions and classes, deprecate example and make examples the main field * ✅ Update tests for OpenAPI 3.1.0 * 📝 Update examples for OpenAPI metadata * ✅ Add and update tests for OpenAPI metadata * 📝 Add source example for webhooks * 📝 Update docs for metadata * 📝 Update docs for Schema extra * 📝 Add docs for webhooks * 🔧 Add webhooks docs to MkDocs * ✅ Update tests for extending OpenAPI * ✅ Add tests for webhooks * ♻️ Refactor generation of OpenAPI and JSON Schema with params * 📝 Update source examples for field examples * ✅ Update tests for examples * ➕ Make sure the minimum version of typing-extensions installed has deprecated() (already a dependency of Pydantic) * ✏️ Fix typo in Webhooks example code * 🔥 Remove commented out code of removed nullable field * 🗑️ Add deprecation warnings for example argument * ✅ Update tests to check for deprecation warnings * ✅ Add test for webhooks with security schemes, for coverage * 🍱 Update image for metadata, with new summary * 🍱 Add docs image for Webhooks * 📝 Update docs for webhooks, add docs UI image
This commit is contained in:
committed by
GitHub
parent
02fc9e8a63
commit
7dad5a820b
@@ -17,8 +17,8 @@ def get_swagger_ui_html(
|
||||
*,
|
||||
openapi_url: str,
|
||||
title: str,
|
||||
swagger_js_url: str = "https://cdn.jsdelivr.net/npm/swagger-ui-dist@4/swagger-ui-bundle.js",
|
||||
swagger_css_url: str = "https://cdn.jsdelivr.net/npm/swagger-ui-dist@4/swagger-ui.css",
|
||||
swagger_js_url: str = "https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui-bundle.js",
|
||||
swagger_css_url: str = "https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui.css",
|
||||
swagger_favicon_url: str = "https://fastapi.tiangolo.com/img/favicon.png",
|
||||
oauth2_redirect_url: Optional[str] = None,
|
||||
init_oauth: Optional[Dict[str, Any]] = None,
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
from enum import Enum
|
||||
from typing import Any, Callable, Dict, Iterable, List, Optional, Union
|
||||
from typing import Any, Callable, Dict, Iterable, List, Optional, Set, Union
|
||||
|
||||
from fastapi.logger import logger
|
||||
from pydantic import AnyUrl, BaseModel, Field
|
||||
from typing_extensions import Literal
|
||||
from typing_extensions import Annotated, Literal
|
||||
from typing_extensions import deprecated as typing_deprecated
|
||||
|
||||
try:
|
||||
import email_validator # type: ignore
|
||||
@@ -37,6 +38,7 @@ class Contact(BaseModel):
|
||||
|
||||
class License(BaseModel):
|
||||
name: str
|
||||
identifier: Optional[str] = None
|
||||
url: Optional[AnyUrl] = None
|
||||
|
||||
class Config:
|
||||
@@ -45,6 +47,7 @@ class License(BaseModel):
|
||||
|
||||
class Info(BaseModel):
|
||||
title: str
|
||||
summary: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
termsOfService: Optional[str] = None
|
||||
contact: Optional[Contact] = None
|
||||
@@ -56,7 +59,7 @@ class Info(BaseModel):
|
||||
|
||||
|
||||
class ServerVariable(BaseModel):
|
||||
enum: Optional[List[str]] = None
|
||||
enum: Annotated[Optional[List[str]], Field(min_items=1)] = None
|
||||
default: str
|
||||
description: Optional[str] = None
|
||||
|
||||
@@ -102,9 +105,42 @@ class ExternalDocumentation(BaseModel):
|
||||
|
||||
|
||||
class Schema(BaseModel):
|
||||
# Ref: JSON Schema 2020-12: https://json-schema.org/draft/2020-12/json-schema-core.html#name-the-json-schema-core-vocabu
|
||||
# Core Vocabulary
|
||||
schema_: Optional[str] = Field(default=None, alias="$schema")
|
||||
vocabulary: Optional[str] = Field(default=None, alias="$vocabulary")
|
||||
id: Optional[str] = Field(default=None, alias="$id")
|
||||
anchor: Optional[str] = Field(default=None, alias="$anchor")
|
||||
dynamicAnchor: Optional[str] = Field(default=None, alias="$dynamicAnchor")
|
||||
ref: Optional[str] = Field(default=None, alias="$ref")
|
||||
title: Optional[str] = None
|
||||
multipleOf: Optional[float] = None
|
||||
dynamicRef: Optional[str] = Field(default=None, alias="$dynamicRef")
|
||||
defs: Optional[Dict[str, "Schema"]] = Field(default=None, alias="$defs")
|
||||
comment: Optional[str] = Field(default=None, alias="$comment")
|
||||
# Ref: JSON Schema 2020-12: https://json-schema.org/draft/2020-12/json-schema-core.html#name-a-vocabulary-for-applying-s
|
||||
# A Vocabulary for Applying Subschemas
|
||||
allOf: Optional[List["Schema"]] = None
|
||||
anyOf: Optional[List["Schema"]] = None
|
||||
oneOf: Optional[List["Schema"]] = None
|
||||
not_: Optional["Schema"] = Field(default=None, alias="not")
|
||||
if_: Optional["Schema"] = Field(default=None, alias="if")
|
||||
then: Optional["Schema"] = None
|
||||
else_: Optional["Schema"] = Field(default=None, alias="else")
|
||||
dependentSchemas: Optional[Dict[str, "Schema"]] = None
|
||||
prefixItems: Optional[List["Schema"]] = None
|
||||
items: Optional[Union["Schema", List["Schema"]]] = None
|
||||
contains: Optional["Schema"] = None
|
||||
properties: Optional[Dict[str, "Schema"]] = None
|
||||
patternProperties: Optional[Dict[str, "Schema"]] = None
|
||||
additionalProperties: Optional["Schema"] = None
|
||||
propertyNames: Optional["Schema"] = None
|
||||
unevaluatedItems: Optional["Schema"] = None
|
||||
unevaluatedProperties: Optional["Schema"] = None
|
||||
# Ref: JSON Schema Validation 2020-12: https://json-schema.org/draft/2020-12/json-schema-validation.html#name-a-vocabulary-for-structural
|
||||
# A Vocabulary for Structural Validation
|
||||
type: Optional[str] = None
|
||||
enum: Optional[List[Any]] = None
|
||||
const: Optional[Any] = None
|
||||
multipleOf: Optional[float] = Field(default=None, gt=0)
|
||||
maximum: Optional[float] = None
|
||||
exclusiveMaximum: Optional[float] = None
|
||||
minimum: Optional[float] = None
|
||||
@@ -115,29 +151,41 @@ class Schema(BaseModel):
|
||||
maxItems: Optional[int] = Field(default=None, ge=0)
|
||||
minItems: Optional[int] = Field(default=None, ge=0)
|
||||
uniqueItems: Optional[bool] = None
|
||||
maxContains: Optional[int] = Field(default=None, ge=0)
|
||||
minContains: Optional[int] = Field(default=None, ge=0)
|
||||
maxProperties: Optional[int] = Field(default=None, ge=0)
|
||||
minProperties: Optional[int] = Field(default=None, ge=0)
|
||||
required: Optional[List[str]] = None
|
||||
enum: Optional[List[Any]] = None
|
||||
type: Optional[str] = None
|
||||
allOf: Optional[List["Schema"]] = None
|
||||
oneOf: Optional[List["Schema"]] = None
|
||||
anyOf: Optional[List["Schema"]] = None
|
||||
not_: Optional["Schema"] = Field(default=None, alias="not")
|
||||
items: Optional[Union["Schema", List["Schema"]]] = None
|
||||
properties: Optional[Dict[str, "Schema"]] = None
|
||||
additionalProperties: Optional[Union["Schema", Reference, bool]] = None
|
||||
description: Optional[str] = None
|
||||
dependentRequired: Optional[Dict[str, Set[str]]] = None
|
||||
# Ref: JSON Schema Validation 2020-12: https://json-schema.org/draft/2020-12/json-schema-validation.html#name-vocabularies-for-semantic-c
|
||||
# Vocabularies for Semantic Content With "format"
|
||||
format: Optional[str] = None
|
||||
# Ref: JSON Schema Validation 2020-12: https://json-schema.org/draft/2020-12/json-schema-validation.html#name-a-vocabulary-for-the-conten
|
||||
# A Vocabulary for the Contents of String-Encoded Data
|
||||
contentEncoding: Optional[str] = None
|
||||
contentMediaType: Optional[str] = None
|
||||
contentSchema: Optional["Schema"] = None
|
||||
# Ref: JSON Schema Validation 2020-12: https://json-schema.org/draft/2020-12/json-schema-validation.html#name-a-vocabulary-for-basic-meta
|
||||
# A Vocabulary for Basic Meta-Data Annotations
|
||||
title: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
default: Optional[Any] = None
|
||||
nullable: Optional[bool] = None
|
||||
discriminator: Optional[Discriminator] = None
|
||||
deprecated: Optional[bool] = None
|
||||
readOnly: Optional[bool] = None
|
||||
writeOnly: Optional[bool] = None
|
||||
examples: Optional[List[Any]] = None
|
||||
# Ref: OpenAPI 3.1.0: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#schema-object
|
||||
# Schema Object
|
||||
discriminator: Optional[Discriminator] = None
|
||||
xml: Optional[XML] = None
|
||||
externalDocs: Optional[ExternalDocumentation] = None
|
||||
example: Optional[Any] = None
|
||||
deprecated: Optional[bool] = None
|
||||
example: Annotated[
|
||||
Optional[Any],
|
||||
typing_deprecated(
|
||||
"Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, "
|
||||
"although still supported. Use examples instead."
|
||||
),
|
||||
] = None
|
||||
|
||||
class Config:
|
||||
extra: str = "allow"
|
||||
@@ -248,7 +296,7 @@ class Operation(BaseModel):
|
||||
parameters: Optional[List[Union[Parameter, Reference]]] = None
|
||||
requestBody: Optional[Union[RequestBody, Reference]] = None
|
||||
# Using Any for Specification Extensions
|
||||
responses: Dict[str, Union[Response, Any]]
|
||||
responses: Optional[Dict[str, Union[Response, Any]]] = None
|
||||
callbacks: Optional[Dict[str, Union[Dict[str, "PathItem"], Reference]]] = None
|
||||
deprecated: Optional[bool] = None
|
||||
security: Optional[List[Dict[str, List[str]]]] = None
|
||||
@@ -375,6 +423,7 @@ class Components(BaseModel):
|
||||
links: Optional[Dict[str, Union[Link, Reference]]] = None
|
||||
# Using Any for Specification Extensions
|
||||
callbacks: Optional[Dict[str, Union[Dict[str, PathItem], Reference, Any]]] = None
|
||||
pathItems: Optional[Dict[str, Union[PathItem, Reference]]] = None
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
@@ -392,9 +441,11 @@ class Tag(BaseModel):
|
||||
class OpenAPI(BaseModel):
|
||||
openapi: str
|
||||
info: Info
|
||||
jsonSchemaDialect: Optional[str] = None
|
||||
servers: Optional[List[Server]] = None
|
||||
# Using Any for Specification Extensions
|
||||
paths: Dict[str, Union[PathItem, Any]]
|
||||
paths: Optional[Dict[str, Union[PathItem, Any]]] = None
|
||||
webhooks: Optional[Dict[str, Union[PathItem, Reference]]] = None
|
||||
components: Optional[Components] = None
|
||||
security: Optional[List[Dict[str, List[str]]]] = None
|
||||
tags: Optional[List[Tag]] = None
|
||||
|
||||
@@ -106,9 +106,7 @@ def get_openapi_operation_parameters(
|
||||
}
|
||||
if field_info.description:
|
||||
parameter["description"] = field_info.description
|
||||
if field_info.examples:
|
||||
parameter["examples"] = jsonable_encoder(field_info.examples)
|
||||
elif field_info.example != Undefined:
|
||||
if field_info.example != Undefined:
|
||||
parameter["example"] = jsonable_encoder(field_info.example)
|
||||
if field_info.deprecated:
|
||||
parameter["deprecated"] = field_info.deprecated
|
||||
@@ -134,9 +132,7 @@ def get_openapi_operation_request_body(
|
||||
if required:
|
||||
request_body_oai["required"] = required
|
||||
request_media_content: Dict[str, Any] = {"schema": body_schema}
|
||||
if field_info.examples:
|
||||
request_media_content["examples"] = jsonable_encoder(field_info.examples)
|
||||
elif field_info.example != Undefined:
|
||||
if field_info.example != Undefined:
|
||||
request_media_content["example"] = jsonable_encoder(field_info.example)
|
||||
request_body_oai["content"] = {request_media_type: request_media_content}
|
||||
return request_body_oai
|
||||
@@ -392,9 +388,11 @@ def get_openapi(
|
||||
*,
|
||||
title: str,
|
||||
version: str,
|
||||
openapi_version: str = "3.0.2",
|
||||
openapi_version: str = "3.1.0",
|
||||
summary: Optional[str] = None,
|
||||
description: Optional[str] = None,
|
||||
routes: Sequence[BaseRoute],
|
||||
webhooks: Optional[Sequence[BaseRoute]] = None,
|
||||
tags: Optional[List[Dict[str, Any]]] = None,
|
||||
servers: Optional[List[Dict[str, Union[str, Any]]]] = None,
|
||||
terms_of_service: Optional[str] = None,
|
||||
@@ -402,6 +400,8 @@ def get_openapi(
|
||||
license_info: Optional[Dict[str, Union[str, Any]]] = None,
|
||||
) -> Dict[str, Any]:
|
||||
info: Dict[str, Any] = {"title": title, "version": version}
|
||||
if summary:
|
||||
info["summary"] = summary
|
||||
if description:
|
||||
info["description"] = description
|
||||
if terms_of_service:
|
||||
@@ -415,13 +415,14 @@ def get_openapi(
|
||||
output["servers"] = servers
|
||||
components: Dict[str, Dict[str, Any]] = {}
|
||||
paths: Dict[str, Dict[str, Any]] = {}
|
||||
webhook_paths: Dict[str, Dict[str, Any]] = {}
|
||||
operation_ids: Set[str] = set()
|
||||
flat_models = get_flat_models_from_routes(routes)
|
||||
flat_models = get_flat_models_from_routes(list(routes or []) + list(webhooks or []))
|
||||
model_name_map = get_model_name_map(flat_models)
|
||||
definitions = get_model_definitions(
|
||||
flat_models=flat_models, model_name_map=model_name_map
|
||||
)
|
||||
for route in routes:
|
||||
for route in routes or []:
|
||||
if isinstance(route, routing.APIRoute):
|
||||
result = get_openapi_path(
|
||||
route=route, model_name_map=model_name_map, operation_ids=operation_ids
|
||||
@@ -436,11 +437,30 @@ def get_openapi(
|
||||
)
|
||||
if path_definitions:
|
||||
definitions.update(path_definitions)
|
||||
for webhook in webhooks or []:
|
||||
if isinstance(webhook, routing.APIRoute):
|
||||
result = get_openapi_path(
|
||||
route=webhook,
|
||||
model_name_map=model_name_map,
|
||||
operation_ids=operation_ids,
|
||||
)
|
||||
if result:
|
||||
path, security_schemes, path_definitions = result
|
||||
if path:
|
||||
webhook_paths.setdefault(webhook.path_format, {}).update(path)
|
||||
if security_schemes:
|
||||
components.setdefault("securitySchemes", {}).update(
|
||||
security_schemes
|
||||
)
|
||||
if path_definitions:
|
||||
definitions.update(path_definitions)
|
||||
if definitions:
|
||||
components["schemas"] = {k: definitions[k] for k in sorted(definitions)}
|
||||
if components:
|
||||
output["components"] = components
|
||||
output["paths"] = paths
|
||||
if webhook_paths:
|
||||
output["webhooks"] = webhook_paths
|
||||
if tags:
|
||||
output["tags"] = tags
|
||||
return jsonable_encoder(OpenAPI(**output), by_alias=True, exclude_none=True) # type: ignore
|
||||
|
||||
Reference in New Issue
Block a user