Skip to content

Commit

Permalink
Don't use the "const" optimization when rendering OpenAPI 3.0. Test a…
Browse files Browse the repository at this point in the history
…dded.
  • Loading branch information
hathawsh committed Aug 15, 2023
1 parent 055d707 commit 77a58ca
Show file tree
Hide file tree
Showing 2 changed files with 91 additions and 6 deletions.
39 changes: 34 additions & 5 deletions openapi_pydantic/v3/v3_0_3/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,24 +62,53 @@ class GenerateOpenAPI30Schema:
...

elif PYDANTIC_V2:
from enum import Enum

from pydantic.json_schema import GenerateJsonSchema, JsonSchemaValue
from pydantic_core import core_schema

class GenerateOpenAPI30Schema(GenerateJsonSchema):
"""Modify the schema generation for OpenAPI 3.0.
In OpenAPI 3.0, types can not be None, but a special "nullable"
field is available.
"""
"""Modify the schema generation for OpenAPI 3.0."""

def nullable_schema(
self,
schema: core_schema.NullableSchema,
) -> JsonSchemaValue:
"""Generates a JSON schema that matches a schema that allows null values.
In OpenAPI 3.0, types can not be None, but a special "nullable" field is
available.
"""
inner_json_schema = self.generate_inner(schema["schema"])
inner_json_schema["nullable"] = True
return inner_json_schema

def literal_schema(self, schema: core_schema.LiteralSchema) -> JsonSchemaValue:
"""Generates a JSON schema that matches a literal value.
In OpenAPI 3.0, the "const" keyword is not supported, so this
version of this method skips that optimization.
"""
expected = [
v.value if isinstance(v, Enum) else v for v in schema["expected"]
]

types = {type(e) for e in expected}
if types == {str}:
return {"enum": expected, "type": "string"}
elif types == {int}:
return {"enum": expected, "type": "integer"}
elif types == {float}:
return {"enum": expected, "type": "number"}
elif types == {bool}:
return {"enum": expected, "type": "boolean"}
elif types == {list}:
return {"enum": expected, "type": "array"}
# there is not None case because if it's mixed it hits the final `else`
# if it's a single Literal[None] then it becomes a `const` schema above
else:
return {"enum": expected}

else:

class GenerateOpenAPI30Schema:
Expand Down
58 changes: 57 additions & 1 deletion tests/v3_0_3/test_util.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import logging
from typing import Callable
from typing import Callable, Literal

from pydantic import BaseModel, Field

Expand Down Expand Up @@ -179,6 +179,42 @@ def construct_base_open_api_3() -> OpenAPI:
)


def construct_base_open_api_3_plus() -> OpenAPI:
return OpenAPI(
info=Info(
title="My own API",
version="v0.0.1",
),
paths={
"/ping": PathItem(
post=Operation(
requestBody=RequestBody(
content={
"application/json": MediaType(
media_type_schema=PydanticSchema(
schema_class=PingPlusRequest
)
)
}
),
responses={
"200": Response(
description="pong",
content={
"application/json": MediaType(
media_type_schema=PydanticSchema(
schema_class=PongResponse
)
)
},
)
},
)
)
},
)


class PingRequest(BaseModel):
"""Ping Request"""

Expand All @@ -198,3 +234,23 @@ 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 PingPlusRequest(BaseModel):
"""Ping Request with extra"""

req_foo: str
req_bar: str
req_single_choice: Literal["one"]


def test_enum_with_single_choice() -> None:
api_obj = construct_open_api_with_schema_class(construct_base_open_api_3_plus())
model_dump = getattr(api_obj, "model_dump" if PYDANTIC_V2 else "dict")
api = model_dump(by_alias=True, exclude_none=True)
schema = api["components"]["schemas"]["PingPlusRequest"]
prop = schema["properties"]["req_single_choice"]
# OpenAPI 3.0 does not support "const", so make sure the enum is not
# rendered that way.
assert not prop.get("const")
assert prop["enum"] == ["one"]

0 comments on commit 77a58ca

Please sign in to comment.