i wanna validate data in request
i have dictionary (a and b are use cases, 1234 are sub use cases)
d ={'a':[1,2],'b':[3,4]}
and request
#router.post("/documents")
from typing import Literal, List, Optional, Dict
#router.post("/documents")
async def data(usecase: Literal[frozenset(d.keys())] = Form(...))
it works and allowed values only a and b
But i wanna extend validation
#router.post("/documents")
async def data(usecase: Literal[frozenset(d.keys())] = Form(...),
subusecase: THERE I WANNA VALIDATE 1234 VALUES = Form(...)
)
I will be very grateful for the help
I'm not sure i quite understand the structure of the data received by your route.
If you want to validate the route input from a complexe structure like nested dict etc, in my opinion it would be beter to use a pydantic model with a pydantic validation function.
you pass to your route a pydantic model as parameter:
#router.post("/documents")
async def data(use_cases: UseCases):
# do something with your uses_cases
pass
pydantic model example:
from typing import List
from pydantic import BaseModel, validator
class UseCases(BaseModel):
a: List[int]
b: List[int]
#validator('a')
def a_must_containt_something(cls, v):
# add your validations here
return v
#validator('b')
def b_must_containt_something(cls, v):
# add your validations here
return v
Related
I have a payload that comes in which has two parameters. One of the parameters is a long string which contains more parameters. Something like this param1%param2%param3. I am using FastAPI and Pydantic BaseModel to get that data and validate it, however since I am using it in other places I also want to transform it and store it in an object so I can access it later without having to transform it when I need to. Something like PayloadObject.param1.
from fastapi import FastAPI
from pydantic import BaseModel
class Payload(BaseModel):
string_params: str #param1%param2%param3
second_param: dict
#validator(string_params)
def string_params_validator(cls, strings_params):
#validation stuff
#validator(second_param)
def second_param(cls, second_param):
#validation stuff
app = FastAPI()
#app.post("/my_route")
async def post_my_route(payload: Payload):
# want to have transformed payload around here
func(payload)
What would be the best way to go about that using pydantic?
I am just thinking of making a class that transforms this information on __init__ without using BaseModel. So after I get that data from the request and validate it I run it through this class and get a format that I am happy with.
class NewPayload:
def __init__(self, payload: Payload):
# do transformations so i end up with
self.param1 = param1
self.param2 = param2
self.param3 = param3
self.second_param = second_param
If this payload structure is specific to this route it's a good idea to transform it directly in your route def.
The structure you gave for NewPayload will not work if the number of param isn't always the same.
example 1:
from typing import List
from fastapi import FastAPI
from pydantic import BaseModel
class Payload(BaseModel):
string_params: str #param1%param2%param3
second_param: dict
#validator(string_params)
def string_params_validator(cls, strings_params):
#validation stuff
#validator(second_param)
def second_param(cls, second_param):
#validation stuff
app = FastAPI()
#app.post("/my_route")
async def post_my_route(payload: Payload):
params: List[str] = payload.string_params.split("%")
# params = ["param1", "param2", "param3"]
# Do something with params
func(payload)
Another idea:
not the best since you accept the data in list format also, you can add a validator to stop this behavior but it will modify the doc
from typing import List
from fastapi import FastAPI
from pydantic import BaseModel
class Payload(BaseModel):
string_params: Union[str, List[str]] #param1%param2%param3
second_param: dict
#validator(string_params)
def string_params_validator(cls, strings_params):
string_params = strings_params.split("%")
return string_params
#validator(string_params)
def params_to_list(cls, strings_params):
#validation stuff
#validator(second_param)
def second_param(cls, second_param):
#validation stuff
app = FastAPI()
#app.post("/my_route")
async def post_my_route(payload: Payload):
func(payload)
You can use a second pydantic model with the second solution to only accept str in input and cast your first model into the other.
from typing import List
from fastapi import FastAPI
from pydantic import BaseModel
class Payload(BaseModel):
string_params: str #param1%param2%param3
second_param: dict
#validator(string_params)
def params_to_list(cls, strings_params):
#validation stuff
#validator(second_param)
def second_param(cls, second_param):
#validation stuff
class Payload1(BaseModel):
string_params: Union[str, List[str]]
second_param: dict
#validator(string_params)
def string_params_validator(cls, strings_params):
string_params = strings_params.split("%")
return string_params
app = FastAPI()
#app.post("/my_route")
async def post_my_route(payload: Payload):
params: Payload1 = Payload1(**Payload.dict())
func(payload)
In the end the cleaner solution would be to make string_params a list of str and not a simple str since you will always need to convert it to list
I'm able to get requests like this with query parameters of a type list like this:
#router.get("/findStuff")
def get_stuff(a: List[int] = Query(None), b: List[str] = Query(None)):
return {'a': a, 'b': b}
But I'm not sure how I'd do this dynamically from an arbitrary Pydantic schema? When I do this, the query params are interpreted as a request body and cannot be processed from the OpenAPI doc. Is there a way to get the same behavior from above without explicitly specifying each query param in the method arguments?
class MySchema(BaseModel):
a: List[int] = Query(None)
b: List[str] = Query(None)
#router.get("/findStuff")
def get_stuff(inputs: MySchema = Depends()):
return inputs
The FastAPI documentation outlines how you can declare classes as dependencies.
An example based on the code you provided in your question:
import uvicorn
from typing import List
from fastapi import FastAPI, Depends, Query
app = FastAPI()
class MySchema:
def __init__(self, a: List[int] = Query(None), b: List[str] = Query(None)):
self.a = a
self.b = b
#app.get("/findStuff")
def get_stuff(inputs: MySchema = Depends()):
return inputs
if __name__ == "__main__":
uvicorn.run(app='main:app', host='127.0.0.1', port=8000)
If you navigate to the documentation (/docs endpoint), you can see the query paramaters for that route:
I have a JSON object that reads:
j = {"id": 1, "label": "x"}
I have two types:
class BaseModel:
def __init__(self, uuid):
self.uuid = uuid
class Entity(BaseModel):
def __init__(self, id, label):
super().__init__(id)
self.name = name
Note how id is stored as uuid in the BaseModel.
I can load Entity from the JSON object as:
entity = Entity(**j)
I want to re-write my model leveraging dataclass:
#dataclass
class BaseModel:
uuid = str
#dataclass
class Entity:
name = str
Since my JSON object does not have the uuid, entity = Entitye(**j) on the dataclass-based model will throw the following error:
TypeError: __init__() got an unexpected keyword argument 'id'
The "ugly" solutions I can think of:
Rename id to uuid in JSON before initialization:
j["uuid"] = j.pop("id")
Define both id and uuid:
#dataclass
class BaseModel:
uuid = str
#dataclass
class Entity:
id = str
name = str
# either use:
uuid = id
# or use this method
def __post_init__(self):
super().uuid = id
Is there any cleaner solution for this kind of object initialization in the dataclass realm?
might be ruining the idea of removing the original __init__ but how about writing a function to initialize the data class?
def init_entity(j):
j["uuid"] = j.pop("id")
return Entity(**j)
and in your code entity = initEntity(j)
I think the answer here might be to define a classmethod that acts as an alternative constructor to the dataclass.
from dataclasses import dataclass
from typing import TypeVar, Any
#dataclass
class BaseModel:
uuid: str
E = TypeVar('E', bound='Entity')
#dataclass
class Entity(BaseModel):
name: str
#classmethod
def from_json(cls: type[E], **kwargs: Any) -> E:
return cls(kwargs['id'], kwargs['label']
(For the from_json type annotation, you'll need to use typing.Type[E] instead of type[E] if you're on python <= 3.8.)
Note that you need to use colons for your type-annotations within the main body of a dataclass, rather than the = operator, as you were doing.
Example usage in the interactive REPL:
>>> my_json_dict = {'id': 1, 'label': 'x'}
>>> Entity.from_json(**my_json_dict)
Entity(uuid=1, name='x')
It's again questionable how much boilerplate code this saves, however. If you find yourself doing this much work to replicate the behaviour of a non-dataclass class, it's often better just to use a non-dataclass class. Dataclasses are not the perfect solution to every problem, nor do they try to be.
Simplest solution seems to be to use an efficient JSON serialization library that supports key remappings. There are actually tons of them that support this, but dataclass-wizard is one example of a (newer) library that supports this particular use case.
Here's an approach using an alias to dataclasses.field() which should be IDE friendly enough:
from dataclasses import dataclass
from dataclass_wizard import json_field, fromdict, asdict
#dataclass
class BaseModel:
uuid: int = json_field('id', all=True)
#dataclass
class Entity(BaseModel):
name: str = json_field('label', all=True)
j = {"id": 1, "label": "x"}
# De-serialize the dictionary object into an `Entity` instance.
e = fromdict(Entity, j)
repr(e)
# Entity(uuid=1, name='x')
# Assert we get the same object when serializing the instance back to a
# JSON-serializable dict.
assert asdict(e) == j
My intention
So, I am developing an API package for one service. I want to make good typehints for every method, which exists in my library
For example, when user types get()., after the dot pycharm will let him know, what response this method will provide.
e.g:
info = get()
info. # and here IDE help with hints.
Pitfalls
But, there are some methods, which provide different responses depending of parameters in methods.
e.g.
# this method responses with object, containing fields:
# count - count of items
# items - list of ids of users
info = get()
# but this method will give additional information. It responses with object, containing fields:
# count - count of items
# items - list of objects with users' information. It has fields:
# id - id of user
# firstname - firstname of user
# lastname - lastname of user
# ... and some others
info = get(fields='firstname')
Objects structure
Now I have such structure (i't simplified)
from typing import List, Union
from pydantic import BaseModel, Field
class UserInfo(BaseModel):
id: int = Field(...)
firstname: str = Field(None)
lastname: str = Field(None)
some_other_fields: str = Field(None)
class GetResponseNoFields(BaseModel):
count: int = Field(...)
items: List[int] = Field(...)
class GetResponseWithFields(BaseModel):
count: int = Field(...)
items: List[UserInfo] = Field(...)
class GetResponseModel(BaseModel):
response: Union[GetResponseNoFields, GetResponseWithFields] = Field(...)
def get(fields=None) -> GetResponseModel:
# some code
pass
The problem
The problem is, when I type get(fields='firsttname').response.items[0]. pycharm shows me typehints only for int. He doesn't think, that items can contain List[UserInfo], he thinks, it only can have List[int]
I have tried
I've tried to use typing.overload decorator, but method 'get' has many parameters, and actually doesn't support default parameter values. Or maybe i didn't do it properly
Here what I have tried with overload (simlified). It didn't work because of 'some_other_param', but I leave it here just in case:
from typing import overload
#overload
def get(fields: None) -> GetResponseNoFields: ...
#overload
def get(fields: str) -> GetResponseWithFields: ...
def get(some_other_param=None, fields=None):
# code here
pass
When I try to call method without parameters, pycharm says, that "Some of the parameters is unfilled"
I want to implement a put or patch request in FastAPI that supports partial update. The official documentation is really confusing and I can't figure out how to do the request. (I don't know that items is in the documentation since my data will be passed with request's body, not a hard-coded dict).
class QuestionSchema(BaseModel):
title: str = Field(..., min_length=3, max_length=50)
answer_true: str = Field(..., min_length=3, max_length=50)
answer_false: List[str] = Field(..., min_length=3, max_length=50)
category_id: int
class QuestionDB(QuestionSchema):
id: int
async def put(id: int, payload: QuestionSchema):
query = (
questions
.update()
.where(id == questions.c.id)
.values(**payload)
.returning(questions.c.id)
)
return await database.execute(query=query)
#router.put("/{id}/", response_model=QuestionDB)
async def update_question(payload: QuestionSchema, id: int = Path(..., gt=0),):
question = await crud.get(id)
if not question:
raise HTTPException(status_code=404, detail="question not found")
## what should be the stored_item_data, as documentation?
stored_item_model = QuestionSchema(**stored_item_data)
update_data = payload.dict(exclude_unset=True)
updated_item = stored_item_model.copy(update=update_data)
response_object = {
"id": question_id,
"title": payload.title,
"answer_true": payload.answer_true,
"answer_false": payload.answer_false,
"category_id": payload.category_id,
}
return response_object
How can I complete my code to get a successful partial update here?
Posting this here for googlers who are looking for an intuitive solution for creating Optional Versions of their pydantic Models without code duplication.
Let's say we have a User model, and we would like to allow for PATCH requests to update the User. But we need to create a schema that tells FastApi what to expect in the content body, and specifically that all the fields are Optional (Since that's the nature of PATCH requests). We can do so without redefining all the fields
from pydantic import BaseModel
from typing import Optional
# Creating our Base User Model
class UserBase(BaseModel):
username: str
email: str
# And a Model that will be used to create an User
class UserCreate(UserBase):
password: str
Code Duplication ❌
class UserOptional(UserCreate):
username: Optional[str]
email: Optional[str]
password: Optional[str]
One Liner ✅
# Now we can make a UserOptional class that will tell FastApi that all the fields are optional.
# Doing it this way cuts down on the duplication of fields
class UserOptional(UserCreate):
__annotations__ = {k: Optional[v] for k, v in UserCreate.__annotations__.items()}
NOTE: Even if one of the fields on the Model is already Optional, it won't make a difference due to the nature of Optional being typing.Union[type passed to Optional, None] in the background.
i.e typing.Union[str, None] == typing.Optional[str]
You can even make it into a function if your going to be using it more than once:
def convert_to_optional(schema):
return {k: Optional[v] for k, v in schema.__annotations__.items()}
class UserOptional(UserCreate):
__annotations__ = convert_to_optional(UserCreate)
I got this answer on the FastAPI's Github issues.
You could make the fields Optional on the base class and create a new QuestionCreate model that extends the QuestionSchema. As an example:
from typing import Optional
class Question(BaseModel):
title: Optional[str] = None # title is optional on the base schema
...
class QuestionCreate(Question):
title: str # Now title is required
The cookiecutter template here provides some good insight too.
I created a library (pydantic-partial) just for that, converting all the fields in the normal DTO model to being optional. See https://medium.com/#david.danier/how-to-handle-patch-requests-with-fastapi-c9a47ac51f04 for a code example and more detailed explanation.
https://github.com/team23/pydantic-partial/
Based on the answer of #cdraper, I made a partial model factory:
from typing import Mapping, Any, List, Type
from pydantic import BaseModel
def model_annotations_with_parents(model: BaseModel) -> Mapping[str, Any]:
parent_models: List[Type] = [
parent_model for parent_model in model.__bases__
if (
issubclass(parent_model, BaseModel)
and hasattr(parent_model, '__annotations__')
)
]
annotations: Mapping[str, Any] = {}
for parent_model in reversed(parent_models):
annotations.update(model_annotations_with_parents(parent_model))
annotations.update(model.__annotations__)
return annotations
def partial_model_factory(model: BaseModel, prefix: str = "Partial", name: str = None) -> BaseModel:
if not name:
name = f"{prefix}{model.__name__}"
return type(
name, (model,),
dict(
__module__=model.__module__,
__annotations__={
k: Optional[v]
for k, v in model_annotations_with_parents(model).items()
}
)
)
def partial_model(cls: BaseModel) -> BaseModel:
return partial_model_factory(cls, name=cls.__name__)
Can be used with the function partial_model_factory:
PartialQuestionSchema = partial_model_factory(QuestionSchema)
Or with decorator partial_model:
#partial_model
class PartialQuestionSchema(QuestionSchema):
pass