diff --git a/README.md b/README.md index 2769cce..2fab3c7 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ open_api = OpenAPI( ) }, ) -print(open_api.json(by_alias=True, exclude_none=True, indent=2)) +print(open_api.model_dump_json(by_alias=True, exclude_none=True, indent=2)) ``` Result: @@ -69,15 +69,14 @@ Result: ## Take advantage of Pydantic -Pydantic is a great tool, allow you to use object / dict / mixed data for for input. - +Pydantic is a great tool. It allows you to use object / dict / mixed data for input. The following examples give the same OpenAPI result as above: ```python from openapi_schema_pydantic import OpenAPI, PathItem, Response # Construct OpenAPI from dict -open_api = OpenAPI.parse_obj({ +open_api = OpenAPI.model_validate({ "info": {"title": "My own API", "version": "v0.0.1"}, "paths": { "/ping": { @@ -87,7 +86,7 @@ open_api = OpenAPI.parse_obj({ }) # Construct OpenAPI with mix of dict/object -open_api = OpenAPI.parse_obj({ +open_api = OpenAPI.model_validate({ "info": {"title": "My own API", "version": "v0.0.1"}, "paths": { "/ping": PathItem( @@ -100,10 +99,10 @@ open_api = OpenAPI.parse_obj({ ## Use Pydantic classes as schema - The [Schema Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#schemaObject) - in OpenAPI has definitions and tweaks in JSON Schema, which is hard to comprehend and define a good data class + in OpenAPI has definitions and tweaks in JSON Schema, which are hard to comprehend and define a good data class - Pydantic already has a good way to [create JSON schema](https://pydantic-docs.helpmanual.io/usage/schema/), let's not re-invent the wheel - + The approach to deal with this: 1. Use `PydanticSchema` objects to represent the `Schema` in `OpenAPI` object @@ -116,7 +115,7 @@ from openapi_schema_pydantic import OpenAPI from openapi_schema_pydantic.util import PydanticSchema, construct_open_api_with_schema_class def construct_base_open_api() -> OpenAPI: - return OpenAPI.parse_obj({ + return OpenAPI.model_validate({ "info": {"title": "My own API", "version": "v0.0.1"}, "paths": { "/ping": { @@ -148,8 +147,8 @@ class PingResponse(BaseModel): open_api = construct_base_open_api() open_api = construct_open_api_with_schema_class(open_api) -# print the result openapi.json -print(open_api.json(by_alias=True, exclude_none=True, indent=2)) +# print the result of openapi.model_dump_json() +print(open_api.model_dump_json(by_alias=True, exclude_none=True, indent=2)) ``` Result: @@ -198,45 +197,45 @@ Result: "components": { "schemas": { "PingRequest": { - "title": "PingRequest", - "required": [ - "req_foo", - "req_bar" - ], - "type": "object", "properties": { "req_foo": { - "title": "Req Foo", "type": "string", + "title": "Req Foo", "description": "foo value of the request" }, "req_bar": { - "title": "Req Bar", "type": "string", + "title": "Req Bar", "description": "bar value of the request" } }, + "type": "object", + "required": [ + "req_foo", + "req_bar" + ], + "title": "PingRequest", "description": "Ping Request" }, "PingResponse": { - "title": "PingResponse", - "required": [ - "resp_foo", - "resp_bar" - ], - "type": "object", "properties": { "resp_foo": { - "title": "Resp Foo", "type": "string", + "title": "Resp Foo", "description": "foo value of the response" }, "resp_bar": { - "title": "Resp Bar", "type": "string", + "title": "Resp Bar", "description": "bar value of the response" } }, + "type": "object", + "required": [ + "resp_foo", + "resp_bar" + ], + "title": "PingResponse", "description": "Ping response" } } @@ -246,21 +245,21 @@ Result: ## Notes -### Use of OpenAPI.json() / OpenAPI.dict() +### Use of OpenAPI.model_dump_json() / OpenAPI.model_dump() -When using `OpenAPI.json()` / `OpenAPI.dict()` function, -arguments `by_alias=True, exclude_none=True` has to be in place. -Otherwise the result json will not fit the OpenAPI standard. +When using `OpenAPI.model_dump_json()` / `OpenAPI.model_dump()` functions, +the arguments `by_alias=True, exclude_none=True` have to be in place, +otherwise the resulting json will not fit the OpenAPI standard. ```python # OK -open_api.json(by_alias=True, exclude_none=True, indent=2) +open_api.model_dump_json(by_alias=True, exclude_none=True, indent=2) # Not good -open_api.json(indent=2) +open_api.model_dump_json(indent=2) ``` -More info about field alias: +More info about field aliases: | OpenAPI version | Field alias info | | --------------- | ---------------- | @@ -280,7 +279,7 @@ Please refer to the following for more info: ### Use OpenAPI 3.0.3 instead of 3.1.0 Some UI renderings (e.g. Swagger) still do not support OpenAPI 3.1.0. -It is allowed to use the old 3.0.3 version by importing from different paths: +The old 3.0.3 version is available by importing from different paths: ```python from openapi_schema_pydantic.v3.v3_0_3 import OpenAPI, ... diff --git a/openapi_schema_pydantic/util.py b/openapi_schema_pydantic/util.py index cb57969..240d5a6 100644 --- a/openapi_schema_pydantic/util.py +++ b/openapi_schema_pydantic/util.py @@ -1,27 +1,34 @@ import logging -from typing import Any, List, Set, Type, TypeVar +from typing import Any, List, Set, Type -from pydantic import BaseModel -from pydantic.schema import schema +from pydantic import BaseModel, create_model +from pydantic.json_schema import models_json_schema, JsonSchemaMode from . import Components, OpenAPI, Reference, Schema logger = logging.getLogger(__name__) -PydanticType = TypeVar("PydanticType", bound=BaseModel) +PydanticType = BaseModel ref_prefix = "#/components/schemas/" +ref_template = ref_prefix + "{model}" class PydanticSchema(Schema): """Special `Schema` class to indicate a reference from pydantic class""" - schema_class: Type[PydanticType] = ... + schema_class: Type[BaseModel] """the class that is used for generate the schema""" +def get_mode(cls: Type[BaseModel], default: JsonSchemaMode = "validation") -> JsonSchemaMode: + if not hasattr(cls, "model_config"): + return default + return cls.model_config.get("json_schema_mode", default) + + def construct_open_api_with_schema_class( open_api: OpenAPI, - schema_classes: List[Type[PydanticType]] = None, + schema_classes: List[Type[BaseModel]] | None = None, scan_for_pydantic_schema_reference: bool = True, by_alias: bool = True, ) -> OpenAPI: @@ -36,7 +43,7 @@ def construct_open_api_with_schema_class( :return: new OpenAPI object with "#/components/schemas" values updated. If there is no update in "#/components/schemas" values, the original `open_api` will be returned. """ - new_open_api: OpenAPI = open_api.copy(deep=True) + new_open_api: OpenAPI = open_api.model_copy(deep=True) if scan_for_pydantic_schema_reference: extracted_schema_classes = _handle_pydantic_schema(new_open_api) if schema_classes: @@ -51,27 +58,31 @@ def construct_open_api_with_schema_class( logger.debug(f"schema_classes{schema_classes}") # update new_open_api with new #/components/schemas - schema_definitions = schema(schema_classes, by_alias=by_alias, ref_prefix=ref_prefix) + key_map, schema_definitions = models_json_schema( + [(c, get_mode(c)) for c in schema_classes], + by_alias=by_alias, + ref_template=ref_template, + ) if not new_open_api.components: new_open_api.components = Components() if new_open_api.components.schemas: for existing_key in new_open_api.components.schemas: - if existing_key in schema_definitions.get("definitions"): + if existing_key in schema_definitions["$defs"]: logger.warning( f'"{existing_key}" already exists in {ref_prefix}. ' f'The value of "{ref_prefix}{existing_key}" will be overwritten.' ) new_open_api.components.schemas.update( - {key: Schema.parse_obj(schema_dict) for key, schema_dict in schema_definitions.get("definitions").items()} + {key: Schema.model_validate(schema_dict) for key, schema_dict in schema_definitions["$defs"].items()} ) else: new_open_api.components.schemas = { - key: Schema.parse_obj(schema_dict) for key, schema_dict in schema_definitions.get("definitions").items() + key: Schema.model_validate(schema_dict) for key, schema_dict in schema_definitions["$defs"].items() } return new_open_api -def _handle_pydantic_schema(open_api: OpenAPI) -> List[Type[PydanticType]]: +def _handle_pydantic_schema(open_api: OpenAPI) -> List[Type[BaseModel]]: """ This function traverses the `OpenAPI` object and @@ -84,11 +95,11 @@ def _handle_pydantic_schema(open_api: OpenAPI) -> List[Type[PydanticType]]: :return: a list of schema classes extracted from `PydanticSchema` objects """ - pydantic_types: Set[Type[PydanticType]] = set() + pydantic_types: Set[Type[BaseModel]] = set() def _traverse(obj: Any): if isinstance(obj, BaseModel): - fields = obj.__fields_set__ + fields = obj.model_fields_set for field in fields: child_obj = obj.__getattribute__(field) if isinstance(child_obj, PydanticSchema): diff --git a/openapi_schema_pydantic/v3/v3_0_3/README.md b/openapi_schema_pydantic/v3/v3_0_3/README.md index d5fae02..4a5a325 100644 --- a/openapi_schema_pydantic/v3/v3_0_3/README.md +++ b/openapi_schema_pydantic/v3/v3_0_3/README.md @@ -20,7 +20,7 @@ the following fields are used with [alias](https://pydantic-docs.helpmanual.io/u > The "in" field in Header object is actually a constant (`{"in": "header"}`). > For convenience of object creation, the classes mentioned in above -> has configured `allow_population_by_field_name=True`. +> has configured `populate_by_name=True`. > > Reference: [Pydantic's Model Config](https://pydantic-docs.helpmanual.io/usage/model_config/) diff --git a/openapi_schema_pydantic/v3/v3_0_3/__init__.py b/openapi_schema_pydantic/v3/v3_0_3/__init__.py index 666664a..1cb9758 100644 --- a/openapi_schema_pydantic/v3/v3_0_3/__init__.py +++ b/openapi_schema_pydantic/v3/v3_0_3/__init__.py @@ -39,5 +39,7 @@ # resolve forward references -Encoding.update_forward_refs(Header=Header) -Schema.update_forward_refs() +Encoding.model_rebuild() +OpenAPI.model_rebuild() +Components.model_rebuild() +Operation.model_rebuild() diff --git a/openapi_schema_pydantic/v3/v3_0_3/components.py b/openapi_schema_pydantic/v3/v3_0_3/components.py index 9fab074..1ffd017 100644 --- a/openapi_schema_pydantic/v3/v3_0_3/components.py +++ b/openapi_schema_pydantic/v3/v3_0_3/components.py @@ -1,6 +1,6 @@ from typing import Dict, Optional, Union -from pydantic import BaseModel, Extra +from pydantic import BaseModel, ConfigDict from .callback import Callback from .example import Example @@ -48,9 +48,9 @@ class Components(BaseModel): callbacks: Optional[Dict[str, Union[Callback, Reference]]] = None """An object to hold reusable [Callback Objects](#callbackObject).""" - class Config: - extra = Extra.ignore - schema_extra = { + model_config = ConfigDict( + extra="ignore", + json_schema_extra={ "examples": [ { "schemas": { @@ -111,4 +111,5 @@ class Config: }, } ] - } + }, + ) diff --git a/openapi_schema_pydantic/v3/v3_0_3/contact.py b/openapi_schema_pydantic/v3/v3_0_3/contact.py index 2bda129..e504bd4 100644 --- a/openapi_schema_pydantic/v3/v3_0_3/contact.py +++ b/openapi_schema_pydantic/v3/v3_0_3/contact.py @@ -1,6 +1,6 @@ from typing import Optional -from pydantic import AnyUrl, BaseModel, Extra +from pydantic import AnyUrl, BaseModel, ConfigDict class Contact(BaseModel): @@ -25,10 +25,11 @@ class Contact(BaseModel): MUST be in the format of an email address. """ - class Config: - extra = Extra.ignore - schema_extra = { + model_config = ConfigDict( + extra="ignore", + json_schema_extra={ "examples": [ {"name": "API Support", "url": "http://www.example.com/support", "email": "support@example.com"} ] - } + }, + ) diff --git a/openapi_schema_pydantic/v3/v3_0_3/discriminator.py b/openapi_schema_pydantic/v3/v3_0_3/discriminator.py index 699d4ea..95c15fa 100644 --- a/openapi_schema_pydantic/v3/v3_0_3/discriminator.py +++ b/openapi_schema_pydantic/v3/v3_0_3/discriminator.py @@ -1,6 +1,6 @@ from typing import Dict, Optional -from pydantic import BaseModel, Extra +from pydantic import BaseModel, Field, ConfigDict class Discriminator(BaseModel): @@ -14,7 +14,7 @@ class Discriminator(BaseModel): When using the discriminator, _inline_ schemas will not be considered. """ - propertyName: str = ... + propertyName: str """ **REQUIRED**. The name of the property in the payload that will hold the discriminator value. """ @@ -24,9 +24,9 @@ class Discriminator(BaseModel): An object to hold mappings between payload values and schema names or references. """ - class Config: - extra = Extra.ignore - schema_extra = { + model_config = ConfigDict( + extra="ignore", + json_schema_extra={ "examples": [ { "propertyName": "petType", @@ -36,4 +36,5 @@ class Config: }, } ] - } + }, + ) diff --git a/openapi_schema_pydantic/v3/v3_0_3/encoding.py b/openapi_schema_pydantic/v3/v3_0_3/encoding.py index 2821da4..21c40bf 100644 --- a/openapi_schema_pydantic/v3/v3_0_3/encoding.py +++ b/openapi_schema_pydantic/v3/v3_0_3/encoding.py @@ -1,6 +1,6 @@ from typing import Dict, Optional, Union -from pydantic import BaseModel, Extra +from pydantic import BaseModel, ConfigDict from .reference import Reference @@ -59,9 +59,9 @@ class Encoding(BaseModel): This property SHALL be ignored if the request body media type is not `application/x-www-form-urlencoded`. """ - class Config: - extra = Extra.ignore - schema_extra = { + model_config = ConfigDict( + extra="ignore", + json_schema_extra={ "examples": [ { "contentType": "image/png, image/jpeg", @@ -73,4 +73,5 @@ class Config: }, } ] - } + }, + ) diff --git a/openapi_schema_pydantic/v3/v3_0_3/example.py b/openapi_schema_pydantic/v3/v3_0_3/example.py index 1d98bb1..46a73b7 100644 --- a/openapi_schema_pydantic/v3/v3_0_3/example.py +++ b/openapi_schema_pydantic/v3/v3_0_3/example.py @@ -1,10 +1,9 @@ from typing import Any, Optional -from pydantic import BaseModel, Extra +from pydantic import BaseModel, ConfigDict class Example(BaseModel): - summary: Optional[str] = None """ Short description for the example. @@ -32,9 +31,9 @@ class Example(BaseModel): The `value` field and `externalValue` field are mutually exclusive. """ - class Config: - extra = Extra.ignore - schema_extra = { + model_config = ConfigDict( + extra="ignore", + json_schema_extra={ "examples": [ {"summary": "A foo example", "value": {"foo": "bar"}}, { @@ -43,4 +42,5 @@ class Config: }, {"summary": "This is a text example", "externalValue": "http://foo.bar/examples/address-example.txt"}, ] - } + }, + ) diff --git a/openapi_schema_pydantic/v3/v3_0_3/external_documentation.py b/openapi_schema_pydantic/v3/v3_0_3/external_documentation.py index 599778c..8cb873f 100644 --- a/openapi_schema_pydantic/v3/v3_0_3/external_documentation.py +++ b/openapi_schema_pydantic/v3/v3_0_3/external_documentation.py @@ -1,6 +1,6 @@ from typing import Optional -from pydantic import AnyUrl, BaseModel, Extra +from pydantic import AnyUrl, BaseModel, ConfigDict class ExternalDocumentation(BaseModel): @@ -12,12 +12,13 @@ class ExternalDocumentation(BaseModel): [CommonMark syntax](https://spec.commonmark.org/) MAY be used for rich text representation. """ - url: AnyUrl = ... + url: AnyUrl """ **REQUIRED**. The URL for the target documentation. Value MUST be in the format of a URL. """ - class Config: - extra = Extra.ignore - schema_extra = {"examples": [{"description": "Find more info here", "url": "https://example.com"}]} + model_config = ConfigDict( + extra="ignore", + json_schema_extra={"examples": [{"description": "Find more info here", "url": "https://example.com"}]}, + ) diff --git a/openapi_schema_pydantic/v3/v3_0_3/header.py b/openapi_schema_pydantic/v3/v3_0_3/header.py index c413cdc..1e078ee 100644 --- a/openapi_schema_pydantic/v3/v3_0_3/header.py +++ b/openapi_schema_pydantic/v3/v3_0_3/header.py @@ -1,4 +1,5 @@ -from pydantic import Extra, Field +from pydantic import ConfigDict, Field +from typing import Literal from .parameter import Parameter @@ -13,14 +14,15 @@ class Header(Parameter): (for example, [`style`](#parameterStyle)). """ - name = Field(default="", const=True) - param_in = Field(default="header", const=True, alias="in") + name: Literal[""] = Field(default="") + param_in: Literal["header"] = Field(default="header", alias="in") - class Config: - extra = Extra.ignore - allow_population_by_field_name = True - schema_extra = { + model_config = ConfigDict( + extra="ignore", + populate_by_name=True, + json_schema_extra={ "examples": [ {"description": "The number of allowed requests in the current period", "schema": {"type": "integer"}} ] - } + }, + ) diff --git a/openapi_schema_pydantic/v3/v3_0_3/info.py b/openapi_schema_pydantic/v3/v3_0_3/info.py index ee12e09..f125045 100644 --- a/openapi_schema_pydantic/v3/v3_0_3/info.py +++ b/openapi_schema_pydantic/v3/v3_0_3/info.py @@ -1,6 +1,6 @@ from typing import Optional -from pydantic import AnyUrl, BaseModel, Extra +from pydantic import AnyUrl, BaseModel, ConfigDict from .contact import Contact from .license import License @@ -13,7 +13,7 @@ class Info(BaseModel): and MAY be presented in editing or documentation generation tools for convenience. """ - title: str = ... + title: str """ **REQUIRED**. The title of the API. """ @@ -40,15 +40,15 @@ class Info(BaseModel): The license information for the exposed API. """ - version: str = ... + version: str """ **REQUIRED**. The version of the OpenAPI document (which is distinct from the [OpenAPI Specification version](#oasVersion) or the API implementation version). """ - class Config: - extra = Extra.ignore - schema_extra = { + model_config = ConfigDict( + extra="ignore", + json_schema_extra={ "examples": [ { "title": "Sample Pet Store App", @@ -63,4 +63,5 @@ class Config: "version": "1.0.1", } ] - } + }, + ) diff --git a/openapi_schema_pydantic/v3/v3_0_3/license.py b/openapi_schema_pydantic/v3/v3_0_3/license.py index 29c97ab..6be0ba7 100644 --- a/openapi_schema_pydantic/v3/v3_0_3/license.py +++ b/openapi_schema_pydantic/v3/v3_0_3/license.py @@ -1,6 +1,6 @@ from typing import Optional -from pydantic import AnyUrl, BaseModel, Extra +from pydantic import AnyUrl, BaseModel, ConfigDict class License(BaseModel): @@ -8,7 +8,7 @@ class License(BaseModel): License information for the exposed API. """ - name: str = ... + name: str """ **REQUIRED**. The license name used for the API. """ @@ -19,6 +19,9 @@ class License(BaseModel): MUST be in the format of a URL. """ - class Config: - extra = Extra.ignore - schema_extra = {"examples": [{"name": "Apache 2.0", "url": "https://www.apache.org/licenses/LICENSE-2.0.html"}]} + model_config = ConfigDict( + extra="ignore", + json_schema_extra={ + "examples": [{"name": "Apache 2.0", "url": "https://www.apache.org/licenses/LICENSE-2.0.html"}] + }, + ) diff --git a/openapi_schema_pydantic/v3/v3_0_3/link.py b/openapi_schema_pydantic/v3/v3_0_3/link.py index 5afd529..1361332 100644 --- a/openapi_schema_pydantic/v3/v3_0_3/link.py +++ b/openapi_schema_pydantic/v3/v3_0_3/link.py @@ -1,6 +1,6 @@ from typing import Any, Dict, Optional -from pydantic import BaseModel, Extra +from pydantic import BaseModel, ConfigDict from .server import Server @@ -62,9 +62,9 @@ class Link(BaseModel): A server object to be used by the target operation. """ - class Config: - extra = Extra.ignore - schema_extra = { + model_config = ConfigDict( + extra="ignore", + json_schema_extra={ "examples": [ {"operationId": "getUserAddressByUUID", "parameters": {"userUuid": "$response.body#/uuid"}}, { @@ -72,4 +72,5 @@ class Config: "parameters": {"username": "$response.body#/username"}, }, ] - } + }, + ) diff --git a/openapi_schema_pydantic/v3/v3_0_3/media_type.py b/openapi_schema_pydantic/v3/v3_0_3/media_type.py index babdeab..4c3235b 100644 --- a/openapi_schema_pydantic/v3/v3_0_3/media_type.py +++ b/openapi_schema_pydantic/v3/v3_0_3/media_type.py @@ -1,6 +1,6 @@ from typing import Any, Dict, Optional, Union -from pydantic import BaseModel, Extra, Field +from pydantic import BaseModel, ConfigDict, Field from .encoding import Encoding from .example import Example @@ -48,10 +48,10 @@ class MediaType(BaseModel): when the media type is `multipart` or `application/x-www-form-urlencoded`. """ - class Config: - extra = Extra.ignore - allow_population_by_field_name = True - schema_extra = { + model_config = ConfigDict( + extra="ignore", + populate_by_name=True, + json_schema_extra={ "examples": [ { "schema": {"$ref": "#/components/schemas/Pet"}, @@ -79,4 +79,5 @@ class Config: }, } ] - } + }, + ) diff --git a/openapi_schema_pydantic/v3/v3_0_3/oauth_flow.py b/openapi_schema_pydantic/v3/v3_0_3/oauth_flow.py index ce76309..ba4008a 100644 --- a/openapi_schema_pydantic/v3/v3_0_3/oauth_flow.py +++ b/openapi_schema_pydantic/v3/v3_0_3/oauth_flow.py @@ -1,6 +1,6 @@ from typing import Dict, Optional, Union -from pydantic import AnyUrl, BaseModel, Extra +from pydantic import AnyUrl, BaseModel, ConfigDict class OAuthFlow(BaseModel): @@ -27,16 +27,16 @@ class OAuthFlow(BaseModel): The URL to be used for obtaining refresh tokens. This MUST be in the form of a URL. """ - scopes: Dict[str, str] = ... + scopes: Dict[str, str] """ **REQUIRED**. The available scopes for the OAuth2 security scheme. A map between the scope name and a short description for it. The map MAY be empty. """ - class Config: - extra = Extra.ignore - schema_extra = { + model_config = ConfigDict( + extra="ignore", + json_schema_extra={ "examples": [ { "authorizationUrl": "https://example.com/api/oauth/dialog", @@ -54,4 +54,5 @@ class Config: "scopes": {"write:pets": "modify pets in your account", "read:pets": "read your pets"}, }, ] - } + }, + ) diff --git a/openapi_schema_pydantic/v3/v3_0_3/oauth_flows.py b/openapi_schema_pydantic/v3/v3_0_3/oauth_flows.py index c60a8ff..edc48ab 100644 --- a/openapi_schema_pydantic/v3/v3_0_3/oauth_flows.py +++ b/openapi_schema_pydantic/v3/v3_0_3/oauth_flows.py @@ -1,6 +1,6 @@ from typing import Optional -from pydantic import BaseModel, Extra +from pydantic import BaseModel, ConfigDict from .oauth_flow import OAuthFlow @@ -34,5 +34,4 @@ class OAuthFlows(BaseModel): Previously called `accessCode` in OpenAPI 2.0. """ - class Config: - extra = Extra.ignore + model_config = ConfigDict(extra="ignore") diff --git a/openapi_schema_pydantic/v3/v3_0_3/open_api.py b/openapi_schema_pydantic/v3/v3_0_3/open_api.py index 6b332e8..27b7200 100644 --- a/openapi_schema_pydantic/v3/v3_0_3/open_api.py +++ b/openapi_schema_pydantic/v3/v3_0_3/open_api.py @@ -1,6 +1,6 @@ from typing import List, Optional -from pydantic import BaseModel, Extra +from pydantic import BaseModel, ConfigDict from .components import Components from .external_documentation import ExternalDocumentation @@ -22,7 +22,7 @@ class OpenAPI(BaseModel): This is *not* related to the API [`info.version`](#infoVersion) string. """ - info: Info = ... + info: Info """ **REQUIRED**. Provides metadata about the API. The metadata MAY be used by tooling as required. """ @@ -34,7 +34,7 @@ class OpenAPI(BaseModel): the default value would be a [Server Object](#serverObject) with a [url](#serverUrl) value of `/`. """ - paths: Paths = ... + paths: Paths """ **REQUIRED**. The available paths and operations for the API. """ @@ -67,5 +67,4 @@ class OpenAPI(BaseModel): Additional external documentation. """ - class Config: - extra = Extra.ignore + model_config = ConfigDict(extra="ignore") diff --git a/openapi_schema_pydantic/v3/v3_0_3/operation.py b/openapi_schema_pydantic/v3/v3_0_3/operation.py index aa28a44..19f08ad 100644 --- a/openapi_schema_pydantic/v3/v3_0_3/operation.py +++ b/openapi_schema_pydantic/v3/v3_0_3/operation.py @@ -1,6 +1,6 @@ from typing import Dict, List, Optional, Union -from pydantic import BaseModel, Extra +from pydantic import BaseModel, ConfigDict from .callback import Callback from .external_documentation import ExternalDocumentation @@ -66,7 +66,7 @@ class Operation(BaseModel): In other cases where the HTTP spec is vague, `requestBody` SHALL be ignored by consumers. """ - responses: Responses = ... + responses: Responses """ **REQUIRED**. The list of possible responses as they are returned from executing this operation. """ @@ -103,9 +103,9 @@ class Operation(BaseModel): it will be overridden by this value. """ - class Config: - extra = Extra.ignore - schema_extra = { + model_config = ConfigDict( + extra="ignore", + json_schema_extra={ "examples": [ { "tags": ["pet"], @@ -147,4 +147,5 @@ class Config: "security": [{"petstore_auth": ["write:pets", "read:pets"]}], } ] - } + }, + ) diff --git a/openapi_schema_pydantic/v3/v3_0_3/parameter.py b/openapi_schema_pydantic/v3/v3_0_3/parameter.py index 0f72df3..093b02e 100644 --- a/openapi_schema_pydantic/v3/v3_0_3/parameter.py +++ b/openapi_schema_pydantic/v3/v3_0_3/parameter.py @@ -1,6 +1,6 @@ from typing import Any, Dict, Optional, Union -from pydantic import BaseModel, Field, Extra +from pydantic import BaseModel, Field, ConfigDict from .example import Example from .media_type import MediaType @@ -17,7 +17,7 @@ class Parameter(BaseModel): """Fixed Fields""" - name: str = ... + name: str """ **REQUIRED**. The name of the parameter. Parameter names are *case sensitive*. @@ -140,10 +140,10 @@ class Parameter(BaseModel): The map MUST only contain one entry. """ - class Config: - extra = Extra.ignore - allow_population_by_field_name = True - schema_extra = { + model_config = ConfigDict( + extra="ignore", + populate_by_name=True, + json_schema_extra={ "examples": [ { "name": "token", @@ -189,4 +189,5 @@ class Config: }, }, ] - } + }, + ) diff --git a/openapi_schema_pydantic/v3/v3_0_3/path_item.py b/openapi_schema_pydantic/v3/v3_0_3/path_item.py index 4431ca2..45bac54 100644 --- a/openapi_schema_pydantic/v3/v3_0_3/path_item.py +++ b/openapi_schema_pydantic/v3/v3_0_3/path_item.py @@ -1,6 +1,6 @@ from typing import List, Optional, Union -from pydantic import BaseModel, Extra, Field +from pydantic import BaseModel, ConfigDict, Field from .operation import Operation from .parameter import Parameter @@ -91,10 +91,10 @@ class PathItem(BaseModel): [OpenAPI Object's components/parameters](#componentsParameters). """ - class Config: - extra = Extra.ignore - allow_population_by_field_name = True - schema_extra = { + model_config = ConfigDict( + extra="ignore", + populate_by_name=True, + json_schema_extra={ "examples": [ { "get": { @@ -126,4 +126,5 @@ class Config: ], } ] - } + }, + ) diff --git a/openapi_schema_pydantic/v3/v3_0_3/reference.py b/openapi_schema_pydantic/v3/v3_0_3/reference.py index 8155daa..747d0d2 100644 --- a/openapi_schema_pydantic/v3/v3_0_3/reference.py +++ b/openapi_schema_pydantic/v3/v3_0_3/reference.py @@ -1,4 +1,4 @@ -from pydantic import BaseModel, Extra, Field +from pydantic import BaseModel, ConfigDict, Field class Reference(BaseModel): @@ -15,9 +15,10 @@ class Reference(BaseModel): ref: str = Field(alias="$ref") """**REQUIRED**. The reference string.""" - class Config: - extra = Extra.ignore - allow_population_by_field_name = True - schema_extra = { + model_config = ConfigDict( + extra="ignore", + populate_by_name=True, + json_schema_extra={ "examples": [{"$ref": "#/components/schemas/Pet"}, {"$ref": "Pet.json"}, {"$ref": "definitions.json#/Pet"}] - } + }, + ) diff --git a/openapi_schema_pydantic/v3/v3_0_3/request_body.py b/openapi_schema_pydantic/v3/v3_0_3/request_body.py index 1bf58ce..6d79e5f 100644 --- a/openapi_schema_pydantic/v3/v3_0_3/request_body.py +++ b/openapi_schema_pydantic/v3/v3_0_3/request_body.py @@ -1,6 +1,6 @@ from typing import Dict, Optional -from pydantic import BaseModel, Extra +from pydantic import BaseModel, ConfigDict from .media_type import MediaType @@ -16,7 +16,7 @@ class RequestBody(BaseModel): [CommonMark syntax](https://spec.commonmark.org/) MAY be used for rich text representation. """ - content: Dict[str, MediaType] = ... + content: Dict[str, MediaType] """ **REQUIRED**. The content of the request body. The key is a media type or [media type range](https://tools.ietf.org/html/rfc7231#appendix-D) @@ -30,9 +30,9 @@ class RequestBody(BaseModel): Determines if the request body is required in the request. Defaults to `false`. """ - class Config: - extra = Extra.ignore - schema_extra = { + model_config = ConfigDict( + extra="ignore", + json_schema_extra={ "examples": [ { "description": "user to add to the system", @@ -78,4 +78,5 @@ class Config: "content": {"text/plain": {"schema": {"type": "array", "items": {"type": "string"}}}}, }, ] - } + }, + ) diff --git a/openapi_schema_pydantic/v3/v3_0_3/response.py b/openapi_schema_pydantic/v3/v3_0_3/response.py index 1f7042c..4a19135 100644 --- a/openapi_schema_pydantic/v3/v3_0_3/response.py +++ b/openapi_schema_pydantic/v3/v3_0_3/response.py @@ -1,6 +1,6 @@ from typing import Dict, Optional, Union -from pydantic import BaseModel, Extra +from pydantic import BaseModel, ConfigDict from .header import Header from .link import Link @@ -14,7 +14,7 @@ class Response(BaseModel): static `links` to operations based on the response. """ - description: str = ... + description: str """ **REQUIRED**. A short description of the response. [CommonMark syntax](https://spec.commonmark.org/) MAY be used for rich text representation. @@ -43,9 +43,9 @@ class Response(BaseModel): following the naming constraints of the names for [Component Objects](#componentsObject). """ - class Config: - extra = Extra.ignore - schema_extra = { + model_config = ConfigDict( + extra="ignore", + json_schema_extra={ "examples": [ { "description": "A complex object array response", @@ -76,4 +76,5 @@ class Config: }, {"description": "object created"}, ] - } + }, + ) diff --git a/openapi_schema_pydantic/v3/v3_0_3/schema.py b/openapi_schema_pydantic/v3/v3_0_3/schema.py index d29bd36..5428efe 100644 --- a/openapi_schema_pydantic/v3/v3_0_3/schema.py +++ b/openapi_schema_pydantic/v3/v3_0_3/schema.py @@ -1,6 +1,6 @@ from typing import Any, Dict, List, Optional, Union -from pydantic import BaseModel, Extra, Field +from pydantic import BaseModel, ConfigDict, Field from .discriminator import Discriminator from .external_documentation import ExternalDocumentation from .reference import Reference @@ -180,7 +180,7 @@ class Schema(BaseModel): value of 0. """ - required: Optional[List[str]] = Field(default=None, min_items=1) + required: Optional[List[str]] = Field(default=None, min_length=1) """ The value of this keyword MUST be an array. This array MUST have at least one element. Elements of this array MUST be strings, and MUST @@ -190,7 +190,7 @@ class Schema(BaseModel): contains all elements in this keyword's array value. """ - enum: Optional[List[Any]] = Field(default=None, min_items=1) + enum: Optional[List[Any]] = Field(default=None, min_length=1) """ The value of this keyword MUST be an array. This array SHOULD have at least one element. Elements in the array SHOULD be unique. @@ -470,10 +470,10 @@ class Schema(BaseModel): Default value is `false`. """ - class Config: - extra = Extra.ignore - allow_population_by_field_name = True - schema_extra = { + model_config = ConfigDict( + extra="ignore", + populate_by_name=True, + json_schema_extra={ "examples": [ {"type": "string", "format": "email"}, { @@ -553,4 +553,5 @@ class Config: ], }, ] - } + }, + ) diff --git a/openapi_schema_pydantic/v3/v3_0_3/security_scheme.py b/openapi_schema_pydantic/v3/v3_0_3/security_scheme.py index df54c64..87962e1 100644 --- a/openapi_schema_pydantic/v3/v3_0_3/security_scheme.py +++ b/openapi_schema_pydantic/v3/v3_0_3/security_scheme.py @@ -1,6 +1,6 @@ from typing import Optional, Union -from pydantic import AnyUrl, BaseModel, Extra, Field +from pydantic import AnyUrl, BaseModel, ConfigDict, Field from .oauth_flows import OAuthFlows @@ -15,7 +15,7 @@ class SecurityScheme(BaseModel): and [OpenID Connect Discovery](https://tools.ietf.org/html/draft-ietf-oauth-discovery-06). """ - type: str = ... + type: str """ **REQUIRED**. The type of the security scheme. Valid values are `"apiKey"`, `"http"`, `"oauth2"`, `"openIdConnect"`. @@ -65,10 +65,10 @@ class SecurityScheme(BaseModel): This MUST be in the form of a URL. """ - class Config: - extra = Extra.ignore - allow_population_by_field_name = True - schema_extra = { + model_config = ConfigDict( + extra="ignore", + populate_by_name=True, + json_schema_extra={ "examples": [ {"type": "http", "scheme": "basic"}, {"type": "apiKey", "name": "api_key", "in": "header"}, @@ -85,4 +85,5 @@ class Config: {"type": "openIdConnect", "openIdConnectUrl": "https://example.com/openIdConnect"}, {"type": "openIdConnect", "openIdConnectUrl": "openIdConnect"}, # #5: allow relative path ] - } + }, + ) diff --git a/openapi_schema_pydantic/v3/v3_0_3/server.py b/openapi_schema_pydantic/v3/v3_0_3/server.py index aeeefd5..1e3371e 100644 --- a/openapi_schema_pydantic/v3/v3_0_3/server.py +++ b/openapi_schema_pydantic/v3/v3_0_3/server.py @@ -1,6 +1,6 @@ from typing import Dict, Optional -from pydantic import BaseModel, Extra +from pydantic import BaseModel, ConfigDict from .server_variable import ServerVariable @@ -8,7 +8,7 @@ class Server(BaseModel): """An object representing a Server.""" - url: str = ... + url: str """ **REQUIRED**. A URL to the target host. @@ -30,9 +30,9 @@ class Server(BaseModel): The value is used for substitution in the server's URL template. """ - class Config: - extra = Extra.ignore - schema_extra = { + model_config = ConfigDict( + extra="ignore", + json_schema_extra={ "examples": [ {"url": "https://development.gigantic-server.com/v1", "description": "Development server"}, { @@ -49,4 +49,5 @@ class Config: }, }, ] - } + }, + ) diff --git a/openapi_schema_pydantic/v3/v3_0_3/server_variable.py b/openapi_schema_pydantic/v3/v3_0_3/server_variable.py index 8808e4a..2e83a59 100644 --- a/openapi_schema_pydantic/v3/v3_0_3/server_variable.py +++ b/openapi_schema_pydantic/v3/v3_0_3/server_variable.py @@ -1,6 +1,6 @@ from typing import List, Optional -from pydantic import BaseModel, Extra +from pydantic import BaseModel, ConfigDict class ServerVariable(BaseModel): @@ -12,7 +12,7 @@ class ServerVariable(BaseModel): The array SHOULD NOT be empty. """ - default: str = ... + default: str """ **REQUIRED**. The default value to use for substitution, which SHALL be sent if an alternate value is _not_ supplied. @@ -27,5 +27,4 @@ class ServerVariable(BaseModel): [CommonMark syntax](https://spec.commonmark.org/) MAY be used for rich text representation. """ - class Config: - extra = Extra.ignore + model_config = ConfigDict(extra="ignore") diff --git a/openapi_schema_pydantic/v3/v3_0_3/tag.py b/openapi_schema_pydantic/v3/v3_0_3/tag.py index 24e926a..d1e439f 100644 --- a/openapi_schema_pydantic/v3/v3_0_3/tag.py +++ b/openapi_schema_pydantic/v3/v3_0_3/tag.py @@ -1,6 +1,6 @@ from typing import Optional -from pydantic import BaseModel, Extra +from pydantic import BaseModel, ConfigDict from .external_documentation import ExternalDocumentation @@ -11,7 +11,7 @@ class Tag(BaseModel): It is not mandatory to have a Tag Object per tag defined in the Operation Object instances. """ - name: str = ... + name: str """ **REQUIRED**. The name of the tag. """ @@ -27,6 +27,7 @@ class Tag(BaseModel): Additional external documentation for this tag. """ - class Config: - extra = Extra.ignore - schema_extra = {"examples": [{"name": "pet", "description": "Pets operations"}]} + model_config = ConfigDict( + extra="ignore", + json_schema_extra={"examples": [{"name": "pet", "description": "Pets operations"}]}, + ) diff --git a/openapi_schema_pydantic/v3/v3_0_3/util.py b/openapi_schema_pydantic/v3/v3_0_3/util.py index cb57969..d59df65 100644 --- a/openapi_schema_pydantic/v3/v3_0_3/util.py +++ b/openapi_schema_pydantic/v3/v3_0_3/util.py @@ -1,27 +1,34 @@ import logging -from typing import Any, List, Set, Type, TypeVar +from typing import Any, List, Set, Type from pydantic import BaseModel -from pydantic.schema import schema +from pydantic.json_schema import models_json_schema, JsonSchemaMode from . import Components, OpenAPI, Reference, Schema logger = logging.getLogger(__name__) -PydanticType = TypeVar("PydanticType", bound=BaseModel) +PydanticType = BaseModel ref_prefix = "#/components/schemas/" +ref_template = ref_prefix + "{model}" class PydanticSchema(Schema): """Special `Schema` class to indicate a reference from pydantic class""" - schema_class: Type[PydanticType] = ... + schema_class: Type[BaseModel] """the class that is used for generate the schema""" +def get_mode(cls: Type[BaseModel], default: JsonSchemaMode = "validation") -> JsonSchemaMode: + if not hasattr(cls, "model_config"): + return default + return cls.model_config.get("json_schema_mode", default) + + def construct_open_api_with_schema_class( open_api: OpenAPI, - schema_classes: List[Type[PydanticType]] = None, + schema_classes: List[Type[BaseModel]] | None = None, scan_for_pydantic_schema_reference: bool = True, by_alias: bool = True, ) -> OpenAPI: @@ -36,7 +43,7 @@ def construct_open_api_with_schema_class( :return: new OpenAPI object with "#/components/schemas" values updated. If there is no update in "#/components/schemas" values, the original `open_api` will be returned. """ - new_open_api: OpenAPI = open_api.copy(deep=True) + new_open_api: OpenAPI = open_api.model_copy(deep=True) if scan_for_pydantic_schema_reference: extracted_schema_classes = _handle_pydantic_schema(new_open_api) if schema_classes: @@ -51,27 +58,33 @@ def construct_open_api_with_schema_class( logger.debug(f"schema_classes{schema_classes}") # update new_open_api with new #/components/schemas - schema_definitions = schema(schema_classes, by_alias=by_alias, ref_prefix=ref_prefix) + # Note: the mode (validation or serialization) affects + # optional and computed fields. + key_map, schema_definitions = models_json_schema( + [(c, get_mode(c)) for c in schema_classes], + by_alias=by_alias, + ref_template=ref_template, + ) if not new_open_api.components: new_open_api.components = Components() if new_open_api.components.schemas: for existing_key in new_open_api.components.schemas: - if existing_key in schema_definitions.get("definitions"): + if existing_key in schema_definitions["$defs"]: logger.warning( f'"{existing_key}" already exists in {ref_prefix}. ' f'The value of "{ref_prefix}{existing_key}" will be overwritten.' ) new_open_api.components.schemas.update( - {key: Schema.parse_obj(schema_dict) for key, schema_dict in schema_definitions.get("definitions").items()} + {key: Schema.model_validate(schema_dict) for key, schema_dict in schema_definitions["$defs"].items()} ) else: new_open_api.components.schemas = { - key: Schema.parse_obj(schema_dict) for key, schema_dict in schema_definitions.get("definitions").items() + key: Schema.model_validate(schema_dict) for key, schema_dict in schema_definitions["$defs"].items() } return new_open_api -def _handle_pydantic_schema(open_api: OpenAPI) -> List[Type[PydanticType]]: +def _handle_pydantic_schema(open_api: OpenAPI) -> List[Type[BaseModel]]: """ This function traverses the `OpenAPI` object and @@ -84,11 +97,11 @@ def _handle_pydantic_schema(open_api: OpenAPI) -> List[Type[PydanticType]]: :return: a list of schema classes extracted from `PydanticSchema` objects """ - pydantic_types: Set[Type[PydanticType]] = set() + pydantic_types: Set[Type[BaseModel]] = set() def _traverse(obj: Any): if isinstance(obj, BaseModel): - fields = obj.__fields_set__ + fields = obj.model_fields_set for field in fields: child_obj = obj.__getattribute__(field) if isinstance(child_obj, PydanticSchema): diff --git a/openapi_schema_pydantic/v3/v3_0_3/xml.py b/openapi_schema_pydantic/v3/v3_0_3/xml.py index e451bee..139f84d 100644 --- a/openapi_schema_pydantic/v3/v3_0_3/xml.py +++ b/openapi_schema_pydantic/v3/v3_0_3/xml.py @@ -1,6 +1,6 @@ from typing import Optional -from pydantic import BaseModel, Extra +from pydantic import BaseModel, ConfigDict class XML(BaseModel): @@ -47,11 +47,12 @@ class XML(BaseModel): The definition takes effect only when defined alongside `type` being `array` (outside the `items`). """ - class Config: - extra = Extra.ignore - schema_extra = { + model_config = ConfigDict( + extra="ignore", + json_schema_extra={ "examples": [ {"namespace": "http://example.com/schema/sample", "prefix": "sample"}, {"name": "aliens", "wrapped": True}, ] - } + }, + ) diff --git a/openapi_schema_pydantic/v3/v3_1_0/README.md b/openapi_schema_pydantic/v3/v3_1_0/README.md index d8a3718..a46e2ad 100644 --- a/openapi_schema_pydantic/v3/v3_1_0/README.md +++ b/openapi_schema_pydantic/v3/v3_1_0/README.md @@ -22,7 +22,7 @@ the following fields are used with [alias](https://pydantic-docs.helpmanual.io/u > The "in" field in Header object is actually a constant (`{"in": "header"}`). > For convenience of object creation, the classes mentioned in above -> has configured `allow_population_by_field_name=True`. +> has configured `populate_by_name=True`. > > Reference: [Pydantic's Model Config](https://pydantic-docs.helpmanual.io/usage/model_config/) diff --git a/openapi_schema_pydantic/v3/v3_1_0/__init__.py b/openapi_schema_pydantic/v3/v3_1_0/__init__.py index dd938d1..9fd5b19 100644 --- a/openapi_schema_pydantic/v3/v3_1_0/__init__.py +++ b/openapi_schema_pydantic/v3/v3_1_0/__init__.py @@ -38,5 +38,7 @@ from .security_requirement import SecurityRequirement # resolve forward references -Encoding.update_forward_refs(Header=Header) -Schema.update_forward_refs() +Encoding.model_rebuild() +OpenAPI.model_rebuild() +Components.model_rebuild() +Operation.model_rebuild() diff --git a/openapi_schema_pydantic/v3/v3_1_0/components.py b/openapi_schema_pydantic/v3/v3_1_0/components.py index 899d333..37f65c1 100644 --- a/openapi_schema_pydantic/v3/v3_1_0/components.py +++ b/openapi_schema_pydantic/v3/v3_1_0/components.py @@ -1,6 +1,6 @@ from typing import Dict, Optional, Union -from pydantic import BaseModel, Extra +from pydantic import BaseModel, ConfigDict from .callback import Callback from .example import Example @@ -52,9 +52,9 @@ class Components(BaseModel): pathItems: Optional[Dict[str, Union[PathItem, Reference]]] = None """An object to hold reusable [Path Item Object](#pathItemObject).""" - class Config: - extra = Extra.ignore - schema_extra = { + model_config = ConfigDict( + extra="ignore", + json_schema_extra={ "examples": [ { "schemas": { @@ -115,4 +115,5 @@ class Config: }, } ] - } + }, + ) diff --git a/openapi_schema_pydantic/v3/v3_1_0/contact.py b/openapi_schema_pydantic/v3/v3_1_0/contact.py index 26be261..ab0e0a5 100644 --- a/openapi_schema_pydantic/v3/v3_1_0/contact.py +++ b/openapi_schema_pydantic/v3/v3_1_0/contact.py @@ -1,6 +1,6 @@ from typing import Optional -from pydantic import AnyUrl, BaseModel, Extra +from pydantic import AnyUrl, BaseModel, ConfigDict class Contact(BaseModel): @@ -25,10 +25,11 @@ class Contact(BaseModel): MUST be in the form of an email address. """ - class Config: - extra = Extra.ignore - schema_extra = { + model_config = ConfigDict( + extra="ignore", + json_schema_extra={ "examples": [ {"name": "API Support", "url": "http://www.example.com/support", "email": "support@example.com"} ] - } + }, + ) diff --git a/openapi_schema_pydantic/v3/v3_1_0/discriminator.py b/openapi_schema_pydantic/v3/v3_1_0/discriminator.py index 699d4ea..666456a 100644 --- a/openapi_schema_pydantic/v3/v3_1_0/discriminator.py +++ b/openapi_schema_pydantic/v3/v3_1_0/discriminator.py @@ -1,6 +1,6 @@ from typing import Dict, Optional -from pydantic import BaseModel, Extra +from pydantic import BaseModel, ConfigDict class Discriminator(BaseModel): @@ -14,7 +14,7 @@ class Discriminator(BaseModel): When using the discriminator, _inline_ schemas will not be considered. """ - propertyName: str = ... + propertyName: str """ **REQUIRED**. The name of the property in the payload that will hold the discriminator value. """ @@ -24,9 +24,9 @@ class Discriminator(BaseModel): An object to hold mappings between payload values and schema names or references. """ - class Config: - extra = Extra.ignore - schema_extra = { + model_config = ConfigDict( + extra="ignore", + json_schema_extra={ "examples": [ { "propertyName": "petType", @@ -36,4 +36,5 @@ class Config: }, } ] - } + }, + ) diff --git a/openapi_schema_pydantic/v3/v3_1_0/encoding.py b/openapi_schema_pydantic/v3/v3_1_0/encoding.py index 7dc6220..73d399e 100644 --- a/openapi_schema_pydantic/v3/v3_1_0/encoding.py +++ b/openapi_schema_pydantic/v3/v3_1_0/encoding.py @@ -1,6 +1,6 @@ from typing import Dict, Optional, Union -from pydantic import BaseModel, Extra +from pydantic import BaseModel, ConfigDict from .reference import Reference @@ -67,9 +67,9 @@ class Encoding(BaseModel): then the value of [`contentType`](#encodingContentType) (implicit or explicit) SHALL be ignored. """ - class Config: - extra = Extra.ignore - schema_extra = { + model_config = ConfigDict( + extra="ignore", + json_schema_extra={ "examples": [ { "contentType": "image/png, image/jpeg", @@ -81,4 +81,5 @@ class Config: }, } ] - } + }, + ) diff --git a/openapi_schema_pydantic/v3/v3_1_0/example.py b/openapi_schema_pydantic/v3/v3_1_0/example.py index a8c8bce..db3b7e9 100644 --- a/openapi_schema_pydantic/v3/v3_1_0/example.py +++ b/openapi_schema_pydantic/v3/v3_1_0/example.py @@ -1,10 +1,9 @@ from typing import Any, Optional -from pydantic import BaseModel, Extra +from pydantic import BaseModel, ConfigDict class Example(BaseModel): - summary: Optional[str] = None """ Short description for the example. @@ -33,9 +32,9 @@ class Example(BaseModel): See the rules for resolving [Relative References](#relativeReferencesURI). """ - class Config: - extra = Extra.ignore - schema_extra = { + model_config = ConfigDict( + extra="ignore", + json_schema_extra={ "examples": [ {"summary": "A foo example", "value": {"foo": "bar"}}, { @@ -44,4 +43,5 @@ class Config: }, {"summary": "This is a text example", "externalValue": "http://foo.bar/examples/address-example.txt"}, ] - } + }, + ) diff --git a/openapi_schema_pydantic/v3/v3_1_0/external_documentation.py b/openapi_schema_pydantic/v3/v3_1_0/external_documentation.py index 0ce2747..5dcbdb0 100644 --- a/openapi_schema_pydantic/v3/v3_1_0/external_documentation.py +++ b/openapi_schema_pydantic/v3/v3_1_0/external_documentation.py @@ -1,6 +1,6 @@ from typing import Optional -from pydantic import AnyUrl, BaseModel, Extra +from pydantic import AnyUrl, BaseModel, ConfigDict class ExternalDocumentation(BaseModel): @@ -12,12 +12,13 @@ class ExternalDocumentation(BaseModel): [CommonMark syntax](https://spec.commonmark.org/) MAY be used for rich text representation. """ - url: AnyUrl = ... + url: AnyUrl """ **REQUIRED**. The URL for the target documentation. Value MUST be in the form of a URL. """ - class Config: - extra = Extra.ignore - schema_extra = {"examples": [{"description": "Find more info here", "url": "https://example.com"}]} + model_config = ConfigDict( + extra="ignore", + json_schema_extra={"examples": [{"description": "Find more info here", "url": "https://example.com"}]}, + ) diff --git a/openapi_schema_pydantic/v3/v3_1_0/header.py b/openapi_schema_pydantic/v3/v3_1_0/header.py index c413cdc..1e078ee 100644 --- a/openapi_schema_pydantic/v3/v3_1_0/header.py +++ b/openapi_schema_pydantic/v3/v3_1_0/header.py @@ -1,4 +1,5 @@ -from pydantic import Extra, Field +from pydantic import ConfigDict, Field +from typing import Literal from .parameter import Parameter @@ -13,14 +14,15 @@ class Header(Parameter): (for example, [`style`](#parameterStyle)). """ - name = Field(default="", const=True) - param_in = Field(default="header", const=True, alias="in") + name: Literal[""] = Field(default="") + param_in: Literal["header"] = Field(default="header", alias="in") - class Config: - extra = Extra.ignore - allow_population_by_field_name = True - schema_extra = { + model_config = ConfigDict( + extra="ignore", + populate_by_name=True, + json_schema_extra={ "examples": [ {"description": "The number of allowed requests in the current period", "schema": {"type": "integer"}} ] - } + }, + ) diff --git a/openapi_schema_pydantic/v3/v3_1_0/info.py b/openapi_schema_pydantic/v3/v3_1_0/info.py index 5e8955b..9122a5f 100644 --- a/openapi_schema_pydantic/v3/v3_1_0/info.py +++ b/openapi_schema_pydantic/v3/v3_1_0/info.py @@ -1,6 +1,6 @@ from typing import Optional -from pydantic import AnyUrl, BaseModel, Extra +from pydantic import AnyUrl, BaseModel, ConfigDict from .contact import Contact from .license import License @@ -13,7 +13,7 @@ class Info(BaseModel): and MAY be presented in editing or documentation generation tools for convenience. """ - title: str = ... + title: str """ **REQUIRED**. The title of the API. """ @@ -45,15 +45,15 @@ class Info(BaseModel): The license information for the exposed API. """ - version: str = ... + version: str """ **REQUIRED**. The version of the OpenAPI document (which is distinct from the [OpenAPI Specification version](#oasVersion) or the API implementation version). """ - class Config: - extra = Extra.ignore - schema_extra = { + model_config = ConfigDict( + extra="ignore", + json_schema_extra={ "examples": [ { "title": "Sample Pet Store App", @@ -69,4 +69,5 @@ class Config: "version": "1.0.1", } ] - } + }, + ) diff --git a/openapi_schema_pydantic/v3/v3_1_0/license.py b/openapi_schema_pydantic/v3/v3_1_0/license.py index f0ba1bb..8599cab 100644 --- a/openapi_schema_pydantic/v3/v3_1_0/license.py +++ b/openapi_schema_pydantic/v3/v3_1_0/license.py @@ -1,6 +1,6 @@ from typing import Optional -from pydantic import AnyUrl, BaseModel, Extra +from pydantic import AnyUrl, BaseModel, ConfigDict class License(BaseModel): @@ -8,7 +8,7 @@ class License(BaseModel): License information for the exposed API. """ - name: str = ... + name: str """ **REQUIRED**. The license name used for the API. """ @@ -26,11 +26,12 @@ class License(BaseModel): The `url` field is mutually exclusive of the `identifier` field. """ - class Config: - extra = Extra.ignore - schema_extra = { + model_config = ConfigDict( + extra="ignore", + json_schema_extra={ "examples": [ {"name": "Apache 2.0", "identifier": "Apache-2.0"}, {"name": "Apache 2.0", "url": "https://www.apache.org/licenses/LICENSE-2.0.html"}, ] - } + }, + ) diff --git a/openapi_schema_pydantic/v3/v3_1_0/link.py b/openapi_schema_pydantic/v3/v3_1_0/link.py index df82633..4f11517 100644 --- a/openapi_schema_pydantic/v3/v3_1_0/link.py +++ b/openapi_schema_pydantic/v3/v3_1_0/link.py @@ -1,6 +1,6 @@ from typing import Any, Dict, Optional -from pydantic import BaseModel, Extra +from pydantic import BaseModel, ConfigDict from .server import Server @@ -62,9 +62,9 @@ class Link(BaseModel): A server object to be used by the target operation. """ - class Config: - extra = Extra.ignore - schema_extra = { + model_config = ConfigDict( + extra="ignore", + json_schema_extra={ "examples": [ {"operationId": "getUserAddressByUUID", "parameters": {"userUuid": "$response.body#/uuid"}}, { @@ -72,4 +72,5 @@ class Config: "parameters": {"username": "$response.body#/username"}, }, ] - } + }, + ) diff --git a/openapi_schema_pydantic/v3/v3_1_0/media_type.py b/openapi_schema_pydantic/v3/v3_1_0/media_type.py index 07b3986..9c09589 100644 --- a/openapi_schema_pydantic/v3/v3_1_0/media_type.py +++ b/openapi_schema_pydantic/v3/v3_1_0/media_type.py @@ -1,6 +1,6 @@ from typing import Any, Dict, Optional, Union -from pydantic import BaseModel, Extra, Field +from pydantic import BaseModel, ConfigDict, Field from .encoding import Encoding from .example import Example @@ -48,10 +48,10 @@ class MediaType(BaseModel): when the media type is `multipart` or `application/x-www-form-urlencoded`. """ - class Config: - extra = Extra.ignore - allow_population_by_field_name = True - schema_extra = { + model_config = ConfigDict( + extra="ignore", + populate_by_name=True, + json_schema_extra={ "examples": [ { "schema": {"$ref": "#/components/schemas/Pet"}, @@ -80,4 +80,5 @@ class Config: }, } ] - } + }, + ) diff --git a/openapi_schema_pydantic/v3/v3_1_0/oauth_flow.py b/openapi_schema_pydantic/v3/v3_1_0/oauth_flow.py index 57ded57..c53e035 100644 --- a/openapi_schema_pydantic/v3/v3_1_0/oauth_flow.py +++ b/openapi_schema_pydantic/v3/v3_1_0/oauth_flow.py @@ -1,6 +1,6 @@ from typing import Dict, Optional, Union -from pydantic import AnyUrl, BaseModel, Extra +from pydantic import AnyUrl, BaseModel, ConfigDict class OAuthFlow(BaseModel): @@ -38,9 +38,9 @@ class OAuthFlow(BaseModel): The map MAY be empty. """ - class Config: - extra = Extra.ignore - schema_extra = { + model_config = ConfigDict( + extra="ignore", + json_schema_extra={ "examples": [ { "authorizationUrl": "https://example.com/api/oauth/dialog", @@ -58,4 +58,5 @@ class Config: "scopes": {"write:pets": "modify pets in your account", "read:pets": "read your pets"}, }, ] - } + }, + ) diff --git a/openapi_schema_pydantic/v3/v3_1_0/oauth_flows.py b/openapi_schema_pydantic/v3/v3_1_0/oauth_flows.py index c60a8ff..edc48ab 100644 --- a/openapi_schema_pydantic/v3/v3_1_0/oauth_flows.py +++ b/openapi_schema_pydantic/v3/v3_1_0/oauth_flows.py @@ -1,6 +1,6 @@ from typing import Optional -from pydantic import BaseModel, Extra +from pydantic import BaseModel, ConfigDict from .oauth_flow import OAuthFlow @@ -34,5 +34,4 @@ class OAuthFlows(BaseModel): Previously called `accessCode` in OpenAPI 2.0. """ - class Config: - extra = Extra.ignore + model_config = ConfigDict(extra="ignore") diff --git a/openapi_schema_pydantic/v3/v3_1_0/open_api.py b/openapi_schema_pydantic/v3/v3_1_0/open_api.py index dc1e534..7049ca7 100644 --- a/openapi_schema_pydantic/v3/v3_1_0/open_api.py +++ b/openapi_schema_pydantic/v3/v3_1_0/open_api.py @@ -1,6 +1,6 @@ from typing import Dict, List, Optional, Union -from pydantic import BaseModel, Extra +from pydantic import BaseModel, ConfigDict from .components import Components from .external_documentation import ExternalDocumentation @@ -24,7 +24,7 @@ class OpenAPI(BaseModel): This is *not* related to the API [`info.version`](#infoVersion) string. """ - info: Info = ... + info: Info """ **REQUIRED**. Provides metadata about the API. The metadata MAY be used by tooling as required. """ @@ -86,5 +86,4 @@ class OpenAPI(BaseModel): Additional external documentation. """ - class Config: - extra = Extra.ignore + model_config = ConfigDict(extra="ignore") diff --git a/openapi_schema_pydantic/v3/v3_1_0/operation.py b/openapi_schema_pydantic/v3/v3_1_0/operation.py index 7b5cde3..00a9278 100644 --- a/openapi_schema_pydantic/v3/v3_1_0/operation.py +++ b/openapi_schema_pydantic/v3/v3_1_0/operation.py @@ -1,6 +1,6 @@ from typing import Dict, List, Optional, Union -from pydantic import BaseModel, Extra +from pydantic import BaseModel, ConfigDict from .callback import Callback from .external_documentation import ExternalDocumentation @@ -106,9 +106,9 @@ class Operation(BaseModel): it will be overridden by this value. """ - class Config: - extra = Extra.ignore - schema_extra = { + model_config = ConfigDict( + extra="ignore", + json_schema_extra={ "examples": [ { "tags": ["pet"], @@ -150,4 +150,5 @@ class Config: "security": [{"petstore_auth": ["write:pets", "read:pets"]}], } ] - } + }, + ) diff --git a/openapi_schema_pydantic/v3/v3_1_0/parameter.py b/openapi_schema_pydantic/v3/v3_1_0/parameter.py index e4f69c4..851c68b 100644 --- a/openapi_schema_pydantic/v3/v3_1_0/parameter.py +++ b/openapi_schema_pydantic/v3/v3_1_0/parameter.py @@ -1,6 +1,6 @@ from typing import Any, Dict, Optional, Union -from pydantic import BaseModel, Field, Extra +from pydantic import BaseModel, Field, ConfigDict from .example import Example from .media_type import MediaType @@ -17,7 +17,7 @@ class Parameter(BaseModel): """Fixed Fields""" - name: str = ... + name: str """ **REQUIRED**. The name of the parameter. Parameter names are *case sensitive*. @@ -140,10 +140,10 @@ class Parameter(BaseModel): The map MUST only contain one entry. """ - class Config: - extra = Extra.ignore - allow_population_by_field_name = True - schema_extra = { + model_config = ConfigDict( + extra="ignore", + populate_by_name=True, + json_schema_extra={ "examples": [ { "name": "token", @@ -189,4 +189,5 @@ class Config: }, }, ] - } + }, + ) diff --git a/openapi_schema_pydantic/v3/v3_1_0/path_item.py b/openapi_schema_pydantic/v3/v3_1_0/path_item.py index 5625617..81f0512 100644 --- a/openapi_schema_pydantic/v3/v3_1_0/path_item.py +++ b/openapi_schema_pydantic/v3/v3_1_0/path_item.py @@ -1,6 +1,6 @@ from typing import List, Optional, Union -from pydantic import BaseModel, Extra, Field +from pydantic import BaseModel, ConfigDict, Field from .operation import Operation from .parameter import Parameter @@ -92,10 +92,10 @@ class PathItem(BaseModel): [OpenAPI Object's components/parameters](#componentsParameters). """ - class Config: - extra = Extra.ignore - allow_population_by_field_name = True - schema_extra = { + model_config = ConfigDict( + extra="ignore", + populate_by_name=True, + json_schema_extra={ "examples": [ { "get": { @@ -127,4 +127,5 @@ class Config: ], } ] - } + }, + ) diff --git a/openapi_schema_pydantic/v3/v3_1_0/reference.py b/openapi_schema_pydantic/v3/v3_1_0/reference.py index 76e16c3..c3cfddb 100644 --- a/openapi_schema_pydantic/v3/v3_1_0/reference.py +++ b/openapi_schema_pydantic/v3/v3_1_0/reference.py @@ -1,6 +1,6 @@ from typing import Optional -from pydantic import BaseModel, Extra, Field +from pydantic import BaseModel, ConfigDict, Field class Reference(BaseModel): @@ -29,9 +29,10 @@ class Reference(BaseModel): If the referenced object-type does not allow a `description` field, then this field has no effect. """ - class Config: - extra = Extra.ignore - allow_population_by_field_name = True - schema_extra = { + model_config = ConfigDict( + extra="ignore", + populate_by_name=True, + json_schema_extra={ "examples": [{"$ref": "#/components/schemas/Pet"}, {"$ref": "Pet.json"}, {"$ref": "definitions.json#/Pet"}] - } + }, + ) diff --git a/openapi_schema_pydantic/v3/v3_1_0/request_body.py b/openapi_schema_pydantic/v3/v3_1_0/request_body.py index 1bf58ce..6d79e5f 100644 --- a/openapi_schema_pydantic/v3/v3_1_0/request_body.py +++ b/openapi_schema_pydantic/v3/v3_1_0/request_body.py @@ -1,6 +1,6 @@ from typing import Dict, Optional -from pydantic import BaseModel, Extra +from pydantic import BaseModel, ConfigDict from .media_type import MediaType @@ -16,7 +16,7 @@ class RequestBody(BaseModel): [CommonMark syntax](https://spec.commonmark.org/) MAY be used for rich text representation. """ - content: Dict[str, MediaType] = ... + content: Dict[str, MediaType] """ **REQUIRED**. The content of the request body. The key is a media type or [media type range](https://tools.ietf.org/html/rfc7231#appendix-D) @@ -30,9 +30,9 @@ class RequestBody(BaseModel): Determines if the request body is required in the request. Defaults to `false`. """ - class Config: - extra = Extra.ignore - schema_extra = { + model_config = ConfigDict( + extra="ignore", + json_schema_extra={ "examples": [ { "description": "user to add to the system", @@ -78,4 +78,5 @@ class Config: "content": {"text/plain": {"schema": {"type": "array", "items": {"type": "string"}}}}, }, ] - } + }, + ) diff --git a/openapi_schema_pydantic/v3/v3_1_0/response.py b/openapi_schema_pydantic/v3/v3_1_0/response.py index 1f7042c..4a19135 100644 --- a/openapi_schema_pydantic/v3/v3_1_0/response.py +++ b/openapi_schema_pydantic/v3/v3_1_0/response.py @@ -1,6 +1,6 @@ from typing import Dict, Optional, Union -from pydantic import BaseModel, Extra +from pydantic import BaseModel, ConfigDict from .header import Header from .link import Link @@ -14,7 +14,7 @@ class Response(BaseModel): static `links` to operations based on the response. """ - description: str = ... + description: str """ **REQUIRED**. A short description of the response. [CommonMark syntax](https://spec.commonmark.org/) MAY be used for rich text representation. @@ -43,9 +43,9 @@ class Response(BaseModel): following the naming constraints of the names for [Component Objects](#componentsObject). """ - class Config: - extra = Extra.ignore - schema_extra = { + model_config = ConfigDict( + extra="ignore", + json_schema_extra={ "examples": [ { "description": "A complex object array response", @@ -76,4 +76,5 @@ class Config: }, {"description": "object created"}, ] - } + }, + ) diff --git a/openapi_schema_pydantic/v3/v3_1_0/schema.py b/openapi_schema_pydantic/v3/v3_1_0/schema.py index 1739f0e..7e104b0 100644 --- a/openapi_schema_pydantic/v3/v3_1_0/schema.py +++ b/openapi_schema_pydantic/v3/v3_1_0/schema.py @@ -1,6 +1,6 @@ from typing import Any, Dict, List, Optional, Union -from pydantic import BaseModel, Extra, Field +from pydantic import BaseModel, ConfigDict, Field from .discriminator import Discriminator from .external_documentation import ExternalDocumentation from .reference import Reference @@ -372,7 +372,7 @@ class Schema(BaseModel): sets listed for this keyword. """ - enum: Optional[List[Any]] = Field(default=None, min_items=1) + enum: Optional[List[Any]] = Field(default=None, min_length=1) """ The value of this keyword MUST be an array. This array SHOULD have at least one element. Elements in the array SHOULD be unique. @@ -830,10 +830,10 @@ class Schema(BaseModel): Use of example is discouraged, and later versions of this specification may remove it. """ - class Config: - extra = Extra.ignore - allow_population_by_field_name = True - schema_extra = { + model_config = ConfigDict( + extra="ignore", + populate_by_name=True, + json_schema_extra={ "examples": [ {"type": "string", "format": "email"}, { @@ -913,4 +913,5 @@ class Config: ], }, ] - } + }, + ) diff --git a/openapi_schema_pydantic/v3/v3_1_0/security_scheme.py b/openapi_schema_pydantic/v3/v3_1_0/security_scheme.py index e1761d0..0449b19 100644 --- a/openapi_schema_pydantic/v3/v3_1_0/security_scheme.py +++ b/openapi_schema_pydantic/v3/v3_1_0/security_scheme.py @@ -1,6 +1,6 @@ from typing import Optional, Union -from pydantic import AnyUrl, BaseModel, Extra, Field +from pydantic import AnyUrl, BaseModel, ConfigDict, Field from .oauth_flows import OAuthFlows @@ -21,7 +21,7 @@ class SecurityScheme(BaseModel): Recommended for most use case is Authorization Code Grant flow with PKCE. """ - type: str = ... + type: str """ **REQUIRED**. The type of the security scheme. Valid values are `"apiKey"`, `"http"`, "mutualTLS", `"oauth2"`, `"openIdConnect"`. @@ -71,10 +71,10 @@ class SecurityScheme(BaseModel): This MUST be in the form of a URL. The OpenID Connect standard requires the use of TLS. """ - class Config: - extra = Extra.ignore - allow_population_by_field_name = True - schema_extra = { + model_config = ConfigDict( + extra="ignore", + populate_by_name=True, + json_schema_extra={ "examples": [ {"type": "http", "scheme": "basic"}, {"type": "apiKey", "name": "api_key", "in": "header"}, @@ -91,4 +91,5 @@ class Config: {"type": "openIdConnect", "openIdConnectUrl": "https://example.com/openIdConnect"}, {"type": "openIdConnect", "openIdConnectUrl": "openIdConnect"}, # issue #5: allow relative path ] - } + }, + ) diff --git a/openapi_schema_pydantic/v3/v3_1_0/server.py b/openapi_schema_pydantic/v3/v3_1_0/server.py index aeeefd5..1e3371e 100644 --- a/openapi_schema_pydantic/v3/v3_1_0/server.py +++ b/openapi_schema_pydantic/v3/v3_1_0/server.py @@ -1,6 +1,6 @@ from typing import Dict, Optional -from pydantic import BaseModel, Extra +from pydantic import BaseModel, ConfigDict from .server_variable import ServerVariable @@ -8,7 +8,7 @@ class Server(BaseModel): """An object representing a Server.""" - url: str = ... + url: str """ **REQUIRED**. A URL to the target host. @@ -30,9 +30,9 @@ class Server(BaseModel): The value is used for substitution in the server's URL template. """ - class Config: - extra = Extra.ignore - schema_extra = { + model_config = ConfigDict( + extra="ignore", + json_schema_extra={ "examples": [ {"url": "https://development.gigantic-server.com/v1", "description": "Development server"}, { @@ -49,4 +49,5 @@ class Config: }, }, ] - } + }, + ) diff --git a/openapi_schema_pydantic/v3/v3_1_0/server_variable.py b/openapi_schema_pydantic/v3/v3_1_0/server_variable.py index ad3d989..ff62784 100644 --- a/openapi_schema_pydantic/v3/v3_1_0/server_variable.py +++ b/openapi_schema_pydantic/v3/v3_1_0/server_variable.py @@ -1,6 +1,6 @@ from typing import List, Optional -from pydantic import BaseModel, Extra +from pydantic import BaseModel, ConfigDict class ServerVariable(BaseModel): @@ -12,7 +12,7 @@ class ServerVariable(BaseModel): The array SHOULD NOT be empty. """ - default: str = ... + default: str """ **REQUIRED**. The default value to use for substitution, which SHALL be sent if an alternate value is _not_ supplied. @@ -27,5 +27,4 @@ class ServerVariable(BaseModel): [CommonMark syntax](https://spec.commonmark.org/) MAY be used for rich text representation. """ - class Config: - extra = Extra.ignore + model_config = ConfigDict(extra="ignore") diff --git a/openapi_schema_pydantic/v3/v3_1_0/tag.py b/openapi_schema_pydantic/v3/v3_1_0/tag.py index 24e926a..d1e439f 100644 --- a/openapi_schema_pydantic/v3/v3_1_0/tag.py +++ b/openapi_schema_pydantic/v3/v3_1_0/tag.py @@ -1,6 +1,6 @@ from typing import Optional -from pydantic import BaseModel, Extra +from pydantic import BaseModel, ConfigDict from .external_documentation import ExternalDocumentation @@ -11,7 +11,7 @@ class Tag(BaseModel): It is not mandatory to have a Tag Object per tag defined in the Operation Object instances. """ - name: str = ... + name: str """ **REQUIRED**. The name of the tag. """ @@ -27,6 +27,7 @@ class Tag(BaseModel): Additional external documentation for this tag. """ - class Config: - extra = Extra.ignore - schema_extra = {"examples": [{"name": "pet", "description": "Pets operations"}]} + model_config = ConfigDict( + extra="ignore", + json_schema_extra={"examples": [{"name": "pet", "description": "Pets operations"}]}, + ) diff --git a/openapi_schema_pydantic/v3/v3_1_0/xml.py b/openapi_schema_pydantic/v3/v3_1_0/xml.py index c9ae224..1d5829f 100644 --- a/openapi_schema_pydantic/v3/v3_1_0/xml.py +++ b/openapi_schema_pydantic/v3/v3_1_0/xml.py @@ -1,6 +1,6 @@ from typing import Optional -from pydantic import BaseModel, Extra +from pydantic import BaseModel, ConfigDict class XML(BaseModel): @@ -47,9 +47,9 @@ class XML(BaseModel): The definition takes effect only when defined alongside `type` being `array` (outside the `items`). """ - class Config: - extra = Extra.ignore - schema_extra = { + model_config = ConfigDict( + extra="ignore", + json_schema_extra={ "examples": [ {"name": "animal"}, {"attribute": True}, @@ -57,4 +57,5 @@ class Config: {"namespace": "http://example.com/schema/sample", "prefix": "sample"}, {"name": "aliens", "wrapped": True}, ] - } + }, + ) diff --git a/setup.py b/setup.py index 19d0753..922ae57 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setuptools.setup( name="openapi-schema-pydantic", - version="1.2.4", + version="2.0", author="Kuimono", description="OpenAPI (v3) specification schema as pydantic class", long_description=long_description, @@ -13,12 +13,12 @@ url="https://github.com/kuimono/openapi-schema-pydantic", packages=setuptools.find_packages(exclude=["tests"]), package_data={"openapi_schema_pydantic": ["py.typed"]}, - install_requires=["pydantic>=1.8.2"], + install_requires=["pydantic>=2.0"], tests_require=["pytest"], classifiers=[ "Programming Language :: Python :: 3", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", ], - python_requires=">=3.6.1", + python_requires=">=3.8", ) diff --git a/tests/schema_classes/test_schema.py b/tests/schema_classes/test_schema.py index 0fd8b8e..f5b812d 100644 --- a/tests/schema_classes/test_schema.py +++ b/tests/schema_classes/test_schema.py @@ -1,24 +1,24 @@ import logging -from pydantic import BaseModel, Extra -from pydantic.schema import schema +from pydantic import BaseModel, ConfigDict +from pydantic.json_schema import models_json_schema from openapi_schema_pydantic import Schema, Reference def test_schema(): - schema = Schema.parse_obj( + schema = Schema.model_validate( { "title": "reference list", "description": "schema for list of reference type", - "allOf": [{"$ref": "#/definitions/TestType"}], + "allOf": [{"$ref": "#/components/schemas/TestType"}], } ) logging.debug(f"schema.allOf={schema.allOf}") assert schema.allOf assert isinstance(schema.allOf, list) assert isinstance(schema.allOf[0], Reference) - assert schema.allOf[0].ref == "#/definitions/TestType" + assert schema.allOf[0].ref == "#/components/schemas/TestType" def test_issue_4(): @@ -27,12 +27,11 @@ def test_issue_4(): class TestModel(BaseModel): test_field: str - class Config: - extra = Extra.forbid + model_config = ConfigDict(extra="forbid") - schema_definition = schema([TestModel]) + _key_map, schema_definition = models_json_schema([(TestModel, "validation")]) assert schema_definition == { - "definitions": { + "$defs": { "TestModel": { "title": "TestModel", "type": "object", @@ -44,5 +43,5 @@ class Config: } # allow "additionalProperties" to have boolean value - result = Schema.parse_obj(schema_definition["definitions"]["TestModel"]) + result = Schema.model_validate(schema_definition["$defs"]["TestModel"]) assert result.additionalProperties is False diff --git a/tests/schema_classes/test_security_scheme.py b/tests/schema_classes/test_security_scheme.py index c768dd9..6ad22ba 100644 --- a/tests/schema_classes/test_security_scheme.py +++ b/tests/schema_classes/test_security_scheme.py @@ -8,18 +8,18 @@ def test_security_scheme_issue_5(): security_scheme_1 = SecurityScheme(type="openIdConnect", openIdConnectUrl="https://example.com/openIdConnect") assert isinstance(security_scheme_1.openIdConnectUrl, AnyUrl) or isinstance(security_scheme_1.openIdConnectUrl, str) - assert security_scheme_1.json(by_alias=True, exclude_none=True) == ( - '{"type": "openIdConnect", "openIdConnectUrl": "https://example.com/openIdConnect"}' + assert security_scheme_1.model_dump_json(by_alias=True, exclude_none=True) == ( + '{"type":"openIdConnect","openIdConnectUrl":"https://example.com/openIdConnect"}' ) security_scheme_2 = SecurityScheme(type="openIdConnect", openIdConnectUrl="/openIdConnect") assert isinstance(security_scheme_2.openIdConnectUrl, str) - assert security_scheme_2.json(by_alias=True, exclude_none=True) == ( - '{"type": "openIdConnect", "openIdConnectUrl": "/openIdConnect"}' + assert security_scheme_2.model_dump_json(by_alias=True, exclude_none=True) == ( + '{"type":"openIdConnect","openIdConnectUrl":"/openIdConnect"}' ) security_scheme_3 = SecurityScheme(type="openIdConnect", openIdConnectUrl="openIdConnect") assert isinstance(security_scheme_3.openIdConnectUrl, str) - assert security_scheme_3.json(by_alias=True, exclude_none=True) == ( - '{"type": "openIdConnect", "openIdConnectUrl": "openIdConnect"}' + assert security_scheme_3.model_dump_json(by_alias=True, exclude_none=True) == ( + '{"type":"openIdConnect","openIdConnectUrl":"openIdConnect"}' ) diff --git a/tests/test_alias.py b/tests/test_alias.py index 2cd0b0f..c9a6de9 100644 --- a/tests/test_alias.py +++ b/tests/test_alias.py @@ -3,50 +3,50 @@ def test_header_alias(): header_1 = Header(param_in="header") - header_2 = Header.parse_obj({"param_in": "header"}) - header_3 = Header.parse_obj({"in": "header"}) + header_2 = Header.model_validate({"param_in": "header"}) + header_3 = Header.model_validate({"in": "header"}) assert header_1 == header_2 == header_3 def test_media_type_alias(): media_type_1 = MediaType(media_type_schema=Schema()) media_type_2 = MediaType(schema=Schema()) - media_type_3 = MediaType.parse_obj({"media_type_schema": Schema()}) - media_type_4 = MediaType.parse_obj({"schema": Schema()}) + media_type_3 = MediaType.model_validate({"media_type_schema": Schema()}) + media_type_4 = MediaType.model_validate({"schema": Schema()}) assert media_type_1 == media_type_2 == media_type_3 == media_type_4 def test_parameter_alias(): parameter_1 = Parameter(name="test", param_in="path", param_schema=Schema()) parameter_2 = Parameter(name="test", param_in="path", schema=Schema()) - parameter_3 = Parameter.parse_obj({"name": "test", "param_in": "path", "param_schema": Schema()}) - parameter_4 = Parameter.parse_obj({"name": "test", "in": "path", "schema": Schema()}) + parameter_3 = Parameter.model_validate({"name": "test", "param_in": "path", "param_schema": Schema()}) + parameter_4 = Parameter.model_validate({"name": "test", "in": "path", "schema": Schema()}) assert parameter_1 == parameter_2 == parameter_3 == parameter_4 def test_path_item_alias(): path_item_1 = PathItem(ref="#/dummy") - path_item_2 = PathItem.parse_obj({"ref": "#/dummy"}) - path_item_3 = PathItem.parse_obj({"$ref": "#/dummy"}) + path_item_2 = PathItem.model_validate({"ref": "#/dummy"}) + path_item_3 = PathItem.model_validate({"$ref": "#/dummy"}) assert path_item_1 == path_item_2 == path_item_3 def test_reference_alias(): reference_1 = Reference(ref="#/dummy") - reference_2 = Reference.parse_obj({"ref": "#/dummy"}) - reference_3 = Reference.parse_obj({"$ref": "#/dummy"}) + reference_2 = Reference.model_validate({"ref": "#/dummy"}) + reference_3 = Reference.model_validate({"$ref": "#/dummy"}) assert reference_1 == reference_2 == reference_3 def test_security_scheme(): security_scheme_1 = SecurityScheme(type="apiKey", security_scheme_in="header") - security_scheme_2 = SecurityScheme.parse_obj({"type": "apiKey", "security_scheme_in": "header"}) - security_scheme_3 = SecurityScheme.parse_obj({"type": "apiKey", "in": "header"}) + security_scheme_2 = SecurityScheme.model_validate({"type": "apiKey", "security_scheme_in": "header"}) + security_scheme_3 = SecurityScheme.model_validate({"type": "apiKey", "in": "header"}) assert security_scheme_1 == security_scheme_2 == security_scheme_3 def test_schema(): schema_1 = Schema(schema_not=Schema(), schema_format="email") - schema_2 = Schema.parse_obj({"schema_not": Schema(), "schema_format": "email"}) - schema_3 = Schema.parse_obj({"not": Schema(), "format": "email"}) + schema_2 = Schema.model_validate({"schema_not": Schema(), "schema_format": "email"}) + schema_3 = Schema.model_validate({"not": Schema(), "format": "email"}) assert schema_1 == schema_2 == schema_3 diff --git a/tests/test_config_example.py b/tests/test_config_example.py index 29394d4..3b3d9e3 100644 --- a/tests/test_config_example.py +++ b/tests/test_config_example.py @@ -70,8 +70,11 @@ def test_config_example(): def _assert_config_examples(schema_type): - if getattr(schema_type, "Config", None) and getattr(schema_type.Config, "schema_extra", None): - examples = schema_type.Config.schema_extra.get("examples") + if not hasattr(schema_type, "model_config"): + return + extra = schema_type.model_config.get("json_schema_extra") + if extra is not None: + examples = extra.get("examples") for example_dict in examples: - obj = schema_type(**example_dict) - assert obj.__fields_set__ + obj = schema_type.model_validate(example_dict) + assert obj.model_fields_set diff --git a/tests/test_example.py b/tests/test_example.py index 1a2e9a0..03f0d4a 100644 --- a/tests/test_example.py +++ b/tests/test_example.py @@ -6,7 +6,7 @@ def test_readme_example(): open_api_1 = readme_example_1() assert open_api_1 - open_api_json_1 = open_api_1.json(by_alias=True, exclude_none=True, indent=2) + open_api_json_1 = open_api_1.model_dump_json(by_alias=True, exclude_none=True, indent=2) logging.debug(open_api_json_1) assert open_api_json_1 @@ -30,7 +30,7 @@ def readme_example_1() -> OpenAPI: def readme_example_2() -> OpenAPI: """Construct OpenAPI from raw data object""" - return OpenAPI.parse_obj( + return OpenAPI.model_validate( { "info": {"title": "My own API", "version": "v0.0.1"}, "paths": {"/ping": {"get": {"responses": {"200": {"description": "pong"}}}}}, @@ -40,7 +40,7 @@ def readme_example_2() -> OpenAPI: def readme_example_3() -> OpenAPI: """Construct OpenAPI from mixed object""" - return OpenAPI.parse_obj( + return OpenAPI.model_validate( { "info": {"title": "My own API", "version": "v0.0.1"}, "paths": {"/ping": PathItem(get={"responses": {"200": Response(description="pong")}})}, diff --git a/tests/test_swagger_openapi_v3.py b/tests/test_swagger_openapi_v3.py index 86b70f8..aa4dd23 100644 --- a/tests/test_swagger_openapi_v3.py +++ b/tests/test_swagger_openapi_v3.py @@ -1,12 +1,13 @@ from typing import Dict, Optional -from pydantic import Field +from pydantic import Field, ConfigDict from openapi_schema_pydantic import OpenAPI, Operation, PathItem def test_swagger_openapi_v3(): - open_api = ExtendedOpenAPI.parse_file("tests/data/swagger_openapi_v3.0.1.json") + with open("tests/data/swagger_openapi_v3.0.1.json") as f: + open_api = ExtendedOpenAPI.model_validate_json(f.read()) assert open_api @@ -15,8 +16,7 @@ class ExtendedOperation(Operation): xCodegenRequestBodyName: Optional[str] = Field(default=None, alias="x-codegen-request-body-name") - class Config: - allow_population_by_field_name = True + model_config = ConfigDict(populate_by_name=True) class ExtendedPathItem(PathItem): @@ -31,4 +31,4 @@ class ExtendedPathItem(PathItem): class ExtendedOpenAPI(OpenAPI): - paths: Dict[str, ExtendedPathItem] = ... + paths: Dict[str, ExtendedPathItem] diff --git a/tests/util/test_optional_and_computed.py b/tests/util/test_optional_and_computed.py new file mode 100644 index 0000000..e1e3be5 --- /dev/null +++ b/tests/util/test_optional_and_computed.py @@ -0,0 +1,87 @@ +from pydantic import BaseModel, computed_field, ConfigDict +from pydantic.json_schema import JsonSchemaMode + +from openapi_schema_pydantic import Info, MediaType, OpenAPI, Operation, PathItem, RequestBody, Response, Schema +from openapi_schema_pydantic.util import PydanticSchema, construct_open_api_with_schema_class + + +def test_optional_and_computed_fields(): + api = construct_sample_api() + + result = construct_open_api_with_schema_class(api) + assert result.components is not None + assert result.components.schemas is not None + + req_schema = result.components.schemas["SampleRequest"] + assert isinstance(req_schema, Schema) + assert req_schema.properties is not None + assert req_schema.required is not None + + resp_schema = result.components.schemas["SampleResponse"] + assert isinstance(resp_schema, Schema) + assert resp_schema.properties is not None + assert resp_schema.required is not None + + # When validating: + # - required fields are still required + # - optional fields are still optional + # - computed fields don't exist + assert "req" in req_schema.properties + assert "opt" in req_schema.properties + assert "comp" not in req_schema.properties + assert set(req_schema.required) == {"req"} + + # When serializing: + # - required fields are still required + # - optional fields become required + # - computed fields are required + assert "req" in resp_schema.properties + assert "opt" in resp_schema.properties + assert "comp" in resp_schema.properties + assert set(resp_schema.required) == {"req", "comp", "opt"} + + +def construct_sample_api() -> OpenAPI: + return OpenAPI( + info=Info( + title="Sample API", + version="v0.0.1", + ), + paths={ + "/callme": PathItem( + post=Operation( + requestBody=RequestBody( + content={"application/json": MediaType(schema=PydanticSchema(schema_class=SampleRequest))} + ), + responses={ + "200": Response( + description="resp", + content={"application/json": MediaType(schema=PydanticSchema(schema_class=SampleResponse))}, + ) + }, + ) + ) + }, + ) + + +class ConfigDictExt(ConfigDict, total=False): + json_schema_mode: JsonSchemaMode + + +class SampleModel(BaseModel): + req: bool + opt: bool | None = None + + @computed_field + @property + def comp(self) -> bool: + return True + + +class SampleRequest(SampleModel): + model_config = ConfigDictExt(json_schema_mode="validation") + + +class SampleResponse(SampleModel): + model_config = ConfigDictExt(json_schema_mode="serialization") diff --git a/tests/util/test_pydantic_field.py b/tests/util/test_pydantic_field.py index e5e72e9..eb297b3 100644 --- a/tests/util/test_pydantic_field.py +++ b/tests/util/test_pydantic_field.py @@ -2,7 +2,7 @@ from typing import Union from pydantic import BaseModel, Field -from pydantic.schema import schema +from pydantic.json_schema import models_json_schema from openapi_schema_pydantic import ( OpenAPI, @@ -22,17 +22,17 @@ def test_pydantic_discriminator_schema_generation(): """https://github.com/kuimono/openapi-schema-pydantic/issues/8""" - json_schema = schema([RequestModel]) + _key_map, json_schema = models_json_schema([(RequestModel, "validation")]) assert json_schema == { - "definitions": { + "$defs": { "DataAModel": { - "properties": {"kind": {"enum": ["a"], "title": "Kind", "type": "string"}}, + "properties": {"kind": {"const": "a", "title": "Kind"}}, "required": ["kind"], "title": "DataAModel", "type": "object", }, "DataBModel": { - "properties": {"kind": {"enum": ["b"], "title": "Kind", "type": "string"}}, + "properties": {"kind": {"const": "b", "title": "Kind"}}, "required": ["kind"], "title": "DataBModel", "type": "object", @@ -40,11 +40,11 @@ def test_pydantic_discriminator_schema_generation(): "RequestModel": { "properties": { "data": { - "anyOf": [{"$ref": "#/definitions/DataAModel"}, {"$ref": "#/definitions/DataBModel"}], "discriminator": { - "mapping": {"a": "#/definitions/DataAModel", "b": "#/definitions/DataBModel"}, + "mapping": {"a": "#/$defs/DataAModel", "b": "#/$defs/DataBModel"}, "propertyName": "kind", }, + "oneOf": [{"$ref": "#/$defs/DataAModel"}, {"$ref": "#/$defs/DataBModel"}], "title": "Data", } }, @@ -60,10 +60,12 @@ def test_pydantic_discriminator_openapi_generation(): """https://github.com/kuimono/openapi-schema-pydantic/issues/8""" open_api = construct_open_api_with_schema_class(construct_base_open_api()) + assert open_api.components is not None + assert open_api.components.schemas is not None json_schema = open_api.components.schemas["RequestModel"] assert json_schema.properties == { "data": Schema( - anyOf=[ + oneOf=[ Reference(ref="#/components/schemas/DataAModel", summary=None, description=None), Reference(ref="#/components/schemas/DataBModel", summary=None, description=None), ], diff --git a/tests/util/test_util.py b/tests/util/test_util.py index a280106..88f60a9 100644 --- a/tests/util/test_util.py +++ b/tests/util/test_util.py @@ -1,6 +1,6 @@ import logging -from pydantic import BaseModel, Field +from pydantic import BaseModel, Field, computed_field from openapi_schema_pydantic import Info, MediaType, OpenAPI, Operation, PathItem, Reference, RequestBody, Response from openapi_schema_pydantic.util import PydanticSchema, construct_open_api_with_schema_class @@ -13,7 +13,7 @@ def test_construct_open_api_with_schema_class_1(): assert result_open_api_1.components == result_open_api_2.components assert result_open_api_1 == result_open_api_2 - open_api_json = result_open_api_1.json(by_alias=True, exclude_none=True, indent=2) + open_api_json = result_open_api_1.model_dump_json(by_alias=True, exclude_none=True, indent=2) logging.debug(open_api_json) @@ -43,7 +43,7 @@ def test_construct_open_api_with_schema_class_3(): def construct_base_open_api_1() -> OpenAPI: - return OpenAPI.parse_obj( + return OpenAPI.model_validate( { "info": {"title": "My own API", "version": "v0.0.1"}, "paths": { @@ -146,3 +146,12 @@ class PongResponse(BaseModel): resp_foo: str = Field(alias="pong_foo", description="foo value of the response") resp_bar: str = Field(alias="pong_bar", description="bar value of the response") + + +class ModelWithOptionalAndComputed(BaseModel): + enable: bool | None = None + + @computed_field + @property + def comp(self) -> bool: + return True diff --git a/tests/v3_0_3/test_config_example.py b/tests/v3_0_3/test_config_example.py index 2fb3f16..3636369 100644 --- a/tests/v3_0_3/test_config_example.py +++ b/tests/v3_0_3/test_config_example.py @@ -1,3 +1,4 @@ +from pydantic import BaseModel from openapi_schema_pydantic.v3.v3_0_3 import ( OpenAPI, Info, @@ -70,8 +71,11 @@ def test_config_example(): def _assert_config_examples(schema_type): - if getattr(schema_type, "Config", None) and getattr(schema_type.Config, "schema_extra", None): - examples = schema_type.Config.schema_extra.get("examples") + if not hasattr(schema_type, "model_config"): + return + extra = schema_type.model_config.get("json_schema_extra") + if extra is not None: + examples = extra.get("examples") for example_dict in examples: - obj = schema_type(**example_dict) - assert obj.__fields_set__ + obj = schema_type.model_validate(example_dict) + assert obj.model_fields_set diff --git a/tests/v3_0_3/test_optional_and_computed.py b/tests/v3_0_3/test_optional_and_computed.py new file mode 100644 index 0000000..fa68e6b --- /dev/null +++ b/tests/v3_0_3/test_optional_and_computed.py @@ -0,0 +1,96 @@ +from pydantic import BaseModel, computed_field, ConfigDict +from pydantic.json_schema import JsonSchemaMode + +from openapi_schema_pydantic.v3.v3_0_3 import ( + Info, + MediaType, + OpenAPI, + Operation, + PathItem, + RequestBody, + Response, + Schema, +) +from openapi_schema_pydantic.v3.v3_0_3.util import PydanticSchema, construct_open_api_with_schema_class + + +def test_optional_and_computed_fields(): + api = construct_sample_api() + + result = construct_open_api_with_schema_class(api) + assert result.components is not None + assert result.components.schemas is not None + + req_schema = result.components.schemas["SampleRequest"] + assert isinstance(req_schema, Schema) + assert req_schema.properties is not None + assert req_schema.required is not None + + resp_schema = result.components.schemas["SampleResponse"] + assert isinstance(resp_schema, Schema) + assert resp_schema.properties is not None + assert resp_schema.required is not None + + # When validating: + # - required fields are still required + # - optional fields are still optional + # - computed fields don't exist + assert "req" in req_schema.properties + assert "opt" in req_schema.properties + assert "comp" not in req_schema.properties + assert set(req_schema.required) == {"req"} + + # When serializing: + # - required fields are still required + # - optional fields become required + # - computed fields are required + assert "req" in resp_schema.properties + assert "opt" in resp_schema.properties + assert "comp" in resp_schema.properties + assert set(resp_schema.required) == {"req", "comp", "opt"} + + +def construct_sample_api() -> OpenAPI: + return OpenAPI( + info=Info( + title="Sample API", + version="v0.0.1", + ), + paths={ + "/callme": PathItem( + post=Operation( + requestBody=RequestBody( + content={"application/json": MediaType(schema=PydanticSchema(schema_class=SampleRequest))} + ), + responses={ + "200": Response( + description="resp", + content={"application/json": MediaType(schema=PydanticSchema(schema_class=SampleResponse))}, + ) + }, + ) + ) + }, + ) + + +class ConfigDictExt(ConfigDict, total=False): + json_schema_mode: JsonSchemaMode + + +class SampleModel(BaseModel): + req: bool + opt: bool | None = None + + @computed_field + @property + def comp(self) -> bool: + return True + + +class SampleRequest(SampleModel): + model_config = ConfigDictExt(json_schema_mode="validation") + + +class SampleResponse(SampleModel): + model_config = ConfigDictExt(json_schema_mode="serialization") diff --git a/tests/v3_0_3/test_util.py b/tests/v3_0_3/test_util.py index 8193726..672341e 100644 --- a/tests/v3_0_3/test_util.py +++ b/tests/v3_0_3/test_util.py @@ -22,7 +22,7 @@ def test_construct_open_api_with_schema_class_1(): assert result_open_api_1.components == result_open_api_2.components assert result_open_api_1 == result_open_api_2 - open_api_json = result_open_api_1.json(by_alias=True, exclude_none=True, indent=2) + open_api_json = result_open_api_1.model_dump_json(by_alias=True, exclude_none=True, indent=2) logging.debug(open_api_json) @@ -52,7 +52,7 @@ def test_construct_open_api_with_schema_class_3(): def construct_base_open_api_1() -> OpenAPI: - return OpenAPI.parse_obj( + return OpenAPI.model_validate( { "info": {"title": "My own API", "version": "v0.0.1"}, "paths": { diff --git a/tests/v3_1_0/__init__.py b/tests/v3_1_0/__init__.py index df2a9c0..01d3dfa 100644 --- a/tests/v3_1_0/__init__.py +++ b/tests/v3_1_0/__init__.py @@ -2,5 +2,5 @@ def test_empty_schema(): - schema = Schema.parse_obj({}) + schema = Schema.model_validate({}) assert schema == Schema()