How to disable schema checking in FastAPI? - python

I am migrating a service from Flask to FastAPI and using Pydantic models to generate the documentation. However, I'm a little unsure about the schema check. I'm afraid there will be some unexpected data (like a different field format) and it will return an error.
In the Pydantic documentation there are ways to create a model without checking the schema: https://pydantic-docs.helpmanual.io/usage/models/#creating-models-without-validation
However, as this is apparently instantiated by FastAPI itself, I don't know how to disable this schema check when returning from FastAPI.

You could return Response directly, or with using custom responses for automatic convertion. In this case, response data is not validated against the response model. But you can still document it as described in Additional Responses in OpenAPI.
Example:
class SomeModel(BaseModel):
num: int
#app.get("/get", response_model=SomeModel)
def handler(param: int):
if param == 1: # ok
return {"num": "1"}
elif param == 2: # validation error
return {"num": "not a number"}
elif param == 3: # ok (return without validation)
return JSONResponse(content={"num": "not a number"})
elif param == 4: # ok (return without validation and conversion)
return Response(content=json.dumps({"num": "not a number"}), media_type="application/json")

You can set the request model as a typing.Dict or typing.List
from typing import Dict
app.post('/')
async def your_function(body: Dict):
return { 'request_body': body}

FastAPI doesn't enforce any kind of validation, so if you don't want it, simply do not use Pydantic models or type hints.
app.get('/')
async def your_function(input_param):
return { 'param': input_param }
# Don't use models or type hints when defining the function params.
# `input_param` can be anything, no validation will be performed.
However, as #Tryph rightly pointed out, since you're using Pydantic to generate the documentation, you could simply use the Any type like so:
from typing import Any
from pydantic import BaseModel
class YourClass(BaseModel):
any_value: Any
Beware that the Any type also accepts None, making in fact the field optional. (See also typing.Any in the Pydantic docs)

Related

Graphene "InputObjectType" causing encoding issues with JSONField

I have the following Django model with a JSONField inside:
from django_jsonfield_backport import models as mysql
class RaceReport(TimeStampedModel):
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='race_reports')
parameters = mysql.JSONField(blank=True, default=dict)
Then, there's a custom InputObjectType which looks like this:
class ParametersInput(graphene.InputObjectType):
start_point = BigInteger(required=True)
end_point = BigInteger(required=True)
data_type = DataType()
I am using the above input type inside the following Mutation:
class CreateRaceReport(graphene.Mutation):
class Arguments:
parameters = ParametersInput(required=True)
def mutate(_root, info, parameters): # noqa: N805
race_report = RaceReport.objects.create(user=info.context.user, parameters=parameters)
return CreateRaceReport(race_report=race_report)
A few points to note before proceeding further:
The main purpose for having this custom input is to validate what gets stored in my model's JSONField.
I want to allow my Frontend app to pass javascript objects as parameters instead of passing JSON strings.
I can not use the GenericScalar type as mentioned here because it accepts anything and requires custom validations.
Here's what I've tried/discovered so far:
On executing the mutation, it throws this Object of type proxy is not JSON serializable on the following line:
race_report = RaceReport.objects.create(user=info.context.user, parameters=parameters)
Tried converting it to dictionary by using parameters.__dict__ method but got the same error.
Tried a manaul json.dumps(parameters) but still same results.
Did some research and found out that the JSONField uses the default json encoders and decoders provided by python's built-in json package.
Thought something is wrong with the JSONEncoder itself and tried DjangoJSONEncoder, and finally it worked!
Now my mutation looks like this:
def mutate(_root, info, parameters): # noqa: N805
parameters = json.dumps(parameters, cls=DjangoJSONEncoder)
parameters = json.loads(parameters)
race_report = RaceReport.objects.create(user=info.context.user, parameters=parameters)
return CreateRaceReport(race_report=race_report)
The JSONField accepts custom encoder and decoder classes. But it starts throwing the same error on trying something like this:
JSONField(blank=True, default=dict, encoder=DjangoJSONEncoder)
Versions:
Python 3.7
Django 2.2.28
Graphene 2.1.8
Is there a better way to do this?

How to access a FastAPI Depends value from a Pydantic validator?

Let's say I have a route that allows clients to create a new user
(pseudocode)
#app.route("POST")
def create_user(user: UserScheme, db: Session = Depends(get_db)) -> User:
...
and my UserScheme accepts a field such as an email. I would like to be able to set some settings (for example max_length) globally in a different model Settings. How do I access that inside a scheme? I'd like to access the db inside my scheme.
So basically my scheme should look something like this (the given code does not work):
class UserScheme(BaseModel):
email: str
#validator("email")
def validate_email(cls, value: str) -> str:
settings = get_settings(db) # `db` should be set somehow
if len(value) > settings.email_max_length:
raise ValueError("Your mail might not be that long")
return value
I couldn't find a way to somehow pass db to the scheme. I was thinking about validating such fields (that depend on db) inside my route. While this approach works somehow, the error message itself is not raised on the specific field but rather on the entire form, but it should report the error for the correct field so that frontends can display it correctly.
One option to accept arbitrary JSON objects as input, and then construct a UserScheme instance manually inside the route handler:
#app.route(
"POST",
response_model=User,
openapi_extra={
"requestBody": {
"content": {
"application/json": {
"schema": UserScheme.schema(ref_template="#/components/schemas/{model}")
}
}
}
},
)
def create_user(request: Request, db: Session = Depends(get_db)) -> User:
settings = get_settings(db)
user_data = request.json()
user_schema = UserScheme(settings, **user_data)
Note that this idea was borrowed from https://stackoverflow.com/a/68815913/2954547, and I have not tested it myself.
In order to facilitate the above, you might want to redesign this class so that the settings object itself as an attribute on the UserScheme model, which means that you don't ever need to perform database access or other effectful operations inside the validator, while also preventing you from instantiating a UserScheme without some kind of sensible settings in place, even if they are fallbacks or defaults.
class SystemSettings(BaseModel):
...
def get_settings(db: Session) -> SystemSettings:
...
EmailAddress = typing.NewType('EmailAddress', st)
class UserScheme(BaseModel):
settings: SystemSettings
if typing.TYPE_CHECKING:
email: EmailAddress
else:
email: str | EmailAddress
#validator("email")
def _validate_email(cls, value: str, values: dict[str, typing.Any]) -> EmailAddress:
if len(value) > values['settings'].max_email_length:
raise ValueError('...')
return EmailAddress(value)
The use of tyipng.NewType isn't necessary here, but I think it's a good tool in situations like this. Note that the typing.TYPE_CHECKING trick is required to make it work, as per https://github.com/pydantic/pydantic/discussions/4823.

How to document default None/null in OpenAPI/Swagger using FastAPI?

Using a ORM, I want to do a POST request letting some fields with a null value, which will be translated in the database for the default value specified there.
The problem is that OpenAPI (Swagger) docs, ignores the default None and still prompts a UUID by default.
from fastapi import FastAPI
from pydantic import BaseModel
from typing import Optional
from uuid import UUID
import uvicorn
class Table(BaseModel):
# ID: Optional[UUID] # the docs show a example UUID, ok
ID: Optional[UUID] = None # the docs still shows a uuid, when it should show a null or valid None value.
app = FastAPI()
#app.post("/table/", response_model=Table)
def create_table(table: Table):
# here we call to sqlalchey orm etc.
return 'nothing important, the important thing is in the docs'
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=8000)
In the OpenAPI schema example (request body) which is at the docs we find:
{
"ID": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
}
This is not ok, because I specified that the default value is None,so I expected this instead:
{
"ID": null, # null is the equivalent of None here
}
Which will pass a null to the ID and finally will be parsed in the db to the default value (that is a new generated UUID).
When you declare Optional parameters, users shouldn't have to include those parameters in their request (specified with null value) in order to be null. The default value of the parameters will be null, unless the user specifies some other value when sending the request.
Hence, all you have to do is to declare a custom example for the Pydantic model using Config and schema_extra, as described in the documentation and as shown below. The below example will create an empty (i.e., {}) request body in OpenAPI (Swagger UI), which can be successfully submitted (as ID is the only attribute of the model and is optional).
class Table(BaseModel):
ID: Optional[UUID] = None
class Config:
schema_extra = {
"example": {
}
}
#app.post("/table/", response_model=Table)
def create_table(table: Table):
return table
If the Table model included some other required attributes, you could add example values for those, as demonstrated below:
class Table(BaseModel):
ID: Optional[UUID] = None
some_attr: str
class Config:
schema_extra = {
"example": {
"some_attr": "Foo"
}
}
If you would like to keep the auto-generated examples for the rest of the attributes except the one for the ID attribute, you could use the below to remove ID from the model's properties in the generated schema (inspired by Schema customization):
class Table(BaseModel):
ID: Optional[UUID] = None
some_attr: str
some_attr2: float
some_attr3: bool
class Config:
#staticmethod
def schema_extra(schema: Dict[str, Any], model: Type['Table']) -> None:
del schema.get('properties')['ID']
Also, if you would like to add custom example to some of the attributes, you could use Field() (as described here); for example, some_attr: str = Field(example="Foo").
Another possible solution would be to modify the generated OpenAPI schema, as described in Solution 3 of this answer. Though, the above solution is likely more suited to this case.
Note
ID: Optional[UUID] = None is the same as ID: UUID = None. As previously documented in FastAPI website (see this answer):
The Optional in Optional[str] is not used by FastAPI, but will allow
your editor to give you better support and detect errors.
Since then, FastAPI has revised their documentation with the following:
The Union in Union[str, None] will allow your editor to give you
better support and detect errors.
Hence, ID: Union[UUID, None] = None is the same as ID: Optional[UUID] = None and ID: UUID = None. In Python 3.10+, one could also use ID: UUID| None = None (see here).
As per FastAPI documentation (see Info section in the link provided):
Have in mind that the most important part to make a parameter optional
is the part:
= None
or the:
= Query(default=None)
as it will use that None as the default value, and that way make the
parameter not required.
The Union[str, None] part allows your editor to provide better
support, but it is not what tells FastAPI that this parameter is not
required.

Send pathlib.Path data to FastAPI: PosixPath is not JSON serializable

I have built an API using FastAPI and am trying to send data to it from a client.
Both the API and the client use a similar Pydantic model for the data that I want to submit. This includes a field that contains a file path, which I store in a field of type pathlib.path.
However, FastAPI does not accept the submission because it apparently cannot handle the path object:
TypeError: Object of type PosixPath is not JSON serializable
Here's a minimal test file that shows the problem:
import pathlib
from pydantic import BaseModel
from fastapi import FastAPI
from fastapi.testclient import TestClient
api = FastAPI()
client = TestClient(api)
class Submission(BaseModel):
file_path: pathlib.Path
#api.post("/", response_model=Submission)
async def add_submission(subm: Submission):
print(subm)
# add submission to database
return subm
def test_add_submission():
data = {"file_path": "/my/path/to/file.csv"}
print("original data:", data)
# create a Submission object, which casts filePath to pathlib.Path:
submission = Submission(**data)
print("submission object:", submission)
payload = submission.dict()
print("payload:", payload)
response = client.post("/", json=payload) # this throws the error
assert response.ok
test_add_submission()
When I change the model on the client side to use a string instead of a Path for file_path, things go through. But then I lose the pydantic power of casting the input to a Path when a Submission object is created, and then having a Path attribute with all its possibilities. Surely, there must be better way?
What is the correct way to send a pathlib.PosixPath object to a FastAPI API as part of the payload?
(This is Python 3.8.9, fastapi 0.68.1, pydantic 1.8.2 on Ubuntu)
The problem with you code is, that you first transform the pydantic model into a dict, which you then pass to the client, which uses its own json serializer and not the one provided by pydantic.
submission.dict() converts any pydantic model into a dict but keeps any other datatype.
With client.post("/", json=payload) requests json serializer is used, which cannot handle pathlib.Path.
The solution is, not to convert the pydantic model into dict first, but use the json() method of the pydantic model itself and pass it to the client.
response = client.post("/", data=submission.json())
Notice that you have to change the parameter json to data.

Is it possible to get query params with Pydantic if param's name use special symbols?

I'm handling this request in my code (Python3.9, FastAPI, Pydantic):
https://myapi.com/api?params[A]=1&params[B]=2
I tried to make following model:
BaseModel for handling special get request
(for fastapi.Query and pydantic.Field is same)
I also set up aliases for it, but in swagger docs I see next field:
Snap of the swagger docs
There are fields that are specified as extra_data
So, if I specify query params in parameters of my endpoint like this:
#app.get('/')
def my_handler(a: str = Query(None, alias="params[A]")):
return None
Everything works fine. How can I fix it? I want to initialize my pydantic.BaseModel with speacial aliases using this way and avoid usage of query-params in
class MyModel(BaseModel):
a = Field(alias="params[A]")
b = Field(alias="params[B]")
def my_handler(model: MyModel = Depends()):
return model.dict()

Categories

Resources