openapi-generators / openapi-generators/openapi-python-client

Multi-body endpoints with required requestBody emit `| Unset` without importing `Unset`

オープン
#1,451 コメント 1 件 リアクション 0 件 担当者 0 名 GitHub で見る

まだ誰も着手していません。

主要言語
Python
スター
2k
フォーク
293
平均マージ
34分
マージ済み PR(30日)
1

説明

Describe the bug

When an endpoint has a required requestBody with multiple content types, the generator appends | Unset = UNSET to the type annotation - even though the body is required.

Furthermore, Unset is not added to the imports, so the generated file references an undefined name - this causes ruff to report multiple instances of F821 Undefined name 'Unset'.

To Reproduce

Create openapi.yaml with the following spec:

openapi: 3.0.3
info:
  title: Unset Bug Demo
  version: 1.0.0
paths:
  /items/{id}:
    put:
      operationId: updateItem
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true # <-- must be true
        content:
          application/json: # <-- must have multiple content types
            schema:
              $ref: '#/components/schemas/Item'
          multipart/form-data: # <-- must have multiple content types
            schema:
              $ref: '#/components/schemas/Item'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Item'
components:
  schemas:
    Item:
      type: object
      properties:
        name:
          type: string

Generate the client and lint:

# Tested on CachyOS with uv 0.11.21 and the following:
uvx --python 3.14 openapi-python-client==0.29.0 generate --path openapi.yaml --output-path out
uvx ruff==0.15.17 check out
Generated code snippet

In out/unset_bug_demo_client/api/default/update_item.py:

from ...types import UNSET, Response  # <-- Unset is not imported

def _get_kwargs(
    id: str,
    *,
    body: Item | Item | Unset = UNSET,  # <-- F821: Undefined name 'Unset' (also note how Item is duplicated)
) -> dict[str, Any]:
    ...

Potential root cause (unverified)

The following was generated by an LLM and has not been verified!

I'm not really familiar with the codebase, but I might take a deeper look in the coming weeks if I find time and turn this into a proper fix/PR...

1. The template always appends | Unset — even for required bodies

In templates/endpoint_macros.py.jinja, the multi-body branch of the arguments macro uses an uninitialized variable > body_required:

{% elif endpoint.bodies | length > 1 %}
body:
    {%- for body in endpoint.bodies -%}{% set body_required = body_required and body.prop.required %}
    {{ body.prop.get_type_string(no_optional=True) }} {% if not loop.last %} | {% endif %}
    {%- endfor -%}{% if not body_required %} | Unset = UNSET{% endif %}
,
{% endif %}

body_required is referenced before it is ever initialized. On the first loop iteration body_required is Jinja Undefined, and Undefined and body.prop.required evaluates to Undefined (falsy). So it stays falsy for the whole loop, and after the loop {% if not body_required %} is always True. Result: | Unset = UNSET is appended for every multi-body endpoint, regardless of whether the bodies are required. (It should have been seeded to True > before the loop so the and chain actually reflects "all bodies required".)

2. The Unset import is never added for required bodies

Each member is rendered with get_type_string(no_optional=True), and the import set is driven by get_imports():

# parser/properties/protocol.py
def get_imports(self, *, prefix: str) -> set[str]:
    ...
    imports = set()
    if not self.required:
        imports.add(f"from {prefix}types import UNSET, Unset")
    return imports

Unset/UNSET are only imported when the property is not required. Endpoint imports are collected from exactly this > method:

# parser/openapi.py
result.bodies.append(body)
result.relative_imports.update(body.prop.get_imports(prefix=models_relative_prefix))

And the module's static imports include only UNSET, never Unset:

# templates/endpoint_module.py.jinja
from ...types import Response, UNSET

コントリビューションガイド

コントリビューションガイドを開く

はじめの一歩

  1. issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
  2. 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
  3. リポジトリをフォークし、ブランチを切って変更します。
  4. issue 番号を参照したプルリクエストを送ります。

調査の方向性

提供された openapi.yaml から生成されたクライアントを再現し、その後 templates/endpoint_macros.py.jinja、parser/properties/protocol.py、parser/openapi.py、templates/endpoint_module.py.jinja を調べます。必須の複数 body 引数と import がどのように収集されるかを追跡します。生成された必須 request body に不要な Unset のデフォルト値や重複した型がなく、ruff が未定義の Unset 名を報告しなければ完了です。

索引モデルが issue の本文から書いたものです。

評価

技術スタック
python
領域
api, tooling
issue の種類
バグ
難易度
3/5
見積もり時間
1〜2日
活発さ
静か
明瞭さ
明確に書かれている
初心者へのやさしさ
72/100

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。