Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FastAPI validation errors for openapi schema #42

Closed
andretheronsa opened this issue Aug 23, 2021 · 14 comments
Closed

FastAPI validation errors for openapi schema #42

andretheronsa opened this issue Aug 23, 2021 · 14 comments

Comments

@andretheronsa
Copy link

andretheronsa commented Aug 23, 2021

Using geojson-pydantic as a FastAPI model attribute type causes a pydantic openapi validation error.

The error is for all coordinates/bbox. I am not sure but it may be related to stricter checking of the coordinate optional numeric values for the openapi schema - previously it would not be able to resolve the items on the docs, but now the docs do not load.

from geojson_pydantic import Point
from pydantic import BaseModel

class TestPoint(BaseModel):
    point: Point

This causes the following exception when attempting to view the fastapi openapi schema docs:

ERROR: Exception in ASGI application
Traceback (most recent call last):
File "d:\projects\vrms\atlis_api\venv\lib\site-packages\uvicorn\protocols\http\httptools_impl.py", line 375, in run_asgi
result = await app(self.scope, self.receive, self.send)
File "d:\projects\vrms\atlis_api\venv\lib\site-packages\uvicorn\middleware\proxy_headers.py", line 75, in call
return await self.app(scope, receive, send)
File "d:\projects\vrms\atlis_api\venv\lib\site-packages\fastapi\applications.py", line 208, in call
await super().call(scope, receive, send)
File "d:\projects\vrms\atlis_api\venv\lib\site-packages\starlette\applications.py", line 112, in call
await self.middleware_stack(scope, receive, send)
File "d:\projects\vrms\atlis_api\venv\lib\site-packages\starlette\middleware\errors.py", line 181, in call
raise exc from None
File "d:\projects\vrms\atlis_api\venv\lib\site-packages\starlette\middleware\errors.py", line 159, in call
await self.app(scope, receive, _send)
File "d:\projects\vrms\atlis_api\venv\lib\site-packages\starlette\middleware\cors.py", line 78, in call
await self.app(scope, receive, send)
File "d:\projects\vrms\atlis_api\venv\lib\site-packages\starlette\exceptions.py", line 82, in call
raise exc from None
File "d:\projects\vrms\atlis_api\venv\lib\site-packages\starlette\exceptions.py", line 71, in call
await self.app(scope, receive, sender)
File "d:\projects\vrms\atlis_api\venv\lib\site-packages\starlette\routing.py", line 580, in call
await route.handle(scope, receive, send)
File "d:\projects\vrms\atlis_api\venv\lib\site-packages\starlette\routing.py", line 241, in handle
await self.app(scope, receive, send)
File "d:\projects\vrms\atlis_api\venv\lib\site-packages\starlette\routing.py", line 52, in app
response = await func(request)
File "d:\projects\vrms\atlis_api\venv\lib\site-packages\fastapi\applications.py", line 161, in openapi
return JSONResponse(self.openapi())
File "d:\projects\vrms\atlis_api\venv\lib\site-packages\fastapi\applications.py", line 136, in openapi
self.openapi_schema = get_openapi(
File "d:\projects\vrms\atlis_api\venv\lib\site-packages\fastapi\openapi\utils.py", line 410, in get_openapi
return jsonable_encoder(OpenAPI(**output), by_alias=True, exclude_none=True) # type: ignore
File "pydantic\main.py", line 406, in pydantic.main.BaseModel.init
pydantic.error_wrappers.ValidationError: 3 validation errors for OpenAPI
components -> schemas -> Point -> properties -> coordinates -> anyOf -> 0 -> items
value is not a valid dict (type=type_error.dict)
components -> schemas -> Point -> properties -> coordinates -> anyOf -> 1 -> items
value is not a valid dict (type=type_error.dict)
components -> schemas -> Point -> $ref
field required (type=value_error.missing)

@andretheronsa andretheronsa changed the title FastAPI calidation errors for openapi FastAPI validation errors for openapi schema Aug 23, 2021
@andretheronsa
Copy link
Author

Update: This is caused by OpenAPI spec not allowing Tuples with exact values inside (for the Coordinate and BBox attributes)(tiangolo/fastapi#466).

I think FastAPI + Geojson Pydantic is an awesome combination so I think it should be looked into.

I was quickly able to fix this by using nested Pydantic models instead - would such a fix be acceptable?

NumType = Union[float, int]

class coord_2d(BaseModel):
    x: NumType
    y: NumType

class coord_3d(BaseModel):
    x: NumType
    y: NumType
    z: NumType


Coordinate = Union[coord_2d, coord_3d]

class BBox_2d(BaseModel):
    tl: coord_2d
    br: coord_2d

class BBox_3d(BaseModel):
    tl: coord_3d
    br: coord_3d


BBox = Union[BBox_2d, BBox_3d]

@kylebarron
Copy link
Member

Note this would be a breaking change because (at least as-is) it would disallow positional arguments like a tuple

Current:

from geojson_pydantic import Point
Point(coordinates=(1, 2)) # Point(coordinates=(1.0, 2.0), type='Point')

New:

from typing import Union, Any
from pydantic import BaseModel, Field
import abc

NumType = Union[float, int]
class coord_2d(BaseModel):
    x: NumType
    y: NumType

class coord_3d(BaseModel):
    x: NumType
    y: NumType
    z: NumType

Coordinate = Union[coord_2d, coord_3d]

class _GeometryBase(BaseModel, abc.ABC):
    """Base class for geometry models"""

    coordinates: Any  # will be constrained in child classes

    @property
    def __geo_interface__(self):
        return self.dict()

class Point(_GeometryBase):
    """Point Model"""

    type: str = Field("Point", const=True)
    coordinates: Coordinate

Point(coordinates=(1, 2))
# ---------------------------------------------------------------------------
# ValidationError                           Traceback (most recent call last)
# <ipython-input-23-ff3c2dca9424> in <module>
# ----> 1 Point(coordinates=(1, 2))
# 
# ~/.pyenv/versions/miniconda3-3.8-4.8.3/lib/python3.8/site-packages/pydantic/main.cpython-38-darwin.so in pydantic.main.BaseModel.__init__()
# 
# ValidationError: 2 validation errors for Point
# coordinates
#   value is not a valid dict (type=type_error.dict)
# coordinates
#   value is not a valid dict (type=type_error.dict)

Point(coordinates={'x': 1, 'y': 2})
# Point(coordinates=coord_2d(x=1.0, y=2.0), type='Point')

@vincentsarago
Copy link
Member

This is really unlucky, but I don't think it's a geojson-pydantic problem. We have to enforce the number of coordinates here and the coordinates are tuples not dict.

Sadly I think this is a fastapi/OpenAPI problem (don't get me wrong I'd love to see this working because I also work a lot with fastapi).

I understand that OpenAPI spec explicitly says that tuples with exact values inside is unsupported but it seems that work has been done to resolve it:
tiangolo/fastapi#3038
pydantic/pydantic#2497

kinda want to know what @geospatial-jeff thinks

@geospatial-jeff
Copy link
Contributor

@vincentsarago OpenAPI doesn't support tuples until version 3.1, looks like FastAPI is not using this version yet.

@vincentsarago
Copy link
Member

FYI I just had the same issue with our TileJSON model which use

    center: Optional[Tuple[float, float, int]]

But the error only happens with the latest fastAPI version, locally with 0.65.1 it works fine

@alexeynick
Copy link

Hi!

I've just met the same issue. I think it'll be possible to make this change in types.py (if using Python 3.9+):

# Python 3.9
BBox = Union[
    Annotated[List[NumType], 4],  # 2D bbox
    Annotated[List[NumType], 6]  # 3D bbox
]

Position = Union[Annotated[List[NumType], 2], Annotated[List[NumType], 3]]

As for me this helped also:

BBox = Union[
    List[NumType],  # 2D bbox
    List[NumType]  # 3D bbox
]

Position = Union[List[NumType], List[NumType]]

I know that it's not correct to use List instead of Tuple, but this helped me to use fastAPI+pydantic+SQLAlchemy.

@muety
Copy link

muety commented Dec 20, 2021

Could somebody have a look, please? :)

@vincentsarago
Copy link
Member

@muety this has been looked at already and is not something that can be fixed until swagger-api/swagger-ui#5891 and tiangolo/fastapi#3038 get resolved

if you know another way, I'll be pleased to learn 🙏

@pt-cervest
Copy link

Hi,

First of all, many thanks for the lib. Really appreciate it. I met with the same issue and the temporary solution is similar to @alexeynick with a little variance.

from typing import List, Union

from pydantic import Field
from typing_extensions import Annotated

NumType = Union[float, int]

Position = Annotated[List[NumType], Field(min_items=2, max_items=3)]

This ensures that the list length is constrained.

Hope the issue is fixed on FastAPI side! 🙂

@vincentsarago
Copy link
Member

vincentsarago commented Dec 21, 2021

I'm trying to follow the different solution but I'm not sure it will work. here are couple notes:

First, using Annotated List will change the type from Tuple to List which is kinda a breaking change, but I think I'll be 👌 with this.

  1. Using pydantic Field

✅ schema
✅ validation

from typing import List, Union
from typing_extensions import Annotated
from pydantic import BaseModel, Field

import pydantic

pydantic.__version__
>> '1.9.0a1'

NumType = Union[float, int]
Position = Annotated[List[NumType], Field(min_items=2, max_items=3)]


class Point(BaseModel):

    coordinates: Position

print(Point.schema_json())
>> {
    "title": "Point",
    "type": "object",
    "properties": {
        "coordinates": {
            "title": "Coordinates",
            "minItems": 2,
            "maxItems": 3,
            "type": "array",
            "items": {
                "anyOf": [
                    {
                        "type": "number"
                    },
                    {
                        "type": "integer"
                    }
                ]
            }
        }
    },
    "required": [
        "coordinates"
    ]
}


Point(coordinates=[0.0])
>> ValidationError: 1 validation error for Point
coordinates
  ensure this value has at least 2 items (type=value_error.list.min_items; limit_value=2)
  1. Pure type

🚫 schema
🚫 validation

from typing import List, Union
from typing_extensions import Annotated
from pydantic import BaseModel
import pydantic

pydantic.__version__
>> '1.9.0a1'

NumType = Union[float, int]
Position = Union[Annotated[List[NumType], 2], Annotated[List[NumType], 3]]


class Point(BaseModel):

    coordinates: Position

print(Point.schema_json())

# Not the schema we expect
>> {
    "title": "Point",
    "type": "object",
    "properties": {
        "coordinates": {
            "title": "Coordinates",
            "anyOf": [
                {
                    "type": "array",
                    "items": {
                        "anyOf": [
                            {
                                "type": "number"
                            },
                            {
                                "type": "integer"
                            }
                        ]
                    }
                },
                {
                    "type": "array",
                    "items": {
                        "anyOf": [
                            {
                                "type": "number"
                            },
                            {
                                "type": "integer"
                            }
                        ]
                    }
                }
            ]
        }
    },
    "required": [
        "coordinates"
    ]
}

Point(coordinates=[0.0])  # Should raise an error, but is not 
>> Point(coordinates=[0.0])

Option 1 (pydantic field), is definitely better than using pure type but it change the Position type to a pydantic object (FieldInfo) 🤷‍♂️

even if Option 1 seems to be working, we can't use it for BBOX

BBox = Union[
    Annotated[List[NumType], Field(min_items=4, max_items=4)],  # 2D bbox
    Annotated[List[NumType], Field(min_items=6, max_items=6)]  # 3D bbox
]

This is not a valid expression 🤷

@pt-cervest
Copy link

pt-cervest commented Jan 25, 2022

Fix bug preventing to use OpenAPI when using tuples. PR #3874

New release of FastAPI 0.73.0 seems to have been shipped with the fix.
Though I haven't gotten chance to try it.

@vincentsarago
Copy link
Member

🥳 thanks for the update @pt-cervest 🙏

I guess we can close the issue then

@vincentsarago
Copy link
Member

considering that this is now fixed!

@RaczeQ
Copy link

RaczeQ commented Apr 9, 2022

I'm trying to follow the different solution but I'm not sure it will work. here are couple notes:

First, using Annotated List will change the type from Tuple to List which is kinda a breaking change, but I think I'll be ok_hand with this.

  1. Using pydantic Field

white_check_mark schema white_check_mark validation

from typing import List, Union
from typing_extensions import Annotated
from pydantic import BaseModel, Field

import pydantic

pydantic.__version__
>> '1.9.0a1'

NumType = Union[float, int]
Position = Annotated[List[NumType], Field(min_items=2, max_items=3)]


class Point(BaseModel):

    coordinates: Position

print(Point.schema_json())
>> {
    "title": "Point",
    "type": "object",
    "properties": {
        "coordinates": {
            "title": "Coordinates",
            "minItems": 2,
            "maxItems": 3,
            "type": "array",
            "items": {
                "anyOf": [
                    {
                        "type": "number"
                    },
                    {
                        "type": "integer"
                    }
                ]
            }
        }
    },
    "required": [
        "coordinates"
    ]
}


Point(coordinates=[0.0])
>> ValidationError: 1 validation error for Point
coordinates
  ensure this value has at least 2 items (type=value_error.list.min_items; limit_value=2)
  1. Pure type

no_entry_sign schema no_entry_sign validation

from typing import List, Union
from typing_extensions import Annotated
from pydantic import BaseModel
import pydantic

pydantic.__version__
>> '1.9.0a1'

NumType = Union[float, int]
Position = Union[Annotated[List[NumType], 2], Annotated[List[NumType], 3]]


class Point(BaseModel):

    coordinates: Position

print(Point.schema_json())

# Not the schema we expect
>> {
    "title": "Point",
    "type": "object",
    "properties": {
        "coordinates": {
            "title": "Coordinates",
            "anyOf": [
                {
                    "type": "array",
                    "items": {
                        "anyOf": [
                            {
                                "type": "number"
                            },
                            {
                                "type": "integer"
                            }
                        ]
                    }
                },
                {
                    "type": "array",
                    "items": {
                        "anyOf": [
                            {
                                "type": "number"
                            },
                            {
                                "type": "integer"
                            }
                        ]
                    }
                }
            ]
        }
    },
    "required": [
        "coordinates"
    ]
}

Point(coordinates=[0.0])  # Should raise an error, but is not 
>> Point(coordinates=[0.0])

Option 1 (pydantic field), is definitely better than using pure type but it change the Position type to a pydantic object (FieldInfo) man_shrugging

even if Option 1 seems to be working, we can't use it for BBOX

BBox = Union[
    Annotated[List[NumType], Field(min_items=4, max_items=4)],  # 2D bbox
    Annotated[List[NumType], Field(min_items=6, max_items=6)]  # 3D bbox
]

This is not a valid expression shrug

Hi, I have also encountered the problem mentioned here (although I am using the latest version of FastAPI). While the application works and the types match, my problem is in generating the client correctly based on the OpenAPI documentation (using openapi-python-client tool). Generally, the documentation can't generate the field definitions correctly and consequently, the generators don't work.
image
image

❯ openapi-python-client update --url http://localhost:8000/openapi.json --config openapi_config.yaml
Error(s) encountered while generating, client was not created

Failed to parse OpenAPI document

73 validation errors for OpenAPI
components -> schemas -> Feature -> $ref
  field required (type=value_error.missing)
components -> schemas -> Feature -> properties -> bbox -> $ref
  field required (type=value_error.missing)
components -> schemas -> Feature -> properties -> bbox -> anyOf -> 0 -> $ref
  field required (type=value_error.missing)
components -> schemas -> Feature -> properties -> bbox -> anyOf -> 0 -> items
  value is not a valid dict (type=type_error.dict)
components -> schemas -> Feature -> properties -> bbox -> anyOf -> 0 -> items
  value is not a valid dict (type=type_error.dict)
components -> schemas -> Feature -> properties -> bbox -> anyOf -> 1 -> $ref
  field required (type=value_error.missing)
components -> schemas -> Feature -> properties -> bbox -> anyOf -> 1 -> items
  value is not a valid dict (type=type_error.dict)
components -> schemas -> Feature -> properties -> bbox -> anyOf -> 1 -> items
  value is not a valid dict (type=type_error.dict)
...

I made a simple change inspired by the above discussion and the generation started working, without the need to use the new definitions from Python 3.9.

BBox = Union[
    conlist(NumType, min_items=4, max_items=4),  # 2D bbox
    conlist(NumType, min_items=6, max_items=6),  # 3D bbox
]
Position = Union[
    conlist(NumType, min_items=2, max_items=2),
    conlist(NumType, min_items=3, max_items=3),
]

image

Part of generated client model for python:

class Feature:
    """Feature Model

    Attributes:
        type (Union[Unset, str]):
        geometry (Union[LineString, MultiLineString, MultiPoint, MultiPolygon, Point, Polygon, Unset]):
        properties (Union[Unset, FeatureProperties]):
        id (Union[Unset, str]):
        bbox (Union[List[Union[float, int]], Unset]):
    """

    type: Union[Unset, str] = UNSET
    geometry: Union[LineString, MultiLineString, MultiPoint, MultiPolygon, Point, Polygon, Unset] = UNSET
    properties: Union[Unset, FeatureProperties] = UNSET
    id: Union[Unset, str] = UNSET
    bbox: Union[List[Union[float, int]], Unset] = UNSET
    ...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

8 participants