3

I have the following code:

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


class Request(BaseModel):
    user_name: str
    age: int
    # other unknown arguments


@app.post("/home")
def write_home(request: Request):
    print(request.__dict__)
    return {
        "user_name": request.user_name,
        "age": request.age,
        # other arguments...
    }

I would like the request to take optional arguments (like height, weight, etc) but these arguments may be unknown.

Thanks in advance

I tried to add them directly in the request but it doesn't print out other unspecified arguments enter image description here

Chris
  • 18,724
  • 6
  • 46
  • 80

3 Answers3

0

Having unknown arguments is kind off completely opposite to the intention of Pydantic (which is type safe data parsing and validation). What you could do (and what I would do), is to define a field extra (or similar), for holding dynamic extra data: from typing import Any

class MyRequest(BaseModel):
    user_name: str
    age: int
    extra: dict[str, Any]

Then you know which fields are always required to be present, and anything unknown is put in the extra field.

M.O.
  • 1,712
  • 1
  • 6
  • 18
0

Simple solution

I think the simplest solution is to configure your model with the extra = "allow" setting (it is set to extra = "ignore" by default). With that setting, passing any extra name-value-pairs to the model constructor will dynamically create fields on that model instance with the values and types as provided.

Here is an example:

from fastapi import FastAPI
from pydantic import BaseModel


app = FastAPI()


class Model(BaseModel):
    user_name: str
    age: int

    class Config:
        extra = "allow"


@app.post("/home")
def write_home(model: Model) -> Model:
    print(model)
    return model

Now you can POST arbitrary additional data like this for example:

{
  "user_name": "string",
  "age": 0,
  "height": 3.14
}

The output of the print statement is user_name='string' age=0 height=3.14 and the response body is exactly the same as that of the request.


Potential risk

There is one big caveat here in my opinion, which is not specific to FastAPI, but to Pydantic models in general:

With the extra = "allow" setting, any field name will be available. This can have potentially serious unintended consequences because the provided names can override existing names in the model namespace, including those of internal attributes (e.g. __fields__) and pre-defined methods (e.g. dict).

In the context of a FastAPI endpoint, imagine a situation, where someone POSTs a payload like this:

{
  "user_name": "string",
  "age": 0,
  "dict": 1
}

This will work just fine up until the point, where the dict method of that instance needs to be called, which happens to be the case during the formation of the response.

In other words, our print(model) will work seemingly fine, yielding user_name='string' age=0 dict=1, but the attempt to return this from our route handler will crash the server with a TypeError: 'int' object is not callable.

This is just an example, but this should illustrate, why this may be dangerous or at least problematic, if you do not handle it properly.


Other caveats

A few minor caveats you also need to be aware of:

  • This may be obvious, but no validation will be done on any of those extra field values. After being parsed via the configured (or default) JSON decoder, they will be assigned to the model instance as is.
  • The OpenAPI documentation can of course not display those fields as being either part of the accepted request body schema or included in the response model schema.
Daniil Fajnberg
  • 12,753
  • 2
  • 10
  • 41
0

You could get the request body parsed as JSON, using request.json() (request should be an instance of Starlette's Request object), as shown here (see Option 4) and here (see Option 1). That way, you could have the BaseModel for the required and known parameters, while still being able to accept previously unknown parameters.

The request.json() will return a dict object—see here if you would like to know how to loop through dictionary items.

Example

from fastapi import FastAPI, Request
from pydantic import BaseModel

app = FastAPI()


class Base(BaseModel):
    username: str
    age: int


@app.post('/')
async def main(base: Base, request: Request):
    return await request.json()

Input example (you could use Swagger UI autodocs at http://127.0.0.1:8000/docs to test the endpoint):

{
  "username": "john",
  "gender": "m",
  "age": 20,
  "height": 1.95,
  "weight": 90
}

If you didn't want to use a Pydantic BaseModel at all, you would still be able to get the request body parsed as JSON using request.json(), but there would be no validation for the required/known parameters you would like to define, unless you performed that validation check on your own inside the endpoint, or in a dependency class/function. If you would like to do so, please have a look at the linked answers given in the first paragraph above, which also demonstrate how to check the validity of the JSON object and raise an exception if a client sends invalid JSON. In the example above, this validation check is taken care of by FastAPI and Pydantic (due to using the BaseModel).

Chris
  • 18,724
  • 6
  • 46
  • 80