Fastapi: [QUESTION] Why not introduce pydantic @validator in docs?

Created on 14 Jun 2019  路  5Comments  路  Source: tiangolo/fastapi

Description

I am new to fastapi and web APIs in general, but found it astonishing simple and fast to create a first working api. In the process, I found using pydantic @validators very useful for custom class validation. I was wondering why this is not introduced in the docs. Perhaps the use of @validators is discouraged and there's a better way?

Here's a minimum working example to discuss. If this is the right way to do it, I can create a pull request to add it to the docs. Otherwise, I would be grateful for any hint towards a better approach to do this:

from fastapi import FastAPI
from pydantic import BaseModel, validator, BaseConfig
from geoalchemy2 import WKTElement

app = FastAPI()


class Coordinates(BaseModel):
    lat: float = 0
    lng: float = 0

    @validator('lat')
    def lat_within_range(cls, v):
        if not -90 <= v <= 90:
            raise ValueError('Latitude outside allowed range')
        return v

    @validator('lng')
    def lng_within_range(cls, v):
        if not -180 <= v <= 180:
            raise ValueError('Longitude outside allowed range')
        return v

class UserIn(BaseModel):
    username: str
    coordinates: Coordinates


class UserOut(BaseModel):
    username: str


class UserInDB(BaseModel):
    username: str
    coordinates_geom: WKTElement

    class Config(BaseConfig):
        arbitrary_types_allowed = True

def get_geom_from_coordinates(coordinates: Coordinates):
    geom_wkte = WKTElement(
        f"Point ({coordinates.lng} {coordinates.lat})",
        srid=4326, extended=True)
    return geom_wkte


def fake_save_user(user_in: UserIn):
    coordinates_geom = get_geom_from_coordinates(user_in.coordinates)
    user_in_db = UserInDB(**user_in.dict(), coordinates_geom=coordinates_geom)
    print("User saved! ..not really")
    return user_in_db

@app.post("/user/", response_model=UserOut)
async def create_user(*, user_in: UserIn):
    user_saved = fake_save_user(user_in)
    return user_saved

Explanation
In the example above, there are two steps where I make use of pydantic, validation (1) and conversion (2).

First (1), I need to validate coordinates, which are usually submitted from the frontend using Latitude and Longitude as floats. Then (2), I need to store these in the database in a special custom format, for which no validator is available from pydantic (e.g. as WKTElement from geoalchemy2, which is then stored as PostGis Geometry in the db). To summarize the use of pydantic:

  • Validation (1): I need a check for input lat/lng range, that's where I make use of the @validator above
  • Conversion (2): I need conversion of input lat/lng values to the custom format WKTElement used to store values in the db, that's the function get_geom_from_coordinates, which constructs a WellKnownText (WKT) representation of lat/lng - this requires making use of pydantic class Config to enable arbitrary_types_allowed

Here is a gist with a complete example that also includes loading of geometry to coordinates conversion.

question

Most helpful comment

Thanks for the insight @Sieboldianus !

For range validations you can use Schema, it's a bit less verbose and also generates the corresponding JSON Schema (used by OpenAPI). That way, even the Swagger UI will complain if the values provided are not in range.

You could do:

from pydantic import BaseModel, Schema

class Coordinates(BaseModel):
    lat: float = Schema(0, gte=-90, lte=90)
    lng: float = Schema(0, gte=-180, lte=180)

Nevertheless, there are other more complex scenarios that benefit from @validator. Although I consider them more of a "power user" feature.

All 5 comments

Perhaps the use of @validators is discouraged and there's a better way?

I think @validator is a great way to do data validation, especially with FastAPI -- you get clear error responses for free! It's probably worth a section in the documentation.

Yep!

Here's how the input looks in docs:
input_latlng

.. and, for the following example input, outside the allowed range:

{
  "username": "sieboldianus",
  "coordinates": {
    "lat": 40,
    "lng": 181
  }
}

.. the output:

output_validation

(for later, if a figure in the docs is needed, and to illustrate the awesomeness of fastapi)

Thanks for the insight @Sieboldianus !

For range validations you can use Schema, it's a bit less verbose and also generates the corresponding JSON Schema (used by OpenAPI). That way, even the Swagger UI will complain if the values provided are not in range.

You could do:

from pydantic import BaseModel, Schema

class Coordinates(BaseModel):
    lat: float = Schema(0, gte=-90, lte=90)
    lng: float = Schema(0, gte=-180, lte=180)

Nevertheless, there are other more complex scenarios that benefit from @validator. Although I consider them more of a "power user" feature.

Thanks for the insight! I already used Schema but somehow didn't connect the pieces in this example.
Also, great to see that ORM models from pydantic are now supported in Fastapi 0.3.0, this makes it much easier now to use native Geometry types from Geoalchemy2. It was a pain to code around this to allow me to use arbitrary_types_allowed = True in my initial example. Hope I can provide a brief guide on how I used Postgis and Geoalchemy2 with Fastapi soon..

That sounds great!

As this is a more advanced topic for the specific field, I think it would fit better in an external blog-post, article. But I encourage you to write it and share it with the community (e.g. in the Gitter chat) :smile: :tada: :memo:

I also have to create a section that links to external articles, I just haven't done it yet.


As I understand the problem is solved, I'll close the issue now. But feel free to add more comments or create new issues.

Was this page helpful?
0 / 5 - 0 ratings