Skip to content

fix(openapi): support non-object request bodies and stop body data loss on parameter name collisions - #344

Closed
K4bain wants to merge 1 commit into
tadata-org:mainfrom
K4bain:fix/319-nonobject-bodies-param-collisions
Closed

K4bain wants to merge 1 commit into
tadata-org:mainfrom
K4bain:fix/319-nonobject-bodies-param-collisions

Conversation

@K4bain

@K4bain K4bain commented Sep 2, 2026

Copy link
Copy Markdown

Fixes #319 (both parts).

Root cause

Two related defects in how requestBody is projected and executed:

  1. Non-object bodies are dropped. The conversion loop only reads schema["properties"] — a top-level array (List[Item] = Body(...)) or a primitive body (Pydantic __root__ model) has no properties, so the tool was generated with no body arguments at all. The endpoint was unreachable: the LLM had no way to supply the payload.
  2. Execution deletes colliding body fields. Path/query/header parameters and body fields are flattened into one inputSchema. When a body field shares a name with a path parameter (e.g. item_id in both /orders/{item_id} and the body model), the executor's arguments.pop(param_name) consumed the value while building the path URL — and the body payload then went out without the field, so FastAPI answered 422 Unprocessable Entity.

The fix

Schema conversion (convert.py)

  • Non-object bodies map to a single payload argument carrying the body schema verbatim (falls back to requestBody if a route parameter is named payload). Purely additive: these tools previously had no body input at all.
  • The operation map now records body_param_names (which flattened arguments belong to the body) and raw_body_argument (set for non-object bodies).
  • On a body/route name collision, the route parameter keeps the inputSchema slot and a warning is logged. Nesting the body under its own namespace would fully separate the names, but that changes the tool schema shape for every existing consumer — a breaking change I deliberately left out (happy to do it behind an opt-in flag if maintainers want it).

Execution (server.py)

  • The body is rebuilt from body_param_names before routing parameters are popped, so a colliding body field reaches the body as well as the path.
  • Non-object bodies pass through verbatim (json=<the array/primitive>, not nested).
  • Operation maps without the new metadata (hand-constructed or from older versions) keep the exact historical leftover-arguments behavior — no breaking change for downstream users of operation_map.

Test contract fix

test_execute_api_tool_with_body previously asserted the old inconsistent behavior: the tool's advertised inputSchema flattens the Item model's fields, but the test supplied {"item": {...}} and expected it forwarded verbatim — a payload the real endpoint would reject with 422. The test now sends what the schema advertises (which is also what http://localhost FastAPI actually expects), and the executor forwards exactly the declared body fields.

Verification

  • Executor-level regression tests: colliding body field survives the path pop (json == {"item_id": 42, "quantity": 7} while URL becomes /orders/42), raw array body passes through unwrapped, legacy map keeps leftover behavior.
  • Schema-level regression tests: array body → payload argument with body_param_names/raw_body_argument metadata; collision → route param keeps the slot + metadata records both body fields.
  • Full non-transport suite green (82 passed in the affected files); ruff check + format clean; mypy clean. (The real-transport SSE/HTTP suites error on this Windows checkout identically on clean main — subprocess fork contexts, unrelated to this change.)

…ss on parameter name collisions (tadata-org#319)

Two related defects in how requestBody is projected and executed:

1. Non-object request bodies (top-level arrays or primitives, e.g. from a
Pydantic __root__ model) have no 'properties' field, so the conversion
loop added no arguments for them — the tool was generated with no way to
supply the body, making those endpoints unreachable. Such bodies are now
mapped to a single 'payload' argument carrying the body schema verbatim.

2. path/query/header parameters and body fields are flattened into one
inputSchema; on a name collision (path 'id' + body 'id') the executor's
arguments.pop() deleted the value while building the path, so the body
payload lost the field and FastAPI answered 422. The operation map now
records which argument names are body fields ('body_param_names') and
optionally the raw-body argument name; the executor rebuilds the body
from that list BEFORE consuming routing parameters, so a colliding body
field reaches the body as well as the path. On schema conversion a
collision logs a warning and the route parameter keeps the inputSchema
slot (the LLM cannot express the two independently — that would require
a breaking nesting change, out of scope here).

Operation maps without the new metadata (hand-constructed or from older
versions) keep the exact historical leftover-arguments behavior.

Also fixes test_execute_api_tool_with_body, which asserted the old
inconsistent contract (caller supplying {'item': {...}} although the
advertised inputSchema flattens the Item model's fields); the test now
sends what the schema advertises and the endpoint actually expects.

Regression tests: schema-level (array body -> payload argument, collision
metadata recorded) and executor-level (body survives path pop, raw body
passthrough, legacy map fallback). 19/19 in touched files, full
non-transport suite green, ruff + mypy clean.
@K4bain

K4bain commented Sep 14, 2026

Copy link
Copy Markdown
Author

Closing for housekeeping - the repo had no reviewer engagement and we are re-approaching these areas through a narrower pipeline. Happy to reopen on request.

@K4bain K4bain closed this Sep 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] Request body handling ignores non-object schemas and suffers from parameter name collisions

1 participant