Skip to content

Conversation

@Viicos
Copy link
Contributor

@Viicos Viicos commented Feb 3, 2025

In Pydantic 2.11 (expected release date March 31st 10th), we removed an unnecessary step that would unpack the Annotated form in FieldInfo.__init__ (see https://github.com/pydantic/pydantic/pull/11109/files#r1888512603).

However, FastAPI relied on this extra step, meaning we get different results.

MRE:

from inspect import signature
from typing import Annotated

from pydantic import Field
from fastapi.dependencies.utils import analyze_param


def func(user: Annotated[int, Field(strict=True)]): ...

analyze_param(
    param_name='user',
    annotation=signature(func).parameters['user'].annotation,
    value=object(),
    is_path_param=False,
)

On 2.10.6:

ParamDetails(
│   type_annotation=<class 'int'>,
│   depends=None,
│   field=ModelField(
│   │   field_info=Query(
│   │   │   annotation=int,
│   │   │   required=False,
│   │   │   default=<object object at 0x7033aa582700>,
│   │   │   alias='user',
│   │   │   json_schema_extra={},
│   │   │   metadata=[FieldInfo(annotation=NoneType, required=True, metadata=[Strict(strict=True)])]
│   │   ),
│   │   name='user',
│   │   mode='validation'
│   )
)

on 2.11.0a1:

ParamDetails(
│   type_annotation=<class 'int'>,
│   depends=None,
│   field=ModelField(
│   │   field_info=Query(
│   │   │   annotation=Annotated[int, FieldInfo(annotation=NoneType, required=True, metadata=[Strict(strict=True)])],
│   │   │   required=False,
│   │   │   default=<object object at 0x76889f3813c0>,
│   │   │   alias='user',
│   │   │   json_schema_extra={}
│   │   ),
│   │   name='user',
│   │   mode='validation'
│   )
)

Fixes pydantic/pydantic#11382.

@Viicos Viicos force-pushed the use-correct-annotation branch from 50e766c to 9a64c56 Compare February 3, 2025 12:49
@svlandeg svlandeg changed the title Use correct annotation when creating field info instance 🐛 Use correct annotation when creating field info instance Feb 5, 2025
@svlandeg svlandeg added the bug Something isn't working label Feb 5, 2025
@svlandeg svlandeg self-assigned this Feb 5, 2025
@svlandeg svlandeg marked this pull request as draft February 5, 2025 14:29
@svlandeg svlandeg marked this pull request as ready for review February 5, 2025 14:44
Copy link
Member

@svlandeg svlandeg left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the explanation and the fix! I've added your MRE to the test suite and confirmed that the unit test works on master with Pydantic 2.8.2 or 2.10.6 but breaks with 2.11.0a1. The failure with 2.11 is resolved with this PR though.

Will leave this to Tiangolo for a final review 🙏

@svlandeg svlandeg removed their assignment Feb 5, 2025
@Viicos
Copy link
Contributor Author

Viicos commented Feb 7, 2025

Thanks @svlandeg for wrapping it up

@Viicos
Copy link
Contributor Author

Viicos commented Feb 26, 2025

Just a friendly reminder that 2.11 is going to be released soon, in two weeks. If this isn't tackled it might cause issues in some projects (we had one report so far from an alpha release).

@svlandeg
Copy link
Member

svlandeg commented Feb 26, 2025

Thanks for the note, @Viicos! Will do - Tiangolo is aware of this one.

@tiangolo tiangolo changed the title 🐛 Use correct annotation when creating field info instance ♻️ Update internal annotation usage for compatibilty with Pydantic 2.11, use correct annotation when creating field info instance Feb 27, 2025
@tiangolo
Copy link
Member

Thanks!

I was hoping to get a failing small FastAPI app that is fixed by this, but I couldn't make one, and it seems the report you had on Pydantic was not really using FastAPI but extending internals in some way, I couldn't make a minimal breaking FastAPI app from that report.

I tried building one using the textual description there, but it kept passing on master, no matter the version of Pydantic.

Do you know in which use cases would it behave differently or how it could break a FastAPI app? I would like to have a test with that.

The current test checks that an internal function returns some specific value, which is better than nothing (thanks @svlandeg!), but still, it seems it is only testing that an internal function returns a specific shape. But it could be refactored in the future and there wouldn't be a way to know what could be broken in an app if not done correctly. 🤔

@Viicos
Copy link
Contributor Author

Viicos commented Feb 28, 2025

I don't think this is reproducible only using FastAPI. The issue came from https://github.com/airtai/faststream, which is manually inspecting the FastAPI dependants to create a OpenAPI-like schema.

@Viicos
Copy link
Contributor Author

Viicos commented Feb 28, 2025

Perhaps a better test would be to build an API route with func, and assert on the dependants instead.

@svlandeg svlandeg changed the title ♻️ Update internal annotation usage for compatibilty with Pydantic 2.11, use correct annotation when creating field info instance ♻️ Update internal annotation usage for compatibilty with Pydantic 2.11 Feb 28, 2025
@tiangolo
Copy link
Member

I just noticed that on line 421 there's a field_info.annotation = type_annotation, which is consistent with your changes, I guess it's more consistent and correct in general, although not obvious how it could break.

Sad not being able to write a test of an app that would break, to prevent accidentally reverting it in the future. Also, because sometimes I prefer not to change things that could break other apps in unknown ways without at least knowing what the change would fix/improve.

But anyway, I understand that the FieldInfo API would expect to get a type annotation without Annotated (extracted from Annotated before passing it to FieldInfo), so let's take this as it is with the current test. 🤓

Thanks @Viicos and @svlandeg! 🍰

@tiangolo tiangolo merged commit 15dd2b6 into fastapi:master Feb 28, 2025
28 checks passed
@Viicos
Copy link
Contributor Author

Viicos commented Feb 28, 2025

On a side note, the FieldInfo API currently suffers from many issues, and a big mistake was to make it public. We're slowly trying to figure out these issues but this is hard as many third-party libraries are relying on it. FastAPI is currently tighly coupled with this API (and Pydantic in general), which doesn't make things easy.

On a side note, is it expected for FastAPI to have the FieldInfo inside the metadata for the Query example in my OP? Posting it here:

ParamDetails(
│   type_annotation=<class 'int'>,
│   depends=None,
│   field=ModelField(
│   │   field_info=Query(
│   │   │   annotation=int,
│   │   │   required=False,
│   │   │   default=<object object at 0x7033aa582700>,
│   │   │   alias='user',
│   │   │   json_schema_extra={},
│   │   │   metadata=[FieldInfo(annotation=NoneType, required=True, metadata=[Strict(strict=True)])]
│   │   ),
│   │   name='user',
│   │   mode='validation'
│   )
)

@tiangolo
Copy link
Member

So, the ModelField is mainly a compat layer between Pydantic v1 (that used to have its own ModelField and Pydantic v2 that doesn't.

For most of the parts, if I remember correctly, FastAPI just gives that field (or series of fields) to Pydantic for:

  • Creating a TypeAdapter for validation or serialization
  • IIRC, in some cases, to create a dynamic model from those fields
  • To generate the JSON Schema

But about the field.metadata, FastAPI never accesses it directly for anything, there's only one function to copy an existing field into a new one that copies the attributes, including that one, that is the closest it gets to accessing it directly.

Side note: I struggle with GitHub notifications, I could miss these replies or tags, if I miss something, you could write to me or tag me on the Pydantic Slack. 🤓

@amacfie-tc
Copy link

#13431

@tiangolo
Copy link
Member

tiangolo commented Mar 1, 2025

So, there was indeed a use case that would break with this. 😅 #13440

I'll revert this PR for now. Meanwhile, @Viicos could you help me think what would be the right way to pass the data and metadata for Pydantic?

You maybe know, but for completeness, the mismatch is mainly that in Pydantic the top level "thing" is normally a model class. But in FastAPI the top level "thing" is actually a field: a param name with a type annotation and maybe a default, the same thing that could go inside Pydantic classes, this allows me to for example support params of lists, with all the Pydantic validation, with no extra effort from users, for example, for query parameters.

I suspect that if I supported Pydantic v2 only, maybe everything about each field could be around a TypeAdapter and that's it, but I need to support Pydantic v1 still, so I need to be able to create the ModelField in v1, and my ModelField shim for v2.

If you have any ideas please let me know (and please ping me on Slack so I notice it).

tiangolo added a commit that referenced this pull request Mar 1, 2025
@tiangolo
Copy link
Member

tiangolo commented Mar 1, 2025

I reverted this PR here: #13442

There I also added a docs with a source example for the use case and tests for it. 🤓

@Viicos
Copy link
Contributor Author

Viicos commented Mar 2, 2025

Thanks @tiangolo for taking care of it. I wasn't expecting this to blow up badly.

Unfortunately, I don't have any satisfying answer. The FastAPI logic around FieldInfo is a bit convoluted and hard to follow.

The ModelField wrapper class is exposing type_:

fastapi/fastapi/_compat.py

Lines 106 to 108 in 3824664

@property
def type_(self) -> Any:
return self.field_info.annotation

And it uses the field info (in the case of FastAPI, the custom Query/Body etc subclasses) annotation. As it is, with Pydantic 2.11, the annotation will be Annotated[<typ>, ...] in the case of the example I gave. This type_ property is used in a bunch of places in the FastAPI code base where I expect unexpected behavior to happen, e.g. here:

fastapi/fastapi/_compat.py

Lines 249 to 253 in 3824664

def is_bytes_field(field: ModelField) -> bool:
return is_bytes_or_nonable_bytes_annotation(field.type_)
def is_bytes_sequence_field(field: ModelField) -> bool:
return is_bytes_sequence_annotation(field.type_)

So perhaps an alternative fix could be to unwrap annotated in the type_ property?

We are also in weird state with 2.11 now:

fastapi/fastapi/_compat.py

Lines 110 to 113 in 3824664

def __post_init__(self) -> None:
self._type_adapter: TypeAdapter[Any] = TypeAdapter(
Annotated[self.field_info.annotation, self.field_info]
)

Annotated[self.field_info.annotation, self.field_info] will be Annotated[Annotated[int, Field(strict=True)], self.field_info]. It works as Python flatten annotated forms, but still, this isn't really expected (self.field_info, used as annotated metadata, is also inconsistent as its annotation is Annotated[int, Field(strict=True)]. Normally, Field() in annotated metadata shouldn't hold any annotation — see the distinction that should be made in pydantic/pydantic#11122; first heading).

I really think that the proper fix would be to avoid having FastAPI relying (i.e. subclassing) on the FieldInfo class. I think it will be hard to do so until Pydantic v1 support is dropped. We expect to do further changes in the FieldInfo API in the future because our logic is currently poorly implemented and has a a number of bugs.

@tiangolo
Copy link
Member

tiangolo commented Mar 3, 2025

Get it, thanks for the thorough review of the code and the detailed explanation!

I see this might get complicated. For now, let's wait and see if people report problems. I'll save the effort to focus it on supporting both v2 and v2's pydantic.v1 at the same time, which is not yet the case. This would help the people that are still stuck in v1 to migrate by changing just the import, and then gradually migrate groups of models to v2. That would enable dropping support for v1 as there's a way to migrate.

@Viicos Viicos deleted the use-correct-annotation branch March 3, 2025 17:18
@Lancetnik
Copy link
Contributor

I see this might get complicated. For now, let's wait and see if people report problems. I'll save the effort to focus it on supporting both v2 and v2's pydantic.v1 at the same time, which is not yet the case. This would help the people that are still stuck in v1 to migrate by changing just the import, and then gradually migrate groups of models to v2. That would enable dropping support for v1 as there's a way to migrate.

Hi! Sorry for ping, but have you any plans about Pydantic V1 drop? Looks like the most tools already leave this version. Probably, users still stuck in v1 could stuck in older FastAPI version as well?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug: Pydantic2.11.0a1 broke desriminator JSON Schema

5 participants