Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -322,10 +322,7 @@ class ApiClient:
match = re.search(r"charset=([a-zA-Z\-\d]+)[\s;]?", content_type)
encoding = match.group(1) if match else "utf-8"
response_text = response_data.data.decode(encoding)
if response_type in ["bytearray", "str"]:
return_data = self.__deserialize_primitive(response_text, response_type)
else:
return_data = self.deserialize(response_text, response_type)
return_data = self.deserialize(response_text, response_type, content_type)
finally:
if not 200 <= response_data.status <= 299:
raise ApiException.from_response(
Expand Down Expand Up @@ -393,21 +390,35 @@ class ApiClient:
for key, val in obj_dict.items()
}

def deserialize(self, response_text, response_type):
def deserialize(self, response_text: str, response_type: str, content_type: Optional[str]):
"""Deserializes response into an object.

:param response: RESTResponse object to be deserialized.
:param response_type: class literal for
deserialized object, or string of class name.
:param content_type: content type of response.

:return: deserialized object.
"""

# fetch data from response object
try:
data = json.loads(response_text)
except ValueError:
if content_type is None:
try:
data = json.loads(response_text)
except ValueError:
data = response_text
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

None for content_type is allowed to maintain backward compatibility, but this is deprecated and may return an incorrect response.

elif content_type.startswith("application/json"):
if response_text == "":
data = ""
else:
data = json.loads(response_text)
elif content_type.startswith("text/plain"):
data = response_text
else:
raise ApiException(
status=0,
reason="Unsupported content type: {0}".format(content_type)
)

return self.__deserialize(data, response_type)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -315,10 +315,7 @@ def response_deserialize(
match = re.search(r"charset=([a-zA-Z\-\d]+)[\s;]?", content_type)
encoding = match.group(1) if match else "utf-8"
response_text = response_data.data.decode(encoding)
if response_type in ["bytearray", "str"]:
return_data = self.__deserialize_primitive(response_text, response_type)
else:
return_data = self.deserialize(response_text, response_type)
return_data = self.deserialize(response_text, response_type, content_type)
finally:
if not 200 <= response_data.status <= 299:
raise ApiException.from_response(
Expand Down Expand Up @@ -386,21 +383,35 @@ def sanitize_for_serialization(self, obj):
for key, val in obj_dict.items()
}

def deserialize(self, response_text, response_type):
def deserialize(self, response_text: str, response_type: str, content_type: Optional[str]):
"""Deserializes response into an object.

:param response: RESTResponse object to be deserialized.
:param response_type: class literal for
deserialized object, or string of class name.
:param content_type: content type of response.

:return: deserialized object.
"""

# fetch data from response object
try:
data = json.loads(response_text)
except ValueError:
if content_type is None:
try:
data = json.loads(response_text)
except ValueError:
data = response_text
elif content_type.startswith("application/json"):
if response_text == "":
data = ""
else:
data = json.loads(response_text)
elif content_type.startswith("text/plain"):
data = response_text
else:
raise ApiException(
status=0,
reason="Unsupported content type: {0}".format(content_type)
)

return self.__deserialize(data, response_type)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,15 +113,12 @@ def testBodyParameter(self):
n = openapi_client.Pet.from_dict({"name": "testing", "photoUrls": ["http://1", "http://2"]})
api_instance = openapi_client.BodyApi()
api_response = api_instance.test_echo_body_pet_response_string(n)
self.assertEqual(api_response, '{"name": "testing", "photoUrls": ["http://1", "http://2"]}')
self.assertEqual(api_response, "{'name': 'testing', 'photoUrls': ['http://1', 'http://2']}")

t = openapi_client.Tag()
api_response = api_instance.test_echo_body_tag_response_string(t)
self.assertEqual(api_response, "{}") # assertion to ensure {} is sent in the body

api_response = api_instance.test_echo_body_tag_response_string(None)
self.assertEqual(api_response, "") # assertion to ensure emtpy string is sent in the body

api_response = api_instance.test_echo_body_free_form_object_response_string({})
self.assertEqual(api_response, "{}") # assertion to ensure {} is sent in the body

Expand Down
27 changes: 19 additions & 8 deletions samples/client/echo_api/python/openapi_client/api_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -315,10 +315,7 @@ def response_deserialize(
match = re.search(r"charset=([a-zA-Z\-\d]+)[\s;]?", content_type)
encoding = match.group(1) if match else "utf-8"
response_text = response_data.data.decode(encoding)
if response_type in ["bytearray", "str"]:
return_data = self.__deserialize_primitive(response_text, response_type)
else:
return_data = self.deserialize(response_text, response_type)
return_data = self.deserialize(response_text, response_type, content_type)
finally:
if not 200 <= response_data.status <= 299:
raise ApiException.from_response(
Expand Down Expand Up @@ -386,21 +383,35 @@ def sanitize_for_serialization(self, obj):
for key, val in obj_dict.items()
}

def deserialize(self, response_text, response_type):
def deserialize(self, response_text: str, response_type: str, content_type: Optional[str]):
"""Deserializes response into an object.

:param response: RESTResponse object to be deserialized.
:param response_type: class literal for
deserialized object, or string of class name.
:param content_type: content type of response.

:return: deserialized object.
"""

# fetch data from response object
try:
data = json.loads(response_text)
except ValueError:
if content_type is None:
try:
data = json.loads(response_text)
except ValueError:
data = response_text
elif content_type.startswith("application/json"):
if response_text == "":
data = ""
else:
data = json.loads(response_text)
elif content_type.startswith("text/plain"):
data = response_text
else:
raise ApiException(
status=0,
reason="Unsupported content type: {0}".format(content_type)
)

return self.__deserialize(data, response_type)

Expand Down
8 changes: 4 additions & 4 deletions samples/client/echo_api/python/tests/test_manual.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,17 +174,17 @@ def testBodyParameter(self):
n = openapi_client.Pet.from_dict({"name": "testing", "photoUrls": ["http://1", "http://2"]})
api_instance = openapi_client.BodyApi()
api_response = api_instance.test_echo_body_pet_response_string(n)
self.assertEqual(api_response, '{"name": "testing", "photoUrls": ["http://1", "http://2"]}')
self.assertEqual(api_response, "{'name': 'testing', 'photoUrls': ['http://1', 'http://2']}")

t = openapi_client.Tag()
api_response = api_instance.test_echo_body_tag_response_string(t)
self.assertEqual(api_response, "{}") # assertion to ensure {} is sent in the body

api_response = api_instance.test_echo_body_tag_response_string(None)
self.assertEqual(api_response, "") # assertion to ensure emtpy string is sent in the body
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This fails because None is not a json.
This test is erroneous and has been removed.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the test is for optional body parameter (e.g. Pet). shall we keep it instead?

Copy link
Contributor Author

@fa0311 fa0311 Jun 5, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the test is for optional body parameter (e.g. Pet).

If that's the case, this test should be performed in petstore_api instead of echo_api.

Since an empty string is not in JSON format, it conflicts with the Content-Type: application/json in the response header.
Alternatively, is there an option to change the response header of echo_api to Content-Type: text/plain ?

Copy link
Member

@wing328 wing328 Jun 5, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using Pet just an example. It can be any models (which will be serialized to JSON string before sending to the server).

Since an empty string is not in JSON format, it conflicts with the Content-Type: application/json in the response header.

Understood as confirmed in https://stackoverflow.com/questions/40614416/is-empty-body-correct-if-content-type-is-application-json. but please take a look at axios/axios#4146 as well, which is also a "use case" presented to use previously (and that's why we've added a test to cover it)

With this change, if None is provided, what will be sent to the server? just empty JSON string "" ?

Copy link
Contributor Author

@fa0311 fa0311 Jun 5, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using Pet just an example. It can be any models (which will be serialized to JSON string before sending to the server).

In this case, None is not serialized to Pet. Therefore, it is not serialized into a JSON string before being sent to the server. Only empty json will be sent.

but please take a look at axios/axios#4146 as well, which is also a "use case" presented to use previously (and that's why we've added a test to cover it)

Ok, add a process to prevent json deserialize if the response is empty.

With this change, if None is provided, what will be sent to the server? just empty JSON string "" ?

This change does not change what is sent to the server. An empty json is sent.

Currently, the client may send an empty json request.
You are correct, response should also be allowed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


api_response = api_instance.test_echo_body_free_form_object_response_string({})
self.assertEqual(api_response, "{}") # assertion to ensure {} is sent in the body

api_response = api_instance.test_echo_body_tag_response_string(None)
self.assertEqual(api_response, "") # assertion to ensure emtpy string is sent in the body

def testAuthHttpBasic(self):
api_instance = openapi_client.AuthApi()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -317,10 +317,7 @@ def response_deserialize(
match = re.search(r"charset=([a-zA-Z\-\d]+)[\s;]?", content_type)
encoding = match.group(1) if match else "utf-8"
response_text = response_data.data.decode(encoding)
if response_type in ["bytearray", "str"]:
return_data = self.__deserialize_primitive(response_text, response_type)
else:
return_data = self.deserialize(response_text, response_type)
return_data = self.deserialize(response_text, response_type, content_type)
finally:
if not 200 <= response_data.status <= 299:
raise ApiException.from_response(
Expand Down Expand Up @@ -388,21 +385,35 @@ def sanitize_for_serialization(self, obj):
for key, val in obj_dict.items()
}

def deserialize(self, response_text, response_type):
def deserialize(self, response_text: str, response_type: str, content_type: Optional[str]):
"""Deserializes response into an object.

:param response: RESTResponse object to be deserialized.
:param response_type: class literal for
deserialized object, or string of class name.
:param content_type: content type of response.

:return: deserialized object.
"""

# fetch data from response object
try:
data = json.loads(response_text)
except ValueError:
if content_type is None:
try:
data = json.loads(response_text)
except ValueError:
data = response_text
elif content_type.startswith("application/json"):
if response_text == "":
data = ""
else:
data = json.loads(response_text)
elif content_type.startswith("text/plain"):
data = response_text
else:
raise ApiException(
status=0,
reason="Unsupported content type: {0}".format(content_type)
)

return self.__deserialize(data, response_type)

Expand Down
27 changes: 19 additions & 8 deletions samples/openapi3/client/petstore/python/petstore_api/api_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -314,10 +314,7 @@ def response_deserialize(
match = re.search(r"charset=([a-zA-Z\-\d]+)[\s;]?", content_type)
encoding = match.group(1) if match else "utf-8"
response_text = response_data.data.decode(encoding)
if response_type in ["bytearray", "str"]:
return_data = self.__deserialize_primitive(response_text, response_type)
else:
return_data = self.deserialize(response_text, response_type)
return_data = self.deserialize(response_text, response_type, content_type)
finally:
if not 200 <= response_data.status <= 299:
raise ApiException.from_response(
Expand Down Expand Up @@ -385,21 +382,35 @@ def sanitize_for_serialization(self, obj):
for key, val in obj_dict.items()
}

def deserialize(self, response_text, response_type):
def deserialize(self, response_text: str, response_type: str, content_type: Optional[str]):
"""Deserializes response into an object.

:param response: RESTResponse object to be deserialized.
:param response_type: class literal for
deserialized object, or string of class name.
:param content_type: content type of response.

:return: deserialized object.
"""

# fetch data from response object
try:
data = json.loads(response_text)
except ValueError:
if content_type is None:
try:
data = json.loads(response_text)
except ValueError:
data = response_text
elif content_type.startswith("application/json"):
if response_text == "":
data = ""
else:
data = json.loads(response_text)
elif content_type.startswith("text/plain"):
data = response_text
else:
raise ApiException(
status=0,
reason="Unsupported content type: {0}".format(content_type)
)

return self.__deserialize(data, response_type)

Expand Down
6 changes: 6 additions & 0 deletions samples/openapi3/client/petstore/python/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ def test_400(self):
mock_resp.data = json.dumps({"reason400": "400 reason"}).encode("utf-8")
mock_resp.getheaders.return_value = {}
mock_resp.getheader.return_value = ""
mock_resp.getheader = (
lambda name: "application/json" if name == "content-type" else Mock()
)

with patch(
"petstore_api.api_client.ApiClient.call_api", return_value=mock_resp
Expand All @@ -71,6 +74,9 @@ def test_404(self):
mock_resp.data = json.dumps({"reason404": "404 reason"}).encode("utf-8")
mock_resp.getheaders.return_value = {}
mock_resp.getheader.return_value = ""
mock_resp.getheader = (
lambda name: "application/json" if name == "content-type" else Mock()
)

with patch(
"petstore_api.api_client.ApiClient.call_api", return_value=mock_resp
Expand Down
Loading