Skip to content

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

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 shows no attributes for models. #150

Closed
TNick opened this issue Nov 16, 2023 · 9 comments
Closed

FastAPI shows no attributes for models. #150

TNick opened this issue Nov 16, 2023 · 9 comments

Comments

@TNick
Copy link

TNick commented Nov 16, 2023

This simple FastAPI script:

from fastapi import FastAPI
from geojson_pydantic import FeatureCollection

app = FastAPI(
    title="Test geojson-pydantic",
    version="1.0"
)

@app.get("/{dataset}", response_model=FeatureCollection)
def get_dataset(dataset: str):
    raise NotImplementedError

Creates this result in /docs:
image

See here for a simple project.

Edit:

  • fastapi==0.104.1
  • pydantic==2.5.1
  • uvicorn==0.24.0.post1
  • geojson-pydantic==1.0.1
@vincentsarago
Copy link
Member

can you add another pydantic model (outside geojson-pydantic) because I feel this is a pydantic issue not directly a geojson-pydantic one

can you try with pydantic==2.4.1 also

@TNick
Copy link
Author

TNick commented Nov 16, 2023

I've edited the code:

from fastapi import FastAPI
from geojson_pydantic import FeatureCollection
from pydantic import BaseModel, Field


class SomeModel(BaseModel):
    lorem: str = Field(..., description="describe me")
    ipsum: int


app = FastAPI(
    title="Test geojson-pydantic",
    version="1.0"
)


@app.get("/{dataset}", response_model=FeatureCollection)
def get_dataset(dataset: str):
    raise NotImplementedError


@app.get("/another/{dataset}", response_model=SomeModel)
def get_another_dataset(dataset: str):
    raise NotImplementedError

which results in:

image

@TNick
Copy link
Author

TNick commented Nov 16, 2023

No change with pydantic==2.4.1.

@vincentsarago
Copy link
Member

There's definitely something going one with fastapi/pydantic

if you add FeatureCollection as input model, it appears fine 🤷

from fastapi import FastAPI, Body
from geojson_pydantic import FeatureCollection
from pydantic import BaseModel, Field
from typing_extensions import Annotated

class SomeModel(BaseModel):
    lorem: str = Field(..., description="describe me")
    ipsum: int


app = FastAPI(
    title="Test geojson-pydantic",
    version="1.0"
)


@app.get("/{dataset}", response_model=FeatureCollection)
def get_dataset(dataset: str):
    raise NotImplementedError


@app.get("/another/{dataset}", response_model=SomeModel)
def get_another_dataset(dataset: str):
    raise NotImplementedError


@app.get("/{dataset}", response_model=FeatureCollection)
def get_dataset(body: Annotated[FeatureCollection, Body()]):
    raise NotImplementedError

@vincentsarago
Copy link
Member

      "FeatureCollection-Input": {
        "properties": {
          "bbox": {
            "anyOf": [
              {
                "prefixItems": [
                  {
                    "type": "number"
                  },
                  {
                    "type": "number"
                  },
                  {
                    "type": "number"
                  },
                  {
                    "type": "number"
                  }
                ],
                "type": "array",
                "maxItems": 4,
                "minItems": 4
              },
              {
                "prefixItems": [
                  {
                    "type": "number"
                  },
                  {
                    "type": "number"
                  },
                  {
                    "type": "number"
                  },
                  {
                    "type": "number"
                  },
                  {
                    "type": "number"
                  },
                  {
                    "type": "number"
                  }
                ],
                "type": "array",
                "maxItems": 6,
                "minItems": 6
              },
              {
                "type": "null"
              }
            ],
            "title": "Bbox"
          },
          "type": {
            "const": "FeatureCollection",
            "title": "Type"
          },
          "features": {
            "items": {
              "$ref": "#/components/schemas/Feature-Input"
            },
            "type": "array",
            "title": "Features"
          }
        },
        "type": "object",
        "required": [
          "type",
          "features"
        ],
        "title": "FeatureCollection",
        "description": "FeatureCollection Model"
      },
      "FeatureCollection-Output": {
        "type": "object",
        "title": "FeatureCollection",
        "description": "FeatureCollection Model"
      },

interesting that when input the schema is within the openapi docs but not for the output

@TNick
Copy link
Author

TNick commented Nov 16, 2023

Replacing _GeoJsonBase with BaseModel seems to generate correct output.

from typing import Any, Dict, Generic, Iterator, List, Literal, Optional, TypeVar, Union

from pydantic import BaseModel, Field, StrictInt, StrictStr, field_validator

# from geojson_pydantic.base import _GeoJsonBase
from geojson_pydantic.geometries import Geometry

Props = TypeVar("Props", bound=Union[Dict[str, Any], BaseModel])
Geom = TypeVar("Geom", bound=Geometry)


class Feature(BaseModel, Generic[Geom, Props]):
    """Feature Model"""

    type: Literal["Feature"]
    geometry: Union[Geom, None] = Field(...)
    properties: Union[Props, None] = Field(...)
    id: Optional[Union[StrictInt, StrictStr]] = None

    __geojson_exclude_if_none__ = {"bbox", "id"}

    @field_validator("geometry", mode="before")
    def set_geometry(cls, geometry: Any) -> Any:
        """set geometry from geo interface or input"""
        if hasattr(geometry, "__geo_interface__"):
            return geometry.__geo_interface__

        return geometry


Feat = TypeVar("Feat", bound=Feature)


class FeatureCollection(BaseModel, Generic[Feat]):
    """FeatureCollection Model"""

    type: Literal["FeatureCollection"]
    features: List[Feat]

    def __iter__(self) -> Iterator[Feat]:  # type: ignore [override]
        """iterate over features"""
        return iter(self.features)

    def __len__(self) -> int:
        """return features length"""
        return len(self.features)

    def __getitem__(self, index: int) -> Feat:
        """get feature at a given index"""
        return self.features[index]

@vincentsarago
Copy link
Member

narrowing down this to

@model_serializer(when_used="always", mode="wrap")
def clean_model(self, serializer: Any, info: SerializationInfo) -> Dict[str, Any]:
"""Custom Model serializer to match the GeoJSON specification.
Used to remove fields which are optional but cannot be null values.
"""
# This seems like the best way to have the least amount of unexpected consequences.
# We want to avoid forcing values in `exclude_none` or `exclude_unset` which could
# cause issues or unexpected behavior for downstream users.
# ref: https://github.com/pydantic/pydantic/issues/6575
data: Dict[str, Any] = serializer(self)
# Only remove fields when in JSON mode.
if info.mode_is_json():
for field in self.__geojson_exclude_if_none__:
if field in data and data[field] is None:
del data[field]
return data

if we remove ☝️ it's fine!

@TNick
Copy link
Author

TNick commented Nov 16, 2023

:)), yep, I just got there myself.

@vincentsarago
Copy link
Member

vincentsarago commented Nov 16, 2023

confirmed this is not a geojson-pydantic issue. It's more a FastAPI (or a pydantic) one!

from typing import Any, Dict
from fastapi import FastAPI
from pydantic import BaseModel, Field, SerializationInfo, model_serializer


class SomeModel(BaseModel):
    lorem: str = Field(..., description="describe me")
    ipsum: int

    @model_serializer(when_used="always", mode="wrap")
    def clean_model(self, serializer: Any, info: SerializationInfo) -> Dict[str, Any]:
        data: Dict[str, Any] = serializer(self)
        return data

app = FastAPI()

@app.get("/another/{dataset}", response_model=SomeModel)
def get_another_dataset(dataset: str):
    raise NotImplementedError

@developmentseed developmentseed locked and limited conversation to collaborators Nov 16, 2023
@vincentsarago vincentsarago converted this issue into discussion #151 Nov 16, 2023

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants