django-ninja
django-ninja copied to clipboard
[BUG] Path and Query parameter Annotated description + title are removed from OpenAPI docs
Describe the bug Given:
@api.get("/items/{item_id}")
def read_item(request, item_id: Annotated[str, Field(examples=["an example"], title="Some Title", description="A description")]):
return {"item_id": item_id}
I expect "Some Title" and "A description" show up in the generated OpenAPI docs. However, only "an example" shows up in the generated OpenAPI docs.
Versions (please complete the following information):
- Python version: 3.11.9
- Django version: 5.0.6
- Django-Ninja version: 1.2.0
- Pydantic version: 2.7.4
Root Cause
ninja
's class Param(FieldInfo)
in ninja.params.models
calls:
super().__init__(
default=default,
alias=alias,
title=title,
description=description,
gt=gt,
ge=ge,
lt=lt,
le=le,
min_length=min_length,
max_length=max_length,
json_schema_extra=json_schema_extra,
**extra,
)
This ultimately causes a Path
instance to be created with these _attributes_set
values:
{'default': Ellipsis, 'alias': None, 'title': None, 'description': None, 'gt': None, 'ge': None, 'lt': None, 'le': None, 'min_length': None, 'max_length': None, 'json_schema_extra': {}}
Note that the values of title
and description
are None
.
Direct Cause
When ninja
constructs an Operation
, it creates a ViewSignature
, which calls _create_models
.
The end of the _create_models
function calls model_cls = type(cls_name, (base_cls,), attrs)
.
This causes Pydantic's ModelMetaclass
__new__
method to be called, which results in calls to set_model_fields
, collect_model_fields
, and from_annotated_attribute
.
from_annotated_attribute
calls merge_field_infos
on a tuple of three elements (I'm not sure why the first two elements are identical):
FieldInfo(annotation=NoneType, required=True, title='Some Title', description='A description', examples=['an example'])
FieldInfo(annotation=NoneType, required=True, title='Some Title', description='A description', examples=['an example'])
Path(annotation=str, required=True, json_schema_extra={}, metadata=[FieldInfo(annotation=NoneType, required=True, title='Some Title', description='A description', examples=['an example'])])
The Path
element in the tuple has an _attributes_set
attribute containing this dictionary:
{'default': Ellipsis, 'alias': None, 'title': None, 'description': None, 'gt': None, 'ge': None, 'lt': None, 'le': None, 'min_length': None, 'max_length': None, 'json_schema_extra': {}}
Note that title
and description
are set to None
, so merge_field_infos
overwrites the desired description
and title
with None
.
Potential Solution
This bug disappears if FieldInfo.__init__
in class Param(FieldInfo)
does not receive None
values in the default case.
For example, adding this to the bottom of __init__
in class Param(FieldInfo)
addresses the immediate problem:
initializer: dict[str, Any] = {}
if alias is not None:
initializer["alias"] = alias
if title is not None:
initializer["title"] = title
if description is not None:
initializer["description"] = description
if gt is not None:
initializer["gt"] = gt
if ge is not None:
initializer["ge"] = ge
if lt is not None:
initializer["lt"] = lt
if le is not None:
initializer["le"] = le
if min_length is not None:
initializer["min_length"] = min_length
if max_length is not None:
initializer["max_length"] = max_length
FieldInfo.__init__(
self,
default=default,
json_schema_extra=json_schema_extra,
**initializer,
**extra,
)
I'd be happy to make a pull request making this change, but first I'd like to know if this is an appropriate solution. Is there an easier way to do this?