Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: JSON schema examples were OpenAPI formatted #3224

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions litestar/_openapi/responses.py
Original file line number Diff line number Diff line change
Expand Up @@ -298,9 +298,7 @@ def create_error_responses(exceptions: list[type[HTTPException]]) -> Iterator[tu
),
},
description=pascal_case_to_text(get_name(exc)),
examples={
exc.__name__: Example(value={"status_code": status_code, "detail": example_detail, "extra": {}})
},
examples=[{"status_code": status_code, "detail": example_detail, "extra": {}}],
)
)
if len(exceptions_schemas) > 1: # noqa: SIM108
Expand Down
8 changes: 4 additions & 4 deletions litestar/_openapi/schema_generation/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
_should_create_enum_schema,
_should_create_literal_schema,
_type_or_first_not_none_inner_type,
get_formatted_examples,
get_json_schema_formatted_examples,
)
from litestar.datastructures import UploadFile
from litestar.exceptions import ImproperlyConfiguredException
Expand Down Expand Up @@ -558,7 +558,7 @@ def process_schema_result(self, field: FieldDefinition, schema: Schema) -> Schem
not isinstance(value, Hashable) or not self.is_undefined(value)
):
if schema_key == "examples":
value = get_formatted_examples(field, cast("list[Example]", value))
value = get_json_schema_formatted_examples(cast("list[Example]", value))

# we only want to transfer values from the `KwargDefinition` to `Schema` if the schema object
# doesn't already have a value for that property. For example, if a field is a constrained date,
Expand All @@ -572,7 +572,7 @@ def process_schema_result(self, field: FieldDefinition, schema: Schema) -> Schem
if not schema.examples and self.generate_examples:
from litestar._openapi.schema_generation.examples import create_examples_for_field

schema.examples = get_formatted_examples(field, create_examples_for_field(field))
schema.examples = get_json_schema_formatted_examples(create_examples_for_field(field))

if schema.title and schema.type == OpenAPIType.OBJECT:
key = _get_normalized_schema_key(field.annotation)
Expand All @@ -587,7 +587,7 @@ def create_component_schema(
property_fields: Mapping[str, FieldDefinition],
openapi_type: OpenAPIType = OpenAPIType.OBJECT,
title: str | None = None,
examples: Mapping[str, Example] | None = None,
examples: list[Any] | None = None,
) -> Schema:
"""Create a schema for the components/schemas section of the OpenAPI spec.

Expand Down
5 changes: 5 additions & 0 deletions litestar/_openapi/schema_generation/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,3 +107,8 @@ def get_formatted_examples(field_definition: FieldDefinition, examples: Sequence
name = name.lower()

return {f"{name}-example-{i}": example for i, example in enumerate(examples, 1)}


def get_json_schema_formatted_examples(examples: Sequence[Example]) -> list[Any]:
"""Format the examples into the JSON schema format."""
return [example.value for example in examples]
11 changes: 2 additions & 9 deletions litestar/contrib/pydantic/pydantic_schema_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

from typing_extensions import Annotated

from litestar._openapi.schema_generation.utils import get_formatted_examples
from litestar.contrib.pydantic.utils import (
create_field_definitions_for_computed_fields,
is_pydantic_2_model,
Expand All @@ -15,7 +14,7 @@
pydantic_unwrap_and_get_origin,
)
from litestar.exceptions import MissingDependencyException
from litestar.openapi.spec import Example, OpenAPIFormat, OpenAPIType, Schema
from litestar.openapi.spec import OpenAPIFormat, OpenAPIType, Schema
from litestar.plugins import OpenAPISchemaPlugin
from litestar.types import Empty
from litestar.typing import FieldDefinition
Expand Down Expand Up @@ -314,11 +313,5 @@ def for_pydantic_model(cls, field_definition: FieldDefinition, schema_creator: S
required=sorted(f.name for f in property_fields.values() if f.is_required),
property_fields=property_fields,
title=title,
examples=(
None
if example is None
else get_formatted_examples(
field_definition, [Example(description=f"Example {field_definition.name} value", value=example)]
)
),
examples=None if example is None else [example],
)
9 changes: 2 additions & 7 deletions litestar/openapi/spec/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
if TYPE_CHECKING:
from litestar.openapi.spec.discriminator import Discriminator
from litestar.openapi.spec.enums import OpenAPIFormat, OpenAPIType
from litestar.openapi.spec.example import Example
from litestar.openapi.spec.external_documentation import ExternalDocumentation
from litestar.openapi.spec.reference import Reference
from litestar.openapi.spec.xml import XML
Expand Down Expand Up @@ -610,12 +609,8 @@ class Schema(BaseSchemaObject):
Omitting these keywords has the same behavior as values of false.
"""

examples: Mapping[str, Example] | None = None
"""The value of this must be an array containing the example values directly or a mapping of string
to an ``Example`` instance.

This is based on the ``examples`` keyword of JSON Schema.
"""
examples: list[Any] | None = None
"""The value of this must be an array containing the example values."""

discriminator: Discriminator | None = None
"""Adds support for polymorphism.
Expand Down
15 changes: 14 additions & 1 deletion litestar/typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,19 @@
from copy import deepcopy
from dataclasses import dataclass, is_dataclass, replace
from inspect import Parameter, Signature
from typing import Any, AnyStr, Callable, Collection, ForwardRef, Literal, Mapping, Protocol, Sequence, TypeVar, cast
from typing import (
Any,
AnyStr,
Callable,
Collection,
ForwardRef,
Literal,
Mapping,
Protocol,
Sequence,
TypeVar,
cast,
)

from msgspec import UnsetType
from typing_extensions import NotRequired, Required, Self, get_args, get_origin, get_type_hints, is_typeddict
Expand Down Expand Up @@ -81,6 +93,7 @@ def _parse_metadata(value: Any, is_sequence_container: bool, extra: dict[str, An
**cast("dict[str, Any]", extra or getattr(value, "extra", None) or {}),
**(getattr(value, "json_schema_extra", None) or {}),
}
example_list: list[Any] | None
if example := extra.pop("example", None):
example_list = [Example(value=example)]
elif examples := getattr(value, "examples", None):
Expand Down
10 changes: 5 additions & 5 deletions tests/unit/test_contrib/test_pydantic/test_openapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
from litestar._openapi.schema_generation.schema import SchemaCreator
from litestar.contrib.pydantic import PydanticPlugin, PydanticSchemaPlugin
from litestar.openapi import OpenAPIConfig
from litestar.openapi.spec import Example, Reference, Schema
from litestar.openapi.spec import Reference, Schema
from litestar.openapi.spec.enums import OpenAPIFormat, OpenAPIType
from litestar.params import KwargDefinition
from litestar.status_codes import HTTP_200_OK
Expand Down Expand Up @@ -393,7 +393,7 @@ async def example_route() -> Lookup:
assert response.status_code == HTTP_200_OK
assert response.json()["components"]["schemas"]["test_schema_generation_v1.Lookup"]["properties"]["id"] == {
"description": "A unique identifier",
"examples": {"id-example-1": {"value": "e4eaaaf2-d142-11e1-b3e4-080027620cdd"}},
"examples": ["e4eaaaf2-d142-11e1-b3e4-080027620cdd"],
"maxLength": 16,
"minLength": 12,
"type": "string",
Expand Down Expand Up @@ -430,7 +430,7 @@ async def example_route() -> Lookup:
assert response.status_code == HTTP_200_OK
assert response.json()["components"]["schemas"]["test_schema_generation_v2.Lookup"]["properties"]["id"] == {
"description": "A unique identifier",
"examples": {"id-example-1": {"value": "e4eaaaf2-d142-11e1-b3e4-080027620cdd"}},
"examples": ["e4eaaaf2-d142-11e1-b3e4-080027620cdd"],
"maxLength": 16,
"minLength": 12,
"type": "string",
Expand Down Expand Up @@ -530,7 +530,7 @@ class Model(pydantic_v1.BaseModel):
assert isinstance(value, Schema)
assert value.description == "description"
assert value.title == "title"
assert value.examples == {"value-example-1": Example(value="example")}
assert value.examples == ["example"]


def test_create_schema_for_field_v2() -> None:
Expand All @@ -550,7 +550,7 @@ class Model(pydantic_v2.BaseModel):
assert isinstance(value, Schema)
assert value.description == "description"
assert value.title == "title"
assert value.examples == {"value-example-1": Example(value="example")}
assert value.examples == ["example"]


@pytest.mark.parametrize("with_future_annotations", [True, False])
Expand Down
2 changes: 1 addition & 1 deletion tests/unit/test_openapi/test_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ async def example_route() -> Lookup:
"id"
] == {
"description": "A unique identifier",
"examples": {"id-example-1": {"value": "e4eaaaf2-d142-11e1-b3e4-080027620cdd"}},
"examples": ["e4eaaaf2-d142-11e1-b3e4-080027620cdd"],
"maxLength": 16,
"minLength": 12,
"type": "string",
Expand Down
12 changes: 6 additions & 6 deletions tests/unit/test_openapi/test_parameters.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,8 @@ def test_create_parameters(person_controller: Type[Controller]) -> None:
assert page_size.schema.type == OpenAPIType.INTEGER
assert page_size.required
assert page_size.description == "Page Size Description"
assert page_size.schema.examples
assert next(iter(page_size.schema.examples.values())).value == 1
assert page_size.examples
assert page_size.schema.examples == [1]

assert name.param_in == ParamType.QUERY
assert name.name == "name"
Expand Down Expand Up @@ -107,19 +107,19 @@ def test_create_parameters(person_controller: Type[Controller]) -> None:
Schema(
type=OpenAPIType.STRING,
enum=["M", "F", "O", "A"],
examples={"gender-example-1": Example(description="Example value", value="M")},
examples=["M"],
),
Schema(
type=OpenAPIType.ARRAY,
items=Schema(
type=OpenAPIType.STRING,
enum=["M", "F", "O", "A"],
examples={"gender-example-1": Example(description="Example value", value="F")},
examples=["F"],
),
examples={"list-example-1": Example(description="Example value", value=["A"])},
examples=[["A"]],
),
],
examples={"gender-example-1": Example(value="M"), "gender-example-2": Example(value=["M", "O"])},
examples=["M", ["M", "O"]],
)
assert not gender.required

Expand Down
6 changes: 3 additions & 3 deletions tests/unit/test_openapi/test_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ def test_process_schema_result() -> None:
assert kwarg_definition.examples
for signature_key, schema_key in KWARG_DEFINITION_ATTRIBUTE_TO_OPENAPI_PROPERTY_MAP.items():
if schema_key == "examples":
assert schema.examples == {"str-example-1": kwarg_definition.examples[0]}
assert schema.examples == [kwarg_definition.examples[0].value]
else:
assert getattr(schema, schema_key) == getattr(kwarg_definition, signature_key)

Expand Down Expand Up @@ -225,7 +225,7 @@ def test_schema_hashing() -> None:
Schema(type=OpenAPIType.NUMBER),
Schema(type=OpenAPIType.OBJECT, properties={"key": Schema(type=OpenAPIType.STRING)}),
],
examples={"example-1": Example(value=None), "example-2": Example(value=[1, 2, 3])},
examples=[None, [1, 2, 3]],
)
assert hash(schema)

Expand Down Expand Up @@ -289,7 +289,7 @@ class Lookup(msgspec.Struct):
schema = get_schema_for_field_definition(FieldDefinition.from_kwarg(name="Lookup", annotation=Lookup))

assert schema.properties["id"].type == OpenAPIType.STRING # type: ignore[index, union-attr]
assert schema.properties["id"].examples == {"id-example-1": Example(value="example")} # type: ignore[index, union-attr]
assert schema.properties["id"].examples == ["example"] # type: ignore[index, union-attr]
assert schema.properties["id"].description == "description" # type: ignore[index]
assert schema.properties["id"].title == "title" # type: ignore[index, union-attr]
assert schema.properties["id"].max_length == 16 # type: ignore[index, union-attr]
Expand Down
Loading