Skip to content

New behavior around optional nested objects breaks deserialization when explicit null is returned #343

Closed
@joshzana

Description

@joshzana

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:

  1. 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)
  1. Start it up in uvicorn
uvicorn main:app &
  1. Curling that endpoint returns this:
curl -s http://localhost:8000/
INFO:     127.0.0.1:32770 - "GET / HTTP/1.1" 200 OK
{"item":null}
  1. Generate a client for this:
openapi-python-client generate --url http://localhost:8000/openapi.json
  1. Look at the generated resource_with_optional.py in the from_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

Metadata

Metadata

Assignees

Labels

🐞bugSomething isn't working

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions