Skip to content

Commit adecf77

Browse files
authored
Merge pull request #17 from OpenPaymentNetwork/main
Compatibility with both Pydantic 1 and 2
2 parents 3dda62a + 093afb7 commit adecf77

Some content is hidden

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

81 files changed

+3260
-1934
lines changed

README.md

Lines changed: 31 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
[![PyPI](https://img.shields.io/pypi/v/openapi-pydantic)](https://pypi.org/project/openapi-pydantic/)
44
[![PyPI - License](https://img.shields.io/pypi/l/openapi-pydantic)](https://github.com/mike-oakley/openapi-pydantic/blob/main/LICENSE)
55

6-
OpenAPI schema implemented in [Pydantic](https://github.com/samuelcolvin/pydantic).
6+
OpenAPI schema implemented in [Pydantic](https://github.com/samuelcolvin/pydantic). Both Pydantic 1.8+ and 2.x are supported.
77

88
The naming of the classes follows the schema in
99
[OpenAPI specification](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.0.md#schema).
@@ -37,7 +37,8 @@ open_api = OpenAPI(
3737
)
3838
},
3939
)
40-
print(open_api.json(by_alias=True, exclude_none=True, indent=2))
40+
# Note: for Pydantic 1.x, replace `model_dump_json` with `json`
41+
print(open_api.model_dump_json(by_alias=True, exclude_none=True, indent=2))
4142
```
4243

4344
Result:
@@ -71,7 +72,7 @@ Result:
7172

7273
## Take advantage of Pydantic
7374

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

7677
The following examples give the same OpenAPI result as above:
7778

@@ -80,6 +81,7 @@ from openapi_pydantic import parse_obj, OpenAPI, PathItem, Response
8081

8182
# Construct OpenAPI from dict, inferring the correct schema version
8283
open_api = parse_obj({
84+
"openapi": "3.1.0",
8385
"info": {"title": "My own API", "version": "v0.0.1"},
8486
"paths": {
8587
"/ping": {
@@ -90,7 +92,8 @@ open_api = parse_obj({
9092

9193

9294
# Construct OpenAPI v3.1.0 schema from dict
93-
open_api = OpenAPI.parse_obj({
95+
# Note: for Pydantic 1.x, replace `model_validate` with `parse_obj`
96+
open_api = OpenAPI.model_validate({
9497
"info": {"title": "My own API", "version": "v0.0.1"},
9598
"paths": {
9699
"/ping": {
@@ -100,7 +103,8 @@ open_api = OpenAPI.parse_obj({
100103
})
101104

102105
# Construct OpenAPI with mix of dict/object
103-
open_api = OpenAPI.parse_obj({
106+
# Note: for Pydantic 1.x, replace `model_validate` with `parse_obj`
107+
open_api = OpenAPI.model_validate({
104108
"info": {"title": "My own API", "version": "v0.0.1"},
105109
"paths": {
106110
"/ping": PathItem(
@@ -113,10 +117,10 @@ open_api = OpenAPI.parse_obj({
113117
## Use Pydantic classes as schema
114118

115119
- 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-
120+
in OpenAPI has definitions and tweaks in JSON Schema, which are hard to comprehend and define a good data class
121+
- Pydantic already has a good way to [create JSON schema](https://pydantic-docs.helpmanual.io/usage/schema/).
122+
Let's not reinvent the wheel.
123+
120124
The approach to deal with this:
121125

122126
1. Use `PydanticSchema` objects to represent the `Schema` in `OpenAPI` object
@@ -129,7 +133,8 @@ from openapi_pydantic import OpenAPI
129133
from openapi_pydantic.util import PydanticSchema, construct_open_api_with_schema_class
130134

131135
def construct_base_open_api() -> OpenAPI:
132-
return OpenAPI.parse_obj({
136+
# Note: for Pydantic 1.x, replace `model_validate` with `parse_obj`
137+
return OpenAPI.model_validate({
133138
"info": {"title": "My own API", "version": "v0.0.1"},
134139
"paths": {
135140
"/ping": {
@@ -162,7 +167,8 @@ open_api = construct_base_open_api()
162167
open_api = construct_open_api_with_schema_class(open_api)
163168

164169
# print the result openapi.json
165-
print(open_api.json(by_alias=True, exclude_none=True, indent=2))
170+
# Note: for Pydantic 1.x, replace `model_dump_json` with `json`
171+
print(open_api.model_dump_json(by_alias=True, exclude_none=True, indent=2))
166172
```
167173

168174
Result:
@@ -259,21 +265,24 @@ Result:
259265

260266
## Notes
261267

262-
### Use of OpenAPI.json() / OpenAPI.dict()
268+
### Use of OpenAPI.model_dump() / OpenAPI.model_dump_json() / OpenAPI.json() / OpenAPI.dict()
263269

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.
270+
When using `OpenAPI.model_dump()` / `OpenAPI.model_dump_json()` / `OpenAPI.json()` / `OpenAPI.dict()` functions,
271+
the arguments `by_alias=True, exclude_none=True` have to be in place.
272+
Otherwise the resulting json will not fit the OpenAPI standard.
267273

268274
```python
269-
# OK
275+
# OK (Pydantic 2)
276+
open_api.model_dump_json(by_alias=True, exclude_none=True, indent=2)
277+
# OK (Pydantic 1)
270278
open_api.json(by_alias=True, exclude_none=True, indent=2)
271279

272280
# Not good
281+
open_api.model_dump_json(indent=2)
273282
open_api.json(indent=2)
274283
```
275284

276-
More info about field alias:
285+
More info about field aliases:
277286

278287
| OpenAPI version | Field alias info |
279288
| --------------- | ---------------- |
@@ -293,13 +302,17 @@ Please refer to the following for more info:
293302
### Use OpenAPI 3.0.3 instead of 3.1.0
294303

295304
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:
305+
The old 3.0.3 version is available by importing from different paths:
297306

298307
```python
299308
from openapi_pydantic.v3.v3_0_3 import OpenAPI, ...
300309
from openapi_pydantic.v3.v3_0_3.util import PydanticSchema, construct_open_api_with_schema_class
301310
```
302311

312+
### Pydantic version compatibility
313+
314+
Compatibility with both major versions of Pydantic (1.8+ and 2.*) is mostly achieved using a module called `compat.py`. It detects the installed version of Pydantic and exports version-specific symbols for use by the rest of the package. It also provides all symbols necessary for type checking. The `compat.py` module is not intended to be imported by other packages, but other packages may find it helpful as an example of how to span major versions of Pydantic.
315+
303316
## Credits
304317

305318
This library is based from the original implementation by Kuimono of [OpenAPI Schema Pydantic](https://github.com/kuimono/openapi-schema-pydantic) which is no longer actively maintained.

openapi_pydantic/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,5 +33,6 @@
3333
from .v3 import ServerVariable as ServerVariable
3434
from .v3 import Tag as Tag
3535
from .v3 import parse_obj as parse_obj
36+
from .v3 import schema_validate as schema_validate
3637

3738
logging.getLogger(__name__).addHandler(logging.NullHandler())

openapi_pydantic/compat.py

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
"""Compatibility layer to make this package usable with Pydantic 1 or 2"""
2+
3+
from typing import TYPE_CHECKING
4+
5+
from pydantic.version import VERSION as PYDANTIC_VERSION
6+
7+
__all__ = [
8+
"PYDANTIC_V2",
9+
"ConfigDict",
10+
"JsonSchemaMode",
11+
"models_json_schema",
12+
"RootModel",
13+
"Extra",
14+
"v1_schema",
15+
"DEFS_KEY",
16+
"min_length_arg",
17+
]
18+
19+
PYDANTIC_MAJOR_VERSION = int(PYDANTIC_VERSION.split(".", 1)[0])
20+
PYDANTIC_V2 = PYDANTIC_MAJOR_VERSION >= 2
21+
22+
if TYPE_CHECKING:
23+
# Provide stubs for either version of Pydantic
24+
25+
from enum import Enum
26+
from typing import Any, Literal, Type, TypedDict
27+
28+
from pydantic import BaseModel
29+
from pydantic import ConfigDict as PydanticConfigDict
30+
31+
def ConfigDict(
32+
extra: Literal["allow", "ignore", "forbid"] = "allow",
33+
json_schema_extra: dict[str, Any] | None = None,
34+
populate_by_name: bool = True,
35+
) -> PydanticConfigDict:
36+
"""Stub for pydantic.ConfigDict in Pydantic 2"""
37+
...
38+
39+
class Extra(Enum):
40+
"""Stub for pydantic.Extra in Pydantic 1"""
41+
42+
allow = "allow"
43+
ignore = "ignore"
44+
forbid = "forbid"
45+
46+
class RootModel(BaseModel):
47+
"""Stub for pydantic.RootModel in Pydantic 2"""
48+
49+
JsonSchemaMode = Literal["validation", "serialization"]
50+
51+
def models_json_schema(
52+
models: list[tuple[Type[BaseModel], JsonSchemaMode]],
53+
*,
54+
by_alias: bool = True,
55+
ref_template: str = "#/$defs/{model}",
56+
schema_generator: type | None = None,
57+
) -> tuple[dict, dict[str, Any]]:
58+
"""Stub for pydantic.json_schema.models_json_schema in Pydantic 2"""
59+
...
60+
61+
def v1_schema(
62+
models: list[Type[BaseModel]],
63+
*,
64+
by_alias: bool = True,
65+
ref_prefix: str = "#/$defs",
66+
) -> dict[str, Any]:
67+
"""Stub for pydantic.schema.schema in Pydantic 1"""
68+
...
69+
70+
DEFS_KEY = "$defs"
71+
72+
class MinLengthArg(TypedDict):
73+
pass
74+
75+
def min_length_arg(min_length: int) -> MinLengthArg:
76+
"""Generate a min_length or min_items parameter for Field(...)"""
77+
...
78+
79+
elif PYDANTIC_V2:
80+
from typing import TypedDict
81+
82+
from pydantic import ConfigDict, RootModel
83+
from pydantic.json_schema import JsonSchemaMode, models_json_schema
84+
85+
# Pydantic 2 renders JSON schemas using the keyword "$defs"
86+
DEFS_KEY = "$defs"
87+
88+
class MinLengthArg(TypedDict):
89+
min_length: int
90+
91+
def min_length_arg(min_length: int) -> MinLengthArg:
92+
return {"min_length": min_length}
93+
94+
# Create V1 stubs. These should not be used when PYDANTIC_V2 is true.
95+
Extra = None
96+
v1_schema = None
97+
98+
99+
else:
100+
from typing import TypedDict
101+
102+
from pydantic import Extra
103+
from pydantic.schema import schema as v1_schema
104+
105+
# Pydantic 1 renders JSON schemas using the keyword "definitions"
106+
DEFS_KEY = "definitions"
107+
108+
class MinLengthArg(TypedDict):
109+
min_items: int
110+
111+
def min_length_arg(min_length: int) -> MinLengthArg:
112+
return {"min_items": min_length}
113+
114+
# Create V2 stubs. These should not be used when PYDANTIC_V2 is false.
115+
ConfigDict = None
116+
models_json_schema = None
117+
JsonSchemaMode = None
118+
RootModel = None

0 commit comments

Comments
 (0)