Skip to content

Commit c52d49c

Browse files
committed
Add tests that use openapi_spec_validator to validate the output
Includes tests of Literal types with a single choice and JSON examples. They used to break openapi-pydantic.
1 parent 5e96a30 commit c52d49c

File tree

2 files changed

+214
-0
lines changed

2 files changed

+214
-0
lines changed
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
# mypy: ignore-errors
2+
3+
import sys
4+
from typing import Any, Optional
5+
6+
import pytest
7+
from openapi_spec_validator import validate
8+
from pydantic import BaseModel
9+
10+
from openapi_pydantic import (
11+
DataType,
12+
Header,
13+
Info,
14+
MediaType,
15+
OpenAPI,
16+
Operation,
17+
PathItem,
18+
RequestBody,
19+
Response,
20+
Schema,
21+
)
22+
from openapi_pydantic.compat import PYDANTIC_V2
23+
from openapi_pydantic.util import PydanticSchema, construct_open_api_with_schema_class
24+
25+
if sys.version_info < (3, 9):
26+
from typing_extensions import Annotated, Literal
27+
else:
28+
from typing import Annotated, Literal
29+
30+
31+
def test_basic_schema() -> None:
32+
class SampleModel(BaseModel):
33+
required: bool
34+
optional: Optional[bool] = None
35+
one_literal_choice: Literal["only_choice"]
36+
multiple_literal_choices: Literal["choice1", "choice2"]
37+
38+
part_api = construct_sample_api(SampleModel)
39+
40+
api = construct_open_api_with_schema_class(part_api)
41+
assert api.components is not None
42+
assert api.components.schemas is not None
43+
44+
if PYDANTIC_V2:
45+
json_api: Any = api.model_dump(mode="json", by_alias=True, exclude_none=True)
46+
else:
47+
json_api: Any = api.dict(by_alias=True, exclude_none=True)
48+
validate(json_api)
49+
50+
51+
@pytest.mark.skipif(
52+
not PYDANTIC_V2,
53+
reason="Field-level JSON examples not supported in Pydantic V1",
54+
)
55+
def test_field_json_schema_example() -> None:
56+
from pydantic import WithJsonSchema
57+
58+
Thing = Annotated[str, WithJsonSchema({"examples": ["thing1"]})]
59+
60+
class SampleModel(BaseModel):
61+
a: Thing
62+
63+
part_api = construct_sample_api(SampleModel)
64+
65+
api = construct_open_api_with_schema_class(part_api)
66+
assert api.components is not None
67+
assert api.components.schemas is not None
68+
69+
json_api: Any = api.model_dump(mode="json", by_alias=True, exclude_none=True)
70+
props = json_api["components"]["schemas"]["SampleRequest"]["properties"]
71+
assert props["a"]["examples"] == ["thing1"]
72+
73+
validate(json_api)
74+
75+
76+
def construct_sample_api(SampleModel) -> OpenAPI:
77+
class SampleRequest(SampleModel):
78+
model_config = {"json_schema_mode": "validation"}
79+
80+
class SampleResponse(SampleModel):
81+
model_config = {"json_schema_mode": "serialization"}
82+
83+
return OpenAPI(
84+
info=Info(
85+
title="Sample API",
86+
version="v0.0.1",
87+
),
88+
paths={
89+
"/callme": PathItem(
90+
post=Operation(
91+
requestBody=RequestBody(
92+
content={
93+
"application/json": MediaType(
94+
schema=PydanticSchema(schema_class=SampleRequest)
95+
)
96+
}
97+
),
98+
responses={
99+
"200": Response(
100+
description="resp",
101+
headers={
102+
"WWW-Authenticate": Header(
103+
description="Indicate how to authenticate",
104+
schema=Schema(type=DataType.STRING),
105+
)
106+
},
107+
content={
108+
"application/json": MediaType(
109+
schema=PydanticSchema(schema_class=SampleResponse)
110+
)
111+
},
112+
)
113+
},
114+
)
115+
)
116+
},
117+
)
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
# mypy: ignore-errors
2+
3+
import sys
4+
from typing import Any, Optional
5+
6+
from openapi_spec_validator import validate
7+
from pydantic import BaseModel
8+
9+
from openapi_pydantic.compat import PYDANTIC_V2
10+
from openapi_pydantic.v3.v3_0_3 import (
11+
Components,
12+
DataType,
13+
Example,
14+
Header,
15+
Info,
16+
MediaType,
17+
OpenAPI,
18+
Operation,
19+
PathItem,
20+
RequestBody,
21+
Response,
22+
Schema,
23+
)
24+
from openapi_pydantic.v3.v3_0_3.util import (
25+
PydanticSchema,
26+
construct_open_api_with_schema_class,
27+
)
28+
29+
if sys.version_info < (3, 9):
30+
from typing_extensions import Literal
31+
else:
32+
from typing import Literal
33+
34+
35+
def test_basic_schema() -> None:
36+
class SampleModel(BaseModel):
37+
required: bool
38+
optional: Optional[bool] = None
39+
one_literal_choice: Literal["only_choice"]
40+
multiple_literal_choices: Literal["choice1", "choice2"]
41+
42+
part_api = construct_sample_api(SampleModel)
43+
44+
api = construct_open_api_with_schema_class(part_api)
45+
assert api.components is not None
46+
assert api.components.schemas is not None
47+
48+
if PYDANTIC_V2:
49+
json_api: Any = api.model_dump(mode="json", by_alias=True, exclude_none=True)
50+
else:
51+
json_api: Any = api.dict(by_alias=True, exclude_none=True)
52+
validate(json_api)
53+
54+
55+
def construct_sample_api(SampleModel) -> OpenAPI:
56+
class SampleRequest(SampleModel):
57+
model_config = {"json_schema_mode": "validation"}
58+
59+
class SampleResponse(SampleModel):
60+
model_config = {"json_schema_mode": "serialization"}
61+
62+
return OpenAPI(
63+
info=Info(
64+
title="Sample API",
65+
version="v0.0.1",
66+
),
67+
paths={
68+
"/callme": PathItem(
69+
post=Operation(
70+
requestBody=RequestBody(
71+
content={
72+
"application/json": MediaType(
73+
schema=PydanticSchema(schema_class=SampleRequest)
74+
)
75+
}
76+
),
77+
responses={
78+
"200": Response(
79+
description="resp",
80+
headers={
81+
"WWW-Authenticate": Header(
82+
description="Indicate how to authenticate",
83+
schema=Schema(type=DataType.STRING),
84+
)
85+
},
86+
content={
87+
"application/json": MediaType(
88+
schema=PydanticSchema(schema_class=SampleResponse)
89+
)
90+
},
91+
)
92+
},
93+
)
94+
)
95+
},
96+
components=Components(examples={"thing-example": Example(value="thing1")}),
97+
)

0 commit comments

Comments
 (0)