OpenAPITools / OpenAPITools/openapi-generator

[BUG] Null fields dropped from HTTP request bodies

Open
#19,067 0 comments 1 reaction 0 assignees View on GitHub

Nobody has claimed this yet.

Issue: Bug
Dominant language
Java
Stars
26.8k
Forks
7.7k
PR merge metrics
PR metrics pending

Description

Bug Report Checklist
  • Have you provided a full/minimal spec to reproduce the issue?
  • Have you validated the input using an OpenAPI validator (example)?
  • Have you tested with the latest master to confirm the issue still exists?
  • Have you searched for related issues/PRs?
  • What's the actual output vs expected output?
  • [Optional] Sponsorship to speed up the bug fix or feature request (example)
Description

Null fields are dropped from HTTP request bodies. This is especially problematic for HTTP PATCH and PUT requests, where null values are semantically important.

Imagine you have a movie logging service, used to log movies you watch. You log having watched Blade and The Matrix, but then decide to clear their ratings so you can mull over your ratings a bit more. Here's how you might accomplish that with HTTP PUT and PATCH:

method  path    target body                          actual body
PUT     /log/1  {"name": "Blade", "rating": 5}       {"name": "Blade", "rating": 5}
PUT     /log/1  {"name": "Blade", "rating": null}    {"name": "Blade"}
PUT     /log/2  {"name": "The Matrix", "rating": 3}  {"name": "The Matrix", "rating": 3}
PATCH   /log/2  {"rating": null}                     {}

The problem, as shown by the "actual body" column above, is that openapi-generator-cli v7.6.0 python bindings drop null fields from requests. Therefore, it's impossible to make HTTP PUT and PATCH requests which explicitly clear fields. The root cause is this line in the python API client:

obj_dict = obj.model_dump(by_alias=True, exclude_unset=True)

Per BaseModel.model_dump(), the exclude_none controls the following behavior:

Whether to exclude fields that have a value of None.

openapi-generator version

This issue is present in "python" bindings generated by openapi-generator-cli v7.6.0 and latest. Earlier versions of openapi-generator-cli shipped a very different python code generator.

OpenAPI declaration file content or url

https://git.sr.ht/~jaudet/impedimenta/tree/main/item/openapi/sample-project/openapi/spec.yml

Generation Details

To generate API bindings:

git clone https://git.sr.ht/~jaudet/impedimenta
cd impedimenta/openapi/sample-project
./openapi/gen-bindings.sh
Steps to reproduce

To reproduce the issue, execute the above, then:

python3 -m unittest sample_project.tests

Several unit tests demonstrating the behavior will fail.

Related issues/PRs

No.

Suggest a fix

There are several possible solutions. One might be to apply this diff:

diff --git a/modules/openapi-generator/src/main/resources/python/api_client.mustache b/modules/openapi-generator/src/main/resources/python/api_client.mustache
index c7d4a6d700f..61d91641dc5 100644
--- a/modules/openapi-generator/src/main/resources/python/api_client.mustache
+++ b/modules/openapi-generator/src/main/resources/python/api_client.mustache
@@ -371,19 +371,12 @@ class ApiClient:
             )
         elif isinstance(obj, (datetime.datetime, datetime.date)):
             return obj.isoformat()
-
         elif isinstance(obj, dict):
             obj_dict = obj
+        elif hasattr(obj, "model_dump") and callable(obj.model_dump):
+            obj_dict = obj.model_dump(by_alias=True, exclude_unset=True)
         else:
-            # Convert model obj to dict except
-            # attributes `openapi_types`, `attribute_map`
-            # and attributes which value is not None.
-            # Convert attribute name to json key in
-            # model definition for request.
-            if hasattr(obj, 'to_dict') and callable(getattr(obj, 'to_dict')):
-                obj_dict = obj.to_dict()
-            else:
-                obj_dict = obj.__dict__
+            raise TypeError(f"Can't serialize obj: {obj}")
 
         return {
             key: self.sanitize_for_serialization(val)

As an aside, there's quite a bit of serialization logic in the python API client and models. This seems odd, given that pydantic models include logic for serialization via model_dump() and similar.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with modules/openapi-generator/src/main/resources/python/api_client.mustache and reproduce the issue using the linked sample project and its gen-bindings.sh script. Run python3 -m unittest sample_project.tests and verify that generated Python clients preserve explicitly null fields in PUT and PATCH request bodies while the existing serialization tests pass.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
api
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Stale
Clarity
Clearly specified
Newbie friendliness
45/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.