pydantic's `Field`'s default value ignores constraint checks - python

from pydantic import BaseModel
class User(BaseModel):
age: int = Field('foo', ge=0)
User() # doesn't raise an error
# User(age='foo')
Why doesn't this raise an error since a string foo is passed even though an int is expected?
User(age='foo') however raises the ValidationError as expected.

This connected to the config that you can add to all of your models.
By default the default of Fields are excluding from validation, simply assuming that the programmer puts a proper default value.
However, if you want to enforce validation you cann enforce it by adding a Config to your model:
class User(BaseModel):
age: int = Field('foo', ge=0)
class Config(BaseConfig):
validate_all = True
if __name__ == "__main__":
User() # Now raise an error
Also have a look at the other options for configs in the the docs: https://pydantic-docs.helpmanual.io/usage/model_config/

Related

Pydantic from_orm to load Django model with related list field

I have the following Django models:
from django.db import models
class Foo(models.Model):
id: int
name = models.TextField(null=False)
class Bar(models.Model):
id: int
foo = models.ForeignKey(
Foo,
on_delete=models.CASCADE,
null=False,
related_name="bars",
)
And Pydantic models (with orm_mode set to True):
from pydantic import BaseModel
class BarPy(BaseModel):
id: int
foo_id: int
class FooPy(BaseModel):
id: int
name: str
bars: list[BarPy]
Now I want to perform a query on the model Foo and load it into FooPy, so i wrote this query:
foo_db = Foo.objects.prefetch_related("bars").all()
pydantic_model = FooPy.from_orm(foo_db)
But it gives me this error:
pydantic.error_wrappers.ValidationError: 1 validation error for FooPy
bars
value is not a valid list (type=type_error.list)
I am able to do it when explicitly using the FooPy constructor and assigning the values manually but i want to use from_orm.
The bars attribute on your Foo model is a ReverseManyToOneDescriptor that just returns a RelatedManager for the Bar model. As with any manager in Django, to get a queryset of all the instances managed by it, you need to call the all method on it. Typically you would do something like foo.bars.all().
You can add your own custom validator to FooPy and make it pre=True to grab all the related Bar instances and pass a sequence of them along to the default validators:
from django.db.models.manager import BaseManager
from pydantic import BaseModel, validator
...
class FooPy(BaseModel):
id: int
name: str
bars: list[BarPy]
#validator("bars", pre=True)
def get_all_from_manager(cls, v: object) -> object:
if isinstance(v, BaseManager):
return list(v.all())
return v
Note that it is not enough to just do .all() because that will return a queryset, which will not pass the default sequence validator built into Pydantic models. You would get the same error.
You need to give it an actual sequence (e.g. list or tuple). A QuerySet is not a sequence, but an iterable. But you can consume it and turn it into a sequence, by calling for example list on it.
More generalized version
You could make an attempt at generalizing that validator and add it to your own (Pydantic) base model. Something like this should work on any field you annotate as list[Model], with Model being a subclass of pydantic.BaseModel:
from django.db.models.manager import BaseManager
from pydantic import BaseModel, validator
from pydantic.fields import ModelField, SHAPE_LIST
...
class CustomBaseModel(BaseModel):
#validator("*", pre=True)
def get_all_from_manager(cls, v: object, field: ModelField) -> object:
if not (isinstance(field.type_, type) and issubclass(field.type_, BaseModel)):
return v
if field.shape is SHAPE_LIST and isinstance(v, BaseManager):
return list(v.all())
return v
I have not thoroughly tested this, but I think you get the idea.
Side note
It is worth mentioning that prefetch_related has nothing to do with the problem. The problem and its solution are the same, whether you do that or not. The difference is that without prefetch_related, you'll trigger additional database queries, when calling from_orm and thus executing the validator that consumes the queryset of .bars.all().

how pydantic model works without instantiating pydantic classes

Can anyone let me know, how come below pydantic model code works without instantiating UserIn and UserOut class object? Is this something handled internally by pydantic library?
from typing import Optional
from fastapi import FastAPI
from pydantic import BaseModel, EmailStr
app = FastAPI()
class UserIn(BaseModel):
username: str
password: str
email: EmailStr
full_name: Optional[str] = None
class UserOut(BaseModel):
username: str
email: EmailStr
full_name: Optional[str] = None
class UserInDB(BaseModel):
username: str
hashed_password: str
email: EmailStr
full_name: Optional[str] = None
def fake_password_hasher(raw_password: str):
return "supersecret" + raw_password
def fake_save_user(user_in: UserIn):
hashed_password = fake_password_hasher(user_in.password)
user_in_db = UserInDB(**user_in.dict(), hashed_password=hashed_password)
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)
print(user_saved.__dict__)
return user_saved
You don't need to instantiate the objects in your code because FastAPI creates them automatically for pydantic schemas in both cases.
For arguments to endpoint methods that are Pydantic models like the user_in: UserIn in your example, it's interpreted as a Request Body, as explained in the docs here. It's worth noting the explanation about how the input data binding works for the endpoint parameters in FastAPI documentation:
If the parameter is also declared in the path, it will be used as a path parameter.
If the parameter is of a singular type (like int, float, str, bool, etc) it will be interpreted as a query parameter.
If the parameter is declared to be of the type of a Pydantic model, it will be interpreted as a request body.
For the endpoint output, when you define the response_model, it will also convert it, as explained in the documentation here:
FastAPI will use this response_model to:
Convert the output data to its type declaration.
Validate the data.
Add a JSON Schema for the response, in the OpenAPI path operation.
Will be used by the automatic documentation systems.

Either of the two Pydantic attributes should be optional

I want validate a payload schema & I am using Pydantic to do that. The class created by inheriting Pydantic's BaseModel is named as PayloadValidator and it has two attributes, addCustomPages which is list of dictionaries & deleteCustomPages which is a list of strings.
class NestedCustomPages(BaseModel):
"""This is the schema for each custom page."""
html: str
label: str
type: int
class PayloadValidator(BaseModelWithContext):
"""This class defines the payload load schema and also validates it."""
addCustomPages: Optional[List[NestedCustomPages]]
deleteCustomPages: Optional[List[str]]
I want to declare either of the attributes of class PayloadValidator as optional. I have tried looking for the solution for this but couldn't find anything.
There was a question on this a while ago on the pydantic Github page: https://github.com/samuelcolvin/pydantic/issues/506. The conclusion there includes a toy example with a model that requires either a or b to be filled by using a validator:
from typing import Optional
from pydantic import validator
from pydantic.main import BaseModel
class MyModel(BaseModel):
a: Optional[str] = None
b: Optional[str] = None
#validator('b', always=True)
def check_a_or_b(cls, b, values):
if not values.get('a') and not b:
raise ValueError('Either a or b is required')
return b
mm = MyModel()

Pydantic allows weird types on field with nested model

Currently i have this script:
from typing import List
from uuid import uuid4, UUID
from pydantic import BaseModel, Field
class Id(BaseModel):
value:UUID = Field(default_factory=uuid4)
class User(BaseModel):
id:Id = Field(default_factory=Id)
roles:List[str] = ['student']
class Project(BaseModel):
id:Id = Field(default_factory=Id)
creator:User
id = Id()
print(id)
user = User()
print(user)
project = Project(creator={})
print(project)
The script should run as it is
When i instantiate a project, pydantic allows me to put non User objects, like dicts, strings, Id(objects of type Id) and some others, but i feel this is not expected. The weirdest thing is the fields are auto filled with an object of the correct type when a bad value is passed
what is happening here? Thanks in advance!

How to validate a pydantic object after editing it

Is there any obvious to validate a pydantic model after some changing some attribute?
Say I create a simple Model and object:
from pydantic import BaseModel
class A(BaseModel):
b: int = 0
a=A()
Then edit it, so that it is actually invalid:
a.b = "foobar"
Can I force a re-validation and expect a ValidationError to be raised?
I tried
A.validate(a) # no error
a.copy(update=dict(b='foobar')) # no error
What did work was
A(**dict(a._iter()))
ValidationError: 1 validation error for A
b
value is not a valid integer (type=type_error.integer)
But that is not really straightforward and I need to use the supposedly private method _iter.
Is there a clean alternative?
pydantic can do this for you, you just need validate_assignment:
from pydantic import BaseModel
class A(BaseModel):
b: int = 0
class Config:
validate_assignment = True

Categories