Description
Describe the bug
We use FastAPI as an API server and generate a python client with open-api-client
that is then used to run integration tests. Our team is using 0.7.3 but I was trying to update to 0.8.0. When doing so, we ran into the following exception running our test suite:
/usr/local/lib/python3.8/site-packages/falkon_sdk/api/observations/get_observations.py:73: in _parse_response
response_200 = ObservationResult.from_dict(response.json())
/usr/local/lib/python3.8/site-packages/falkon_sdk/models/observation_result.py:104: in from_dict
metric_change = ObservationFactWindowSummaryResource.from_dict(
/usr/local/lib/python3.8/site-packages/falkon_sdk/models/observation_fact_window_summary_resource.py:156: in from_dict
weighted_change_unfiltered = WeightedChange.from_dict(
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
cls = <class 'falkon_sdk.models.weighted_change.WeightedChange'>
src_dict = None
@classmethod
def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T:
> d = src_dict.copy()
E AttributeError: 'NoneType' object has no attribute 'copy'
I was able to narrow down the problem to handling of Optional[T]
objects within response models. Here is a minimal repro:
To Reproduce
Steps to reproduce the behavior:
- Make a main.py file with the following contents:
from typing import Optional
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class ItemResource(BaseModel):
id: int
class ResourceWithOptional(BaseModel):
item: Optional[ItemResource]
@app.get("/", response_model=ResourceWithOptional)
def read_item():
return ResourceWithOptional(item=None)
- Start it up in uvicorn
uvicorn main:app &
- Curling that endpoint returns this:
curl -s http://localhost:8000/
INFO: 127.0.0.1:32770 - "GET / HTTP/1.1" 200 OK
{"item":null}
- Generate a client for this:
openapi-python-client generate --url http://localhost:8000/openapi.json
- Look at the generated
resource_with_optional.py
in thefrom_dict
method:
item: Union[ItemResource, Unset] = UNSET
_item = d.pop("item", UNSET)
if not isinstance(_item, Unset):
item = ItemResource.from_dict(_item)
Expected behavior
I expect that this allows for an explicit null
to be returned, as was the case in openapi-python-client==0.7.3, where the generated code looks like this:
item: Union[ItemResource, Unset] = UNSET
_item = d.pop("item", UNSET)
if _item is not None and not isinstance(_item, Unset):
item = ItemResource.from_dict(cast(Dict[str, Any], _item))
OpenAPI Spec File
{"openapi":"3.0.2","info":{"title":"FastAPI","version":"0.1.0"},"paths":{"/":{"get":{"summary":"Read Item","operationId":"read_item__get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResourceWithOptional"}}}}}}}},"components":{"schemas":{"ItemResource":{"title":"ItemResource","required":["id"],"type":"object","properties":{"id":{"title":"Id","type":"integer"}}},"ResourceWithOptional":{"title":"ResourceWithOptional","type":"object","properties":{"item":{"$ref":"#/components/schemas/ItemResource"}}}}}}
Desktop (please complete the following information):
- OS: Ubuntu Linux 20.04
- Python Version: 3.8.6
- openapi-python-client version 0.8.0
Additional context
FastAPI has support for excluding nulls from responses with response_model_exclude_none
but this means changing how our API works to suit the sdk generator, which I would prefer not to have to do.
The new behavior I was referring to seems to have come in with #334