Skip to content

Commit e4a5e14

Browse files
committed
Support Pydantic versions 1 and 2
1 parent 251290e commit e4a5e14

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

75 files changed

+2731
-1681
lines changed

README.md

Lines changed: 32 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ The naming of the classes follows the schema in
1818

1919
```python
2020
from openapi_pydantic import OpenAPI, Info, PathItem, Operation, Response
21+
from openapi_pydantic.compat import PYDANTIC_V2
2122

2223
# Construct OpenAPI by pydantic objects
2324
open_api = OpenAPI(
@@ -37,7 +38,10 @@ open_api = OpenAPI(
3738
)
3839
},
3940
)
40-
print(open_api.json(by_alias=True, exclude_none=True, indent=2))
41+
if PYDANTIC_V2:
42+
print(open_api.model_dump_json(by_alias=True, exclude_none=True, indent=2))
43+
else:
44+
print(open_api.json(by_alias=True, exclude_none=True, indent=2))
4145
```
4246

4347
Result:
@@ -71,12 +75,13 @@ Result:
7175

7276
## Take advantage of Pydantic
7377

74-
Pydantic is a great tool, allow you to use object / dict / mixed data for for input.
78+
Pydantic is a great tool. It allows you to use object / dict / mixed data for input.
7579

7680
The following examples give the same OpenAPI result as above:
7781

7882
```python
7983
from openapi_pydantic import parse_obj, OpenAPI, PathItem, Response
84+
from openapi_pydantic.compat import PYDANTIC_V2
8085

8186
# Construct OpenAPI from dict, inferring the correct schema version
8287
open_api = parse_obj({
@@ -90,7 +95,8 @@ open_api = parse_obj({
9095

9196

9297
# Construct OpenAPI v3.1.0 schema from dict
93-
open_api = OpenAPI.parse_obj({
98+
openapi_validate = OpenAPI.model_validate if PYDANTIC_V2 else OpenAPI.parse_obj
99+
open_api = openapi_validate({
94100
"info": {"title": "My own API", "version": "v0.0.1"},
95101
"paths": {
96102
"/ping": {
@@ -100,7 +106,8 @@ open_api = OpenAPI.parse_obj({
100106
})
101107

102108
# Construct OpenAPI with mix of dict/object
103-
open_api = OpenAPI.parse_obj({
109+
openapi_validate = OpenAPI.model_validate if PYDANTIC_V2 else OpenAPI.parse_obj
110+
open_api = openapi_validate({
104111
"info": {"title": "My own API", "version": "v0.0.1"},
105112
"paths": {
106113
"/ping": PathItem(
@@ -113,10 +120,10 @@ open_api = OpenAPI.parse_obj({
113120
## Use Pydantic classes as schema
114121

115122
- The [Schema Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#schemaObject)
116-
in OpenAPI has definitions and tweaks in JSON Schema, which is hard to comprehend and define a good data class
117-
- Pydantic already has a good way to [create JSON schema](https://pydantic-docs.helpmanual.io/usage/schema/),
118-
let's not re-invent the wheel
119-
123+
in OpenAPI has definitions and tweaks in JSON Schema, which are hard to comprehend and define a good data class
124+
- Pydantic already has a good way to [create JSON schema](https://pydantic-docs.helpmanual.io/usage/schema/).
125+
Let's not reinvent the wheel.
126+
120127
The approach to deal with this:
121128

122129
1. Use `PydanticSchema` objects to represent the `Schema` in `OpenAPI` object
@@ -126,10 +133,12 @@ The approach to deal with this:
126133
from pydantic import BaseModel, Field
127134

128135
from openapi_pydantic import OpenAPI
136+
from openapi_pydantic.compat import PYDANTIC_V2
129137
from openapi_pydantic.util import PydanticSchema, construct_open_api_with_schema_class
130138

131139
def construct_base_open_api() -> OpenAPI:
132-
return OpenAPI.parse_obj({
140+
openapi_validate = OpenAPI.model_validate if PYDANTIC_V2 else OpenAPI.parse_obj
141+
return openapi_validate({
133142
"info": {"title": "My own API", "version": "v0.0.1"},
134143
"paths": {
135144
"/ping": {
@@ -162,7 +171,10 @@ open_api = construct_base_open_api()
162171
open_api = construct_open_api_with_schema_class(open_api)
163172

164173
# print the result openapi.json
165-
print(open_api.json(by_alias=True, exclude_none=True, indent=2))
174+
if PYDANTIC_V2:
175+
print(open_api.model_dump_json(by_alias=True, exclude_none=True, indent=2))
176+
else:
177+
print(open_api.json(by_alias=True, exclude_none=True, indent=2))
166178
```
167179

168180
Result:
@@ -259,21 +271,24 @@ Result:
259271

260272
## Notes
261273

262-
### Use of OpenAPI.json() / OpenAPI.dict()
274+
### Use of OpenAPI.model_dump() / OpenAPI.model_dump_json() / OpenAPI.json() / OpenAPI.dict()
263275

264-
When using `OpenAPI.json()` / `OpenAPI.dict()` function,
265-
arguments `by_alias=True, exclude_none=True` has to be in place.
266-
Otherwise the result json will not fit the OpenAPI standard.
276+
When using `OpenAPI.model_dump()` / `OpenAPI.model_dump_json()` / `OpenAPI.json()` / `OpenAPI.dict()` functions,
277+
the arguments `by_alias=True, exclude_none=True` have to be in place.
278+
Otherwise the resulting json will not fit the OpenAPI standard.
267279

268280
```python
269-
# OK
281+
# OK (Pydantic 2)
282+
open_api.model_dump_json(by_alias=True, exclude_none=True, indent=2)
283+
# OK (Pydantic 1)
270284
open_api.json(by_alias=True, exclude_none=True, indent=2)
271285

272286
# Not good
287+
open_api.model_dump_json(indent=2)
273288
open_api.json(indent=2)
274289
```
275290

276-
More info about field alias:
291+
More info about field aliases:
277292

278293
| OpenAPI version | Field alias info |
279294
| --------------- | ---------------- |
@@ -293,7 +308,7 @@ Please refer to the following for more info:
293308
### Use OpenAPI 3.0.3 instead of 3.1.0
294309

295310
Some UI renderings (e.g. Swagger) still do not support OpenAPI 3.1.0.
296-
It is allowed to use the old 3.0.3 version by importing from different paths:
311+
The old 3.0.3 version is available by importing from different paths:
297312

298313
```python
299314
from openapi_pydantic.v3.v3_0_3 import OpenAPI, ...

openapi_pydantic/compat.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
"""Compatibility layer to make this package usable with Pydantic 1 or 2"""
2+
3+
from pydantic.version import VERSION as PYDANTIC_VERSION
4+
5+
PYDANTIC_MAJOR_VERSION = int(PYDANTIC_VERSION.split(".", 1)[0])
6+
7+
if int(PYDANTIC_MAJOR_VERSION) >= 2:
8+
PYDANTIC_V2 = True
9+
else:
10+
PYDANTIC_V2 = False
11+
12+
if PYDANTIC_V2:
13+
from typing import Literal
14+
15+
from pydantic import ConfigDict
16+
from pydantic.json_schema import JsonSchemaMode, models_json_schema # type: ignore
17+
18+
# Pydantic 2 renders JSON schemas using the keyword "$defs"
19+
DEFS_KEY = "$defs"
20+
21+
# Add V1 stubs to this module, but hide them from typing
22+
globals().update(
23+
{
24+
"Extra": None,
25+
"v1_schema": None,
26+
}
27+
)
28+
29+
else:
30+
from pydantic import Extra
31+
from pydantic.schema import schema as v1_schema
32+
33+
# Pydantic 1 renders JSON schemas using the keyword "definitions"
34+
DEFS_KEY = "definitions"
35+
36+
# Add V2 stubs to this module, but hide them from typing
37+
globals().update(
38+
{
39+
"ConfigDict": None,
40+
"Literal": None,
41+
"models_json_schema": None,
42+
"JsonSchemaMode": None,
43+
}
44+
)
45+
46+
__all__ = [
47+
"Literal",
48+
"ConfigDict",
49+
"JsonSchemaMode",
50+
"models_json_schema",
51+
"Extra",
52+
"v1_schema",
53+
]

openapi_pydantic/util.py

Lines changed: 51 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,22 @@
22
from typing import Any, Generic, List, Optional, Set, Type, TypeVar
33

44
from pydantic import BaseModel
5-
from pydantic.schema import schema
5+
6+
from openapi_pydantic.compat import (
7+
DEFS_KEY,
8+
PYDANTIC_V2,
9+
JsonSchemaMode,
10+
models_json_schema,
11+
v1_schema,
12+
)
613

714
from . import Components, OpenAPI, Reference, Schema
815

916
logger = logging.getLogger(__name__)
1017

1118
PydanticType = TypeVar("PydanticType", bound=BaseModel)
1219
ref_prefix = "#/components/schemas/"
20+
ref_template = "#/components/schemas/{model}"
1321

1422

1523
class PydanticSchema(Schema, Generic[PydanticType]):
@@ -19,28 +27,46 @@ class PydanticSchema(Schema, Generic[PydanticType]):
1927
"""the class that is used for generate the schema"""
2028

2129

30+
def get_mode(
31+
cls: Type[BaseModel], default: JsonSchemaMode = "validation"
32+
) -> JsonSchemaMode:
33+
"""Get the JSON schema mode for a model class.
34+
35+
The mode can be either "serialization" or "validation". In validation mode,
36+
computed fields are dropped and optional fields remain optional. In
37+
serialization mode, computed and optional fields are required.
38+
"""
39+
if not hasattr(cls, "model_config"):
40+
return default
41+
return cls.model_config.get("json_schema_mode", default)
42+
43+
2244
def construct_open_api_with_schema_class(
2345
open_api: OpenAPI,
2446
schema_classes: Optional[List[Type[BaseModel]]] = None,
2547
scan_for_pydantic_schema_reference: bool = True,
2648
by_alias: bool = True,
2749
) -> OpenAPI:
2850
"""
29-
Construct a new OpenAPI object, utilising pydantic classes to produce JSON schemas
51+
Construct a new OpenAPI object, utilising pydantic classes to produce JSON schemas.
3052
3153
:param open_api: the base `OpenAPI` object
32-
:param schema_classes: pydanitic classes that their schema will be used
54+
:param schema_classes: Pydantic classes that their schema will be used
3355
"#/components/schemas" values
3456
:param scan_for_pydantic_schema_reference: flag to indicate if scanning for
35-
`PydanticSchemaReference` class is
36-
needed for "#/components/schemas" value
37-
updates
57+
`PydanticSchemaReference` class
58+
is needed for "#/components/schemas"
59+
value updates
3860
:param by_alias: construct schema by alias (default is True)
3961
:return: new OpenAPI object with "#/components/schemas" values updated.
4062
If there is no update in "#/components/schemas" values, the original
4163
`open_api` will be returned.
4264
"""
43-
new_open_api: OpenAPI = open_api.copy(deep=True)
65+
if PYDANTIC_V2:
66+
new_open_api = open_api.model_copy(deep=True)
67+
else:
68+
new_open_api = open_api.copy(deep=True)
69+
4470
if scan_for_pydantic_schema_reference:
4571
extracted_schema_classes = _handle_pydantic_schema(new_open_api)
4672
if schema_classes:
@@ -57,28 +83,37 @@ def construct_open_api_with_schema_class(
5783
logger.debug(f"schema_classes{schema_classes}")
5884

5985
# update new_open_api with new #/components/schemas
60-
schema_definitions = schema(
61-
schema_classes, by_alias=by_alias, ref_prefix=ref_prefix
62-
)
86+
if PYDANTIC_V2:
87+
_key_map, schema_definitions = models_json_schema(
88+
[(c, get_mode(c)) for c in schema_classes],
89+
by_alias=by_alias,
90+
ref_template=ref_template,
91+
)
92+
else:
93+
schema_definitions = v1_schema(
94+
schema_classes, by_alias=by_alias, ref_prefix=ref_prefix
95+
)
96+
97+
schema_validate = Schema.model_validate if PYDANTIC_V2 else Schema.parse_obj
6398
if not new_open_api.components:
6499
new_open_api.components = Components()
65100
if new_open_api.components.schemas:
66101
for existing_key in new_open_api.components.schemas:
67-
if existing_key in schema_definitions["definitions"]:
102+
if existing_key in schema_definitions[DEFS_KEY]:
68103
logger.warning(
69104
f'"{existing_key}" already exists in {ref_prefix}. '
70105
f'The value of "{ref_prefix}{existing_key}" will be overwritten.'
71106
)
72107
new_open_api.components.schemas.update(
73108
{
74-
key: Schema.parse_obj(schema_dict)
75-
for key, schema_dict in schema_definitions["definitions"].items()
109+
key: schema_validate(schema_dict)
110+
for key, schema_dict in schema_definitions[DEFS_KEY].items()
76111
}
77112
)
78113
else:
79114
new_open_api.components.schemas = {
80-
key: Schema.parse_obj(schema_dict)
81-
for key, schema_dict in schema_definitions["definitions"].items()
115+
key: schema_validate(schema_dict)
116+
for key, schema_dict in schema_definitions[DEFS_KEY].items()
82117
}
83118
return new_open_api
84119

@@ -101,7 +136,7 @@ def _handle_pydantic_schema(open_api: OpenAPI) -> List[Type[BaseModel]]:
101136

102137
def _traverse(obj: Any) -> None:
103138
if isinstance(obj, BaseModel):
104-
fields = obj.__fields_set__
139+
fields = obj.model_fields_set if PYDANTIC_V2 else obj.__fields_set__
105140
for field in fields:
106141
child_obj = obj.__getattribute__(field)
107142
if isinstance(child_obj, PydanticSchema):

openapi_pydantic/v3/parser.py

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,28 @@
22

33
from pydantic import BaseModel, Field
44

5+
from openapi_pydantic.compat import PYDANTIC_V2
6+
57
from .v3_0_3 import OpenAPI as OpenAPIv3_0
68
from .v3_1_0 import OpenAPI as OpenAPIv3_1
79

10+
OpenAPIv3 = Union[OpenAPIv3_1, OpenAPIv3_0]
11+
12+
if PYDANTIC_V2:
13+
from pydantic import RootModel
14+
15+
class _OpenAPIV2(RootModel):
16+
root: OpenAPIv3 = Field(discriminator="openapi")
17+
18+
else:
819

9-
class _OpenAPI(BaseModel):
10-
__root__: Union[OpenAPIv3_1, OpenAPIv3_0] = Field(discriminator="openapi")
20+
class _OpenAPIV1(BaseModel):
21+
__root__: OpenAPIv3 = Field(discriminator="openapi")
1122

1223

13-
def parse_obj(data: Any) -> Union[OpenAPIv3_1, OpenAPIv3_0]:
24+
def parse_obj(data: Any) -> OpenAPIv3:
1425
"""Parse a raw object into an OpenAPI model with version inference."""
15-
return _OpenAPI.parse_obj(data).__root__
26+
if PYDANTIC_V2:
27+
return _OpenAPIV2.model_validate(data).root
28+
else:
29+
return _OpenAPIV1.parse_obj(data).__root__

openapi_pydantic/v3/v3_0_3/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ the following fields are used with [alias](https://pydantic-docs.helpmanual.io/u
2020
> <a name="header_param_in"></a>The "in" field in Header object is actually a constant (`{"in": "header"}`).
2121
2222
> For convenience of object creation, the classes mentioned in above
23-
> has configured `allow_population_by_field_name=True`.
23+
> have configured `allow_population_by_field_name=True` (Pydantic V1) or `populate_by_name=True` (Pydantic V2).
2424
>
2525
> Reference: [Pydantic's Model Config](https://pydantic-docs.helpmanual.io/usage/model_config/)
2626

openapi_pydantic/v3/v3_0_3/__init__.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#table-of-contents
77
"""
88

9+
from openapi_pydantic.compat import PYDANTIC_V2
10+
911
from .callback import Callback as Callback
1012
from .components import Components as Components
1113
from .contact import Contact as Contact
@@ -40,6 +42,12 @@
4042
from .xml import XML as XML
4143

4244
# resolve forward references
43-
Encoding.update_forward_refs(Header=Header)
44-
Schema.update_forward_refs()
45-
Operation.update_forward_refs(PathItem=PathItem)
45+
if PYDANTIC_V2:
46+
Encoding.model_rebuild()
47+
OpenAPI.model_rebuild()
48+
Components.model_rebuild()
49+
Operation.model_rebuild()
50+
else:
51+
Encoding.update_forward_refs(Header=Header)
52+
Schema.update_forward_refs()
53+
Operation.update_forward_refs(PathItem=PathItem)

0 commit comments

Comments
 (0)