Fastapi: [BUG] openapi.json fails to be generated for nested models

Created on 12 Jul 2019  Â·  10Comments  Â·  Source: tiangolo/fastapi

Describe the bug
Fails to autogenerate docs.

To Reproduce

  1. Create a response model with other nested models and try to generate openapi.json or open autogenerated docs.
  2. Application throws an error

Expected behavior
Generate openapi.json and open autogenerated docs without problems.

Environment:

  • OS: macOS
  • FastAPI Version: 0.30.0
  • Python version: 3.7.3

Additional context

My models look like this:

class PaginatedItems(Generic[Item], BaseModel, abc.ABC):
    items: List[Any]
    has_after: bool = False
    has_before: bool = False

class OrderUser(BaseModel):
    name: Optional[str] = None
    email: EmailStr


class Order(BaseModel):
    order_id: UUID
    amount: Decimal
    currency: str

    # Users part
    merchants: List[OrderUser]
    handlers: List[OrderUser]

class PaginatedOrders(PaginatedItems[Order]):
    items: List[Order] = list()

My route definition looks like that:

@router.get(orders_uri,
            response_model=PaginatedOrders
async def list():
    ...

This results in the following exception when trying to open autogenerated docs:

pydantic.error_wrappers.ValidationError: 4 validation errors
schemas -> OrderUser
  value is not a valid dict (type=type_error.dict)
schemas -> OrderUser
  value is not a valid dict (type=type_error.dict)
components -> schemas
  value is not none (type=type_error.none.allowed)
components
  value is not none (type=type_error.none.allowed)

When I comment out the response_model=PaginatedOrders part from route definition everything works, but docs obviously miss response type.

bug

Most helpful comment

oups sorry I think your mistake is putting response_model=SimilarProducts, in the wrong spot, it's in the @router part

All 10 comments

I believe this was very recently fixed in the most recent version of pydantic (not yet supported by FastAPI, if I recall correctly). I'm currently using the following as a workaround:

# my_fast_api.py

from typing import Any, Dict, Sequence, Set, Type

from fastapi import FastAPI, routing
from fastapi.encoders import jsonable_encoder
from fastapi.openapi.constants import REF_PREFIX
from fastapi.openapi.models import OpenAPI
from fastapi.openapi.utils import get_openapi_path
from fastapi.utils import get_flat_models_from_routes
from pydantic import BaseModel
from pydantic.schema import get_model_name_map, model_process_schema
from starlette.routing import BaseRoute


def get_model_definitions(
    *, flat_models: Set[Type[BaseModel]], model_name_map: Dict[Type[BaseModel], str]
) -> Dict[str, Any]:
    definitions: Dict[str, Dict[Any, Any]] = {}
    for model in flat_models:
        m_schema, m_definitions = model_process_schema(model, model_name_map=model_name_map, ref_prefix=REF_PREFIX)
        # definitions.update(m_definitions)
        model_name = model_name_map[model]
        definitions[model_name] = m_schema
    return definitions


def get_openapi(
    *,
    title: str,
    version: str,
    openapi_version: str = "3.0.2",
    description: str = None,
    routes: Sequence[BaseRoute],
    openapi_prefix: str = "",
) -> Dict[str, Any]:
    info = {"title": title, "version": version}
    if description:
        info["description"] = description
    output = {"openapi": openapi_version, "info": info}
    components: Dict[str, Dict[str, Any]] = {}
    paths: Dict[str, Dict[str, Any]] = {}
    flat_models = get_flat_models_from_routes(routes)
    model_name_map = get_model_name_map(flat_models)
    definitions = get_model_definitions(flat_models=flat_models, model_name_map=model_name_map)
    for route in routes:
        if isinstance(route, routing.APIRoute):
            result = get_openapi_path(route=route, model_name_map=model_name_map)
            if result:
                path, security_schemes, path_definitions = result
                if path:
                    paths.setdefault(openapi_prefix + route.path_format, {}).update(path)
                if security_schemes:
                    components.setdefault("securitySchemes", {}).update(security_schemes)
                if path_definitions:
                    definitions.update(path_definitions)
    if definitions:
        components.setdefault("schemas", {}).update(definitions)
    if components:
        output["components"] = components
    output["paths"] = paths
    openapi = OpenAPI(**output)
    return jsonable_encoder(openapi, by_alias=True, include_none=False)


class MyFastAPI(FastAPI):
    def openapi(self) -> Dict[str, Any]:
        if not self.openapi_schema:
            self.openapi_schema = get_openapi(
                title=self.title,
                version=self.version,
                openapi_version=self.openapi_version,
                description=self.description,
                routes=self.routes,
                openapi_prefix=self.openapi_prefix,
            )
        return self.openapi_schema

I then import MyFastAPI as FastAPI; I'll swap it out once this is fixed again.

(Basically the only change is the commented line in get_model_definitions which is currently overwriting previously computed schemas with None in some situations involving nested models.)

I am not 100% sure that this handles all edge cases properly, but it generated everything correctly in my api (many models of varied shape and nesting).

Thanks for the help @dmontagu!

The latest Pydantic is now included/supported in the latest FastAPI 0.33.0.

Can you update and check?

@tiangolo yes, I’m happy to say I’ve removed the above workaround from my projects. Thanks!

I can confirm it now works fine! Thanks for fixing it so quickly!

@tiangolo I am facing a similar issue,

my code is currently like this -

class SimilarProducts(BaseModel):
    count: int
    productIds: List[str]

@router.get("/api/v1/similar/products/{user_id}/{product_id}")
async def get_similar_products(
    user_id: str = Path(..., title="userid"),
    product_id: str = Path(..., title="productid"),
    response_model=SimilarProducts, 
):
    try:
        dummy_result = {
            "count": 1,
            "productIds": [product_id]
        }
        return dummy_result
    except Exception as e:
        raise HTTPException(status_code=202, detail="Exception: {}".format(e))

When trying to open /docs I get this error -

TypeError: Object of type 'MetaModel' is not JSON serializable

I'm using fastapi version 0.33.0
pydantic version 0.30

what is MetaModel

On Tue, Jul 23, 2019 at 10:42 AM Dhruv Karan notifications@github.com
wrote:

@tiangolo https://github.com/tiangolo I am facing a similar issue,

my code is currently like this -

class SimilarProducts(BaseModel):
count: int
productIds: List[str]

@router.get("/api/v1/similar/products/{user_id}/{product_id}")
async def get_similar_products(
user_id: str = Path(..., title="userid"),
product_id: str = Path(..., title="productid"),
response_model=SimilarProducts,
):
try:
dummy_result = {
"count": 1,
"productIds": [product_id]
}
return dummy_result
except Exception as e:
raise HTTPException(status_code=202, detail="Exception: {}".format(e))

When trying to open /docs I get this error -

TypeError: Object of type 'MetaModel' is not JSON serializable

—
You are receiving this because you are subscribed to this thread.
Reply to this email directly, view it on GitHub
https://github.com/tiangolo/fastapi/issues/383?email_source=notifications&email_token=AAINSPRBJXVZV76AGEKOBU3QA276LA5CNFSM4ICBLKDKYY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOD2SMCDI#issuecomment-514113805,
or mute the thread
https://github.com/notifications/unsubscribe-auth/AAINSPXWMX55R3KMDZTTURDQA276LANCNFSM4ICBLKDA
.

--
benoit barthelet
http://pgp.mit.edu/pks/lookup?op=get&search=0xF150E01A72F6D2EE

@euri10 it's not there in my code, I thought it was something inside Pydantic?

oups sorry I think your mistake is putting response_model=SimilarProducts, in the wrong spot, it's in the @router part

oh yes, it works, my bad.
thanks! @euri10

Thanks @dmontagu and @LKay for reporting back (and closing the issue).

Thanks @euri10 for your help! I'm glad it works now @unography.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

DrPyser picture DrPyser  Â·  3Comments

kkinder picture kkinder  Â·  3Comments

zero0nee picture zero0nee  Â·  3Comments

laith43d picture laith43d  Â·  3Comments

tsdmrfth picture tsdmrfth  Â·  3Comments