django-ninja icon indicating copy to clipboard operation
django-ninja copied to clipboard

[BUG] Path and Query parameter Annotated description + title are removed from OpenAPI docs

Open jceipek opened this issue 7 months ago • 5 comments

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?

jceipek avatar Jul 05 '24 23:07 jceipek