Skip to content

Commit e88400b

Browse files
authored
Merge pull request #50 from jfschneider/normalize_refname
fix: replace invalid characters in $ref field with underscore.
2 parents 5b4fe4e + 143d088 commit e88400b

File tree

4 files changed

+178
-4
lines changed

4 files changed

+178
-4
lines changed

openapi_pydantic/util.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import logging
2+
import re
23
from typing import Any, Dict, Generic, List, Optional, Set, Type, TypeVar, cast
34

45
from pydantic import BaseModel
@@ -170,6 +171,18 @@ def _traverse(obj: Any) -> None:
170171

171172

172173
def _construct_ref_obj(pydantic_schema: PydanticSchema[PydanticType]) -> Reference:
173-
ref_obj = Reference(**{"$ref": ref_prefix + pydantic_schema.schema_class.__name__})
174+
"""
175+
Construct a reference object from the Pydantic schema name
176+
177+
characters in the schema name that are invalid/problematic
178+
for JSONschema $ref names will get replaced with underscores.
179+
Especially needed for Pydantic generic Models with brackets "[]"
180+
181+
see: https://github.com/pydantic/pydantic/blob/aee6057378ccfec02126bf9c984a9b6d6b411777/pydantic/json_schema.py#L2031
182+
"""
183+
ref_name = re.sub(
184+
r"[^a-zA-Z0-9.\-_]", "_", pydantic_schema.schema_class.__name__
185+
).replace(".", "__")
186+
ref_obj = Reference(**{"$ref": ref_prefix + ref_name})
174187
logger.debug(f"ref_obj={ref_obj}")
175188
return ref_obj

openapi_pydantic/v3/v3_0/util.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import logging
2+
import re
23
from typing import (
34
TYPE_CHECKING,
45
Any,
@@ -242,6 +243,18 @@ def _traverse(obj: Any) -> None:
242243

243244

244245
def _construct_ref_obj(pydantic_schema: PydanticSchema[PydanticType]) -> Reference:
245-
ref_obj = Reference(**{"$ref": ref_prefix + pydantic_schema.schema_class.__name__})
246+
"""
247+
Construct a reference object from the Pydantic schema name
248+
249+
characters in the schema name that are invalid/problematic
250+
for JSONschema $ref names will get replaced with underscores.
251+
Especially needed for Pydantic generic Models with brackets "[]"
252+
253+
see: https://github.com/pydantic/pydantic/blob/aee6057378ccfec02126bf9c984a9b6d6b411777/pydantic/json_schema.py#L2031
254+
"""
255+
ref_name = re.sub(
256+
r"[^a-zA-Z0-9.\-_]", "_", pydantic_schema.schema_class.__name__
257+
).replace(".", "__")
258+
ref_obj = Reference(**{"$ref": ref_prefix + ref_name})
246259
logger.debug(f"ref_obj={ref_obj}")
247260
return ref_obj

tests/util/test_util.py

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import logging
2-
from typing import Callable
2+
from typing import Callable, Generic, TypeVar
33

4+
import pytest
45
from pydantic import BaseModel, Field
56

67
from openapi_pydantic import (
@@ -68,6 +69,43 @@ def test_construct_open_api_with_schema_class_3() -> None:
6869
assert "resp_bar" in schema_without_alias.properties
6970

7071

72+
@pytest.mark.skipif(PYDANTIC_V2, reason="generic type for Pydantic V1")
73+
def test_construct_open_api_with_schema_class_4_generic_response_v1() -> None:
74+
DataT = TypeVar("DataT")
75+
from pydantic.v1.generics import GenericModel
76+
77+
class GenericResponse(GenericModel, Generic[DataT]):
78+
msg: str = Field(description="message of the generic response")
79+
data: DataT = Field(description="data value of the generic response")
80+
81+
open_api_4 = construct_base_open_api_4_generic_response(
82+
GenericResponse[PongResponse]
83+
)
84+
85+
result = construct_open_api_with_schema_class(open_api_4)
86+
assert result.components is not None
87+
assert result.components.schemas is not None
88+
assert "GenericResponse_PongResponse_" in result.components.schemas
89+
90+
91+
@pytest.mark.skipif(not PYDANTIC_V2, reason="generic type for Pydantic V2")
92+
def test_construct_open_api_with_schema_class_4_generic_response_v2() -> None:
93+
DataT = TypeVar("DataT")
94+
95+
class GenericResponse(BaseModel, Generic[DataT]):
96+
msg: str = Field(description="message of the generic response")
97+
data: DataT = Field(description="data value of the generic response")
98+
99+
open_api_4 = construct_base_open_api_4_generic_response(
100+
GenericResponse[PongResponse]
101+
)
102+
103+
result = construct_open_api_with_schema_class(open_api_4)
104+
assert result.components is not None
105+
assert result.components.schemas is not None
106+
assert "GenericResponse_PongResponse_" in result.components.schemas
107+
108+
71109
def construct_base_open_api_1() -> OpenAPI:
72110
model_validate: Callable[[dict], OpenAPI] = getattr(
73111
OpenAPI, "model_validate" if PYDANTIC_V2 else "parse_obj"
@@ -176,6 +214,42 @@ def construct_base_open_api_3() -> OpenAPI:
176214
)
177215

178216

217+
def construct_base_open_api_4_generic_response(response_schema: type) -> OpenAPI:
218+
return OpenAPI(
219+
info=Info(
220+
title="My own API",
221+
version="v0.0.1",
222+
),
223+
paths={
224+
"/ping": PathItem(
225+
post=Operation(
226+
requestBody=RequestBody(
227+
content={
228+
"application/json": MediaType(
229+
media_type_schema=PydanticSchema(
230+
schema_class=PingRequest
231+
)
232+
)
233+
}
234+
),
235+
responses={
236+
"200": Response(
237+
description="pong",
238+
content={
239+
"application/json": MediaType(
240+
media_type_schema=PydanticSchema(
241+
schema_class=response_schema
242+
)
243+
)
244+
},
245+
)
246+
},
247+
)
248+
)
249+
},
250+
)
251+
252+
179253
class PingRequest(BaseModel):
180254
"""Ping Request"""
181255

tests/v3_0/test_util.py

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import logging
2-
from typing import Callable, Literal
2+
from typing import Callable, Generic, Literal, TypeVar
33

4+
import pytest
45
from pydantic import BaseModel, Field
56

67
from openapi_pydantic.compat import PYDANTIC_V2
@@ -74,6 +75,43 @@ def test_construct_open_api_with_schema_class_3() -> None:
7475
assert "resp_bar" in schema_without_alias.properties
7576

7677

78+
@pytest.mark.skipif(PYDANTIC_V2, reason="generic type for Pydantic V1")
79+
def test_construct_open_api_with_schema_class_4_generic_response_v1() -> None:
80+
DataT = TypeVar("DataT")
81+
from pydantic.v1.generics import GenericModel
82+
83+
class GenericResponse(GenericModel, Generic[DataT]):
84+
msg: str = Field(description="message of the generic response")
85+
data: DataT = Field(description="data value of the generic response")
86+
87+
open_api_4 = construct_base_open_api_4_generic_response(
88+
GenericResponse[PongResponse]
89+
)
90+
91+
result = construct_open_api_with_schema_class(open_api_4)
92+
assert result.components is not None
93+
assert result.components.schemas is not None
94+
assert "GenericResponse_PongResponse_" in result.components.schemas
95+
96+
97+
@pytest.mark.skipif(not PYDANTIC_V2, reason="generic type for Pydantic V2")
98+
def test_construct_open_api_with_schema_class_4_generic_response() -> None:
99+
DataT = TypeVar("DataT")
100+
101+
class GenericResponse(BaseModel, Generic[DataT]):
102+
msg: str = Field(description="message of the generic response")
103+
data: DataT = Field(description="data value of the generic response")
104+
105+
open_api_4 = construct_base_open_api_4_generic_response(
106+
GenericResponse[PongResponse]
107+
)
108+
109+
result = construct_open_api_with_schema_class(open_api_4)
110+
assert result.components is not None
111+
assert result.components.schemas is not None
112+
assert "GenericResponse_PongResponse_" in result.components.schemas
113+
114+
77115
def construct_base_open_api_1() -> OpenAPI:
78116
model_validate: Callable[[dict], OpenAPI] = getattr(
79117
OpenAPI, "model_validate" if PYDANTIC_V2 else "parse_obj"
@@ -215,6 +253,42 @@ def construct_base_open_api_3_plus() -> OpenAPI:
215253
)
216254

217255

256+
def construct_base_open_api_4_generic_response(response_schema: type) -> OpenAPI:
257+
return OpenAPI(
258+
info=Info(
259+
title="My own API",
260+
version="v0.0.1",
261+
),
262+
paths={
263+
"/ping": PathItem(
264+
post=Operation(
265+
requestBody=RequestBody(
266+
content={
267+
"application/json": MediaType(
268+
media_type_schema=PydanticSchema(
269+
schema_class=PingRequest
270+
)
271+
)
272+
}
273+
),
274+
responses={
275+
"200": Response(
276+
description="pong",
277+
content={
278+
"application/json": MediaType(
279+
media_type_schema=PydanticSchema(
280+
schema_class=response_schema
281+
)
282+
)
283+
},
284+
)
285+
},
286+
)
287+
)
288+
},
289+
)
290+
291+
218292
class PingRequest(BaseModel):
219293
"""Ping Request"""
220294

0 commit comments

Comments
 (0)