Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Pydantic 2.0 ignores Optional in schema and requires the field to be available

Pydantic 2.0 seems to have drastically changed.

Previously with FastAPI and Pydantic 1.X I could define the schema like this, where receipt is optional:

class VerifyReceiptIn(BaseModel):
    device_id: str
    device_type: DeviceType
    receipt: Optional[str]

Then in FastAPI endpoint I could do this:

@router_verify_receipt.post(
    "/",
    status_code=201,
    response_model=VerifyReceiptOut,
    responses={201: {"model": VerifyReceiptOut}, 400: {"model": HTTPError}},
)
async def verify_receipt(body: VerifyReceiptIn):
    auth_service = AuthService()
    ...

And the unit test without receipt in body was fine with it, but now with Pydantic 2.0 it's failing. Now it claims that Receipt is required and throws a 422 error.

response = await client.post(
            "/verify-receipt/",
            headers={"api-token": "abc123"},
            json={
                "device_id": "u1",
                "device_type": DeviceType.ANDROID.value,
            },
        )

But this is why we had Optional. Why do I have to pass receipt=None in body? This is not ideal as it will break everything on production. Is there a way around this? Thanks

like image 376
Houman Avatar asked Sep 08 '25 16:09

Houman


1 Answers

Indeed Pydantic v2 changed the behavior of Optional to a more strict and correct one. The Pydantic 2.0 Migration Guide has a special section describing the new behavior.

According to the Guide Optional[str] is now treated as required but allowed to have None as its value. See the example:

from pydantic import BaseModel

class Model(BaseModel):
    value: Optional[str]

m = Model(value='abc')  # passes
m = Model(value=None)  # passes
m = Model()  # fails

Think of Optional[str] as of str | None. And in general A | B requires the value to be of type A or B.

For the value to be not required it has to have a default value like in the following example:

from pydantic import BaseModel

class Model(BaseModel):
    value: str | None = 'nothing'

m = Model(value='abc')  # passes
m = Model(value=None)  # passes
m = Model()  # passes and sets `value` to `'nothing'`

If you'd change value: str | None = 'nothing' to value: str | None = None you'll get a value that is not required and has a default value None.

like image 164
lig Avatar answered Sep 10 '25 06:09

lig