Repository: pydantic/pydantic

Issue:
Using multiple `Field`s in a single `Annotated` is broken
For BaseModel metadata gets dropped. For dataclasses we prohibit it, but I suspect we don't do a good job of detecting it in all cases.

```python
from typing import Annotated

from pydantic import Field, BaseModel


class Model(BaseModel):
    foo: Annotated[int, Field(description='xyz'), Field(title='abc')]


print(Model.model_json_schema())  # title gets dropped
```

Similarly with TypeAdapter:

```python
from typing import Annotated

from pydantic import Field, TypeAdapter


# all metadata ignored
print(
    TypeAdapter(
        Annotated[int, Field(description='xyz'), Field(title='abc')]
    ).json_schema()
)
```

(I think this last case is being fixed in https://github.com/pydantic/pydantic/pull/6336)

```python
from typing import Annotated

from pydantic import TypeAdapter, Field, dataclasses


@dataclasses.dataclass
class Model:
    foo: Annotated[int, Field(description='xyz'), Field(title='abc')]


print(TypeAdapter(Model).json_schema())  # TypeError: Field may not be used twice on the same field
```


I think we should figure out if we can merge `Fields` w/ some sort of semantics for overriding and support that everywhere.

Selected Assignee: @lig
Expose ability to generate a CoreSchemaOrField for json schema building from a FieldInfo
cc @tiangolo @adriangb 

@tiangolo would be good if you could confirm whether this would do the trick for you. I think @adriangb might want it addressed in a different way, I also understand that perspective. I was surprised by how little code it took to do this though (and how it affected basically nothing else).

Also, to be clear, I don't think this is fully ready for primetime, probably could use some minor tweaks/improved tests/a bit of documentation, but I also think there's a good chance it needs substantial revisions before merging and/or will be rejected, so I don't want to do that until it's confirmed.


Discussion:
In my head, it feels that the outer annotation `Field2` should have higher precedence than `Field1`:
```
assert Annotated[Annotated[T, Field1], Field2] == Annotated[T, Field1, Field2]
```

Typically, newer annotations probably have more information than older annotations (assuming they're constructed from the inside to the outside). The contextual information about the _thing_ is bigger. This also aligns with the reading flow by starting at the leftmost field and updating it with new occurrences.
## Deploying with &nbsp;<a href="https://pages.dev"><img alt="Cloudflare Pages" src="https://user-images.githubusercontent.com/23264/106598434-9e719e00-654f-11eb-9e59-6167043cfa01.png" width="16"></a> &nbsp;Cloudflare Pages

<table><tr><td><strong>Latest commit:</strong> </td><td>
<code>30276a0</code>
</td></tr>
<tr><td><strong>Status:</strong></td><td>&nbsp;✅&nbsp; Deploy successful!</td></tr>
<tr><td><strong>Preview URL:</strong></td><td>
<a href='https://35dd3f39.pydantic-docs2.pages.dev'>https://35dd3f39.pydantic-docs2.pages.dev</a>
</td></tr>
<tr><td><strong>Branch Preview URL:</strong></td><td>
<a href='https://field-info-json-schema.pydantic-docs2.pages.dev'>https://field-info-json-schema.pydantic-docs2.pages.dev</a>
</td></tr>
</table>

[View logs](https://dash.cloudflare.com/?to=/:account/pages/view/pydantic-docs/35dd3f39-ba85-41cc-83b1-41185555c95b)

whoa we can have siblings keys for $ref? That probably means we can remove a lot of otherwise-error-prone code and also will probably simplify things for users in some cases where there may-or-may-not be an allOf
Agreed. I still think we should provide a better way of dealing with the need to generate multiple json schemas while resolving conflicts and such.
I found the right/better link saying `$ref` can now have siblings: https://json-schema.org/draft/2019-09/release-notes.html#core-vocabulary
Just FYI, I updated the tests in `main-pv2` in FastAPI with this branch, and now I made this draft PR that just installs this branch to confirm it makes tests pass: https://github.com/tiangolo/fastapi/pull/9777 🎉 
@tiangolo could you clarify a bit what the use case is within FastAPI for this? I'm assuming it's to take a collection of types and generate JSON schemas for all of them (which will go into path operations, query parameters, etc. in the OpenAPI def) with a single `"$def"` section, handling name collisions and such. Is this right?

I just want to make sure this isn't an XY problem. Maybe this fix makes sense regardless, but I think `pydantic` should make have a good story for the above use case. I spent a couple of hours trying to update Xpresso to Pydantic V2 and this was one of the more painful things. We even have [an existing API for this](https://github.com/pydantic/pydantic/blob/b465ead20036832ef1d1fbcc611a1d7e6bb33863/pydantic/type_adapter.py#L332-L372) but I think it's not great, I wasn't able to use it and it seems like you weren't either.

One thing I always wanted within FastAPI was to customize model name collision resolution to use `Model1`, `Model2` instead of including the Python module for external facnig APIs, [and it seems like I'm not the only one](https://github.com/pydantic/pydantic/issues/6304). If we think about the right way to do this now I think we can come up with a good API that servers all projects that want to do similar things with Pydantic, not just FastAPI, and allows things like customizing name clash resolution.
@adriangb yep! That's right. In FastAPI, the first-class citizens are fields (in v1 there was a `ModelField` object) not the models. And this is indeed the pure types with their metadata, including lists, ints, and anything else that is not strictly a Pydantic model (and also including Pydantic models).

I collect all those, generate the JSON Schemas for them, and then put any relevant definitions in OpenAPI's components (e.g. for models), and the rest that don't produce a reusable definition but a pure JSON Schema I put them directly in the relevant parts of OpenAPI (e.g. a path operation parameter).

The specific thing I'm not being able to do is this, with FastAPI:

```Python
from fastapi import FastAPI, Query

app = FastAPI()


@app.get("/items/")
async def create_item(
    batch: str = Query(
        default="asdf",
        description="some query param",
        examples=["yes please", "oh, no"],
    ),
) -> str:
    return ""
```

The JSON Schema for that includes `batch`, includes the default value, but doesn't include the `description` and `examples`.


<details>
<summary>Here's a test for that code with the long OpenAPI</summary>

There's a comment inside the OpenAPI showing where the extra metadata should be:

```Python
def test_model_field():
    # insert_assert(app.openapi())
    assert app.openapi() == {
        "openapi": "3.1.0",
        "info": {"title": "FastAPI", "version": "0.1.0"},
        "paths": {
            "/items/": {
                "get": {
                    "summary": "Create Item",
                    "operationId": "create_item_items__get",
                    "parameters": [
                        {
                            "name": "batch",
                            "in": "query",
                            "required": False,
                            "schema": {
                                "type": "string",
                                "default": "asdf",
                                "title": "Batch",
                                # Here's the problem, this doesn't show up without this PR
                                "description": "some query param",
                                "examples": ["yes please", "oh, no"],
                                # End of the problem
                            },
                            "description": "some query param",
                        }
                    ],
                    "responses": {
                        "200": {
                            "description": "Successful Response",
                            "content": {
                                "application/json": {
                                    "schema": {
                                        "type": "string",
                                        "title": "Response Create Item Items  Get",
                                    }
                                }
                            },
                        },
                        "422": {
                            "description": "Validation Error",
                            "content": {
                                "application/json": {
                                    "schema": {
                                        "$ref": "#/components/schemas/HTTPValidationError"
                                    }
                                }
                            },
                        },
                    },
                }
            }
        },
        "components": {
            "schemas": {
                "HTTPValidationError": {
                    "properties": {
                        "detail": {
                            "items": {"$ref": "#/components/schemas/ValidationError"},
                            "type": "array",
                            "title": "Detail",
                        }
                    },
                    "type": "object",
                    "title": "HTTPValidationError",
                },
                "ValidationError": {
                    "properties": {
                        "loc": {
                            "items": {
                                "anyOf": [{"type": "string"}, {"type": "integer"}]
                            },
                            "type": "array",
                            "title": "Location",
                        },
                        "msg": {"type": "string", "title": "Message"},
                        "type": {"type": "string", "title": "Error Type"},
                    },
                    "type": "object",
                    "required": ["loc", "msg", "type"],
                    "title": "ValidationError",
                },
            }
        },
    }
```

</details>
Right but instead of you calling `GenerateSchema.generate_inner` and cobbling together a solution from disparate pieces of functionality in Pydantic I feel like it'd be better if Pydantic had first-class support for this, essentially I'd like something like `TypeAdapter.json_schemas` that actually works well, and in particular I'd like an easy hook to customize name clash resolution.
To be clear, I'm not against this change, I think making `Field` and `TypeAdapter` play well together makes a ton of sense.
Agreed with both @adriangb!

---

More context on my side:

The main reason why I used `GenerateSchema.generate_inner` and not something else is because if I'm asking for a model schema, it will give me the `{"$ref": "blahblah/Foo"}` instead of rendering the model schema at the top level and including internal defs, which is what other methods did (I don't remember exactly which ones I tried).

This way I can re-use that ref schema in OpenAPI.

For FastAPI, the current (and I think sort of ideal? at least up to now) process would be:

* Collect all Pydantic *fields* (including Pydantic models, but also other things), each field is a combination of a type and a `FieldInfo` (e.g. `Annotated[str, Field(description="The name of the user")]`)
* Give a list of all that (some of them reference each other) to `GenerateSchema`
* Get a bunch of definitions that can be put in `$defs` (or in `components` in OpenAPI)

Up to this point, the process is just for `GenerateSchema` to know all the models, resolve name conflicts, and generate the definitions and usable refs.

* Later, give `GenerateSchema` (currently with `GenerateSchema.generate_inner`) one specific *field* (not model, a combination of type and `FieldInfo`)
* It gives back something I can inject in OpenAPI, so a `{"$ref": "blahblah/Foo"}` or a pure JSON Schema for things that don't live in the pre-created definitions.

Now, for this current process, the main thing I'm missing is being able to get all that, but making sure `FieldInfo` metadata attached to a type is preserved (what this PR does).

But if there's any (feasible) better way to do that process with different steps, that would also be very welcome, of course. 😅 
> But if there's any (feasible) better way to do that process with different steps, that would also be very welcome, of course

That's what I think we should design :)

> each field is a combination of a type and a FieldInfo (e.g. Annotated[str, Field(description="The name of the user")])

I think this is an interesting bit. I get the feeling that `FieldInfo` is being abused to represent a different concept just because it can hold a type, a default value, and a name (the name matters, e.g. `authorization: FromHeader[str]` the `authorization` part matters. But maybe that's fine.

At the very least, I think this PR would be better if the function had the signature `generate_schemas(fields: list[FieldInfo], mode: Literal["input", "output"]) -> tuple[list[JsonSchemaValue], dict[str, JsonSchemaValue]]` where the returned tuple is (1:1 mapping to input fields, definitions). It'd be nice if there was also a parameter to customize name conflict resolution.

And I don't think that should go in TypeAdapter, it seems like a better fit for a function in `json_schema.py`.

As it stands right now, I think fixing https://github.com/pydantic/pydantic/issues/6353 and related issues may be a better path forward so that at least `TypeAdapter(Annotated[TheType, Field(...)])` works correctly.
Looking through what we have, we already have a `GenerateJsonSchema.generate_definitions` that does pretty much exactly what you want Sebastián. It seems like we just need a version that accepts a `FieldInfo`. Given that you can always convert that to `Annotated[field_info.annotation, field_info]` (assuming we fix #6353) is there any reason that wouldn't work? I'm also not sure that using a type as a key (which is what `GenerateJsonSchema.generate_definitions` does) is always going to work, e.g. you could have two `List[int]` and I'd assume want to treat them separately? Not sure about that last bit.
> I think this is an interesting bit. I get the feeling that FieldInfo is being abused to represent a different concept just because it can hold a type, a default value, and a name (the name matters, e.g. authorization: FromHeader[str] the authorization part matters. But maybe that's fine.

Just to clarify, in FastAPI the main class I move around is a custom `ModelField` that contains the `name` and the `FieldInfo` (as the name is not stored anywhere in v2, not even in `FieldInfo`, only in the parent model if there's one). 

Up to now, from what I've seen, the `FieldInfo` is the lowest common denominator that can store the information and can have information related to more than Pydantic models (it can hold information about lists, ints, etc). I think the lower layer would be pure Pydantic core schemas, but those don't contain the metadata (e.g. `description` and `examples`), that metadata is inside closures in callables in the Pydantic core `"metadata"` dict key.

> As it stands right now, I think fixing https://github.com/pydantic/pydantic/issues/6353 and related issues may be a better path forward so that at least TypeAdapter(Annotated[TheType, Field(...)]) works correctly.

Probably, it seems using `TypeAdapter` currently doesn't generate the extra metadata from `FieldInfo` for JSON Schema, if solving multiple `Field()` in annotated solves it, then that would certainly solve it.

> Looking through what we have, we already have a GenerateJsonSchema.generate_definitions that does pretty much exactly what you want Sebastián. It seems like we just need a version that accepts a FieldInfo. Given that you can always convert that to Annotated[field_info.annotation, field_info] (assuming we fix https://github.com/pydantic/pydantic/issues/6353) is there any reason that wouldn't work? I'm also not sure that using a type as a key (which is what GenerateJsonSchema.generate_definitions does) is always going to work, e.g. you could have two List[int] and I'd assume want to treat them separately? Not sure about that last bit.

Yep, `GenerateJsonSchema.generate_definitions` is exactly what I use right now. The only problem is that currently, it doesn't take types, it takes Pydantic core schemas, so `Annotated[field_info.annotation, field_info]` (which is what I was using up to this PR) still needed to go in a `TypeAdapter` to extract the Pydantic core schema from that.

I was doing:

```Python
TypeAdapter(Annotated[self.field_info.annotation, self.field_info]).core_schema
```

...but that didn't include the JSON Schema metadata (`description`, `examples`, etc).

I was collection all those, and then passing them to `GenerateJsonSchema.generate_definitions()`.

And then the next thing I need to do is "query" the `GenerateJsonSchema` for the JSON Schema for a specific type annotation (a specific `ModelField`).

If `GenerateJsonSchema.generate_definitions` could take `FieldInfo`s directly, that would definitely work. If it still took core schemas but `TypeAdapter` had a way to generate core schemas that end up including the metadata, that would work as well.

If there are two `List[int]` without more metadata I think it would be fine to use the same JSON Schema 🤔 ...it would probably be different if one is `Annotated[List[int], Query(description="the tokens")]` and the other is `Annotated[List[int], Query(description="the search terms")]`, but that's already different, I guess different objects with different id/hash, so that should be fine. I'm not sure if I'm missing something... but I think that's fine how it has worked up to now. The key thing now is being able to pass that, in some way, to `GenerateJsonSchema.generate_definitions()`, and then being able to query it again.

---

It also doesn't have to allow "querying" the `GenerateJsonSchema.generate_definitions()`, if the initial return had a way to map from input to output definitions, I could (probably?) handle the rest outside.

Resolving patch:
diff --git a/pydantic/_internal/_generate_schema.py b/pydantic/_internal/_generate_schema.py
--- a/pydantic/_internal/_generate_schema.py
+++ b/pydantic/_internal/_generate_schema.py
@@ -1228,6 +1228,11 @@ def _apply_annotations(  # noqa: C901
         # expand annotations before we start processing them so that `__prepare_pydantic_annotations` can consume
         # individual items from GroupedMetadata
         annotations = list(_known_annotated_metadata.expand_grouped_metadata(annotations))
+        non_field_infos, field_infos = [a for a in annotations if not isinstance(a, FieldInfo)], [
+            a for a in annotations if isinstance(a, FieldInfo)
+        ]
+        if field_infos:
+            annotations = [*non_field_infos, FieldInfo.merge_field_infos(*field_infos)]
         idx = -1
         prepare = getattr(source_type, '__prepare_pydantic_annotations__', None)
         if prepare:
diff --git a/pydantic/fields.py b/pydantic/fields.py
--- a/pydantic/fields.py
+++ b/pydantic/fields.py
@@ -28,6 +28,9 @@
     DeprecationWarning = PydanticDeprecatedSince20
 
 
+_Unset: Any = PydanticUndefined
+
+
 class _FromFieldInfoInputs(typing_extensions.TypedDict, total=False):
     """This class exists solely to add type checking for the `**kwargs` in `FieldInfo.from_field`."""
 
@@ -139,6 +142,7 @@ class FieldInfo(_repr.Representation):
         'init_var',
         'kw_only',
         'metadata',
+        '_attributes_set',
     )
 
     # used to convert kwargs to metadata/constraints,
@@ -164,7 +168,9 @@ def __init__(self, **kwargs: Unpack[_FieldInfoInputs]) -> None:
 
         See the signature of `pydantic.fields.Field` for more details about the expected arguments.
         """
-        self.annotation, annotation_metadata = self._extract_metadata(kwargs.get('annotation'), kwargs.get('default'))
+        self._attributes_set = {k: v for k, v in kwargs.items() if v is not _Unset}
+        kwargs = {k: _DefaultValues.get(k) if v is _Unset else v for k, v in kwargs.items()}  # type: ignore
+        self.annotation, annotation_metadata = self._extract_metadata(kwargs.get('annotation'))
 
         default = kwargs.pop('default', PydanticUndefined)
         if default is Ellipsis:
@@ -307,7 +313,7 @@ class MyModel(pydantic.BaseModel):
                 annotation = typing_extensions.get_args(annotation)[0]
 
         if isinstance(default, cls):
-            default.annotation, annotation_metadata = cls._extract_metadata(annotation, default)
+            default.annotation, annotation_metadata = cls._extract_metadata(annotation)
             default.metadata += annotation_metadata
             default.frozen = final or default.frozen
             return default
@@ -323,7 +329,7 @@ class MyModel(pydantic.BaseModel):
                 init_var = True
                 annotation = annotation.type
             pydantic_field = cls._from_dataclass_field(default)
-            pydantic_field.annotation, annotation_metadata = cls._extract_metadata(annotation, default)
+            pydantic_field.annotation, annotation_metadata = cls._extract_metadata(annotation)
             pydantic_field.metadata += annotation_metadata
             pydantic_field.frozen = final or pydantic_field.frozen
             pydantic_field.init_var = init_var
@@ -333,20 +339,25 @@ class MyModel(pydantic.BaseModel):
             if _typing_extra.is_annotated(annotation):
                 first_arg, *extra_args = typing_extensions.get_args(annotation)
                 field_infos = [a for a in extra_args if isinstance(a, FieldInfo)]
-                if field_infos:
-                    if len(field_infos) > 1:
-                        raise TypeError('Field may not be used twice on the same field')
-                    field_info = next(iter(field_infos))
-                    if not field_info.is_required():
-                        raise TypeError('Default may not be specified twice on the same field')
-                    new_field_info = copy(field_info)
-                    new_field_info.default = default
-                    new_field_info.annotation = first_arg
-                    new_field_info.metadata += [a for a in extra_args if not isinstance(a, FieldInfo)]
-                    return new_field_info
+                field_info = cls.merge_field_infos(*field_infos, annotation=first_arg, default=default)
+                field_info.metadata += [a for a in extra_args if not isinstance(a, FieldInfo)]
+                return field_info
 
             return cls(annotation=annotation, default=default, frozen=final or None)
 
+    @staticmethod
+    def merge_field_infos(*field_infos: FieldInfo, **overrides: Any) -> FieldInfo:
+        """Merge `FieldInfo` instances keeping only explicitly set attributes.
+
+        Returns:
+            FieldInfo: A merged FieldInfo instance.
+        """
+        new_kwargs: dict[str, Any] = {}
+        for field_info in field_infos:
+            new_kwargs.update(field_info._attributes_set)
+        new_kwargs.update(overrides)
+        return FieldInfo(**new_kwargs)
+
     @classmethod
     def _from_dataclass_field(cls, dc_field: DataclassField[Any]) -> typing_extensions.Self:
         """Return a new `FieldInfo` instance from a `dataclasses.Field` instance.
@@ -372,30 +383,23 @@ def _from_dataclass_field(cls, dc_field: DataclassField[Any]) -> typing_extensio
         # use the `Field` function so in correct kwargs raise the correct `TypeError`
         field = Field(default=default, default_factory=default_factory, repr=dc_field.repr, **dc_field.metadata)
 
-        field.annotation, annotation_metadata = cls._extract_metadata(dc_field.type, dc_field.default)
+        field.annotation, annotation_metadata = cls._extract_metadata(dc_field.type)
         field.metadata += annotation_metadata
         return field
 
     @classmethod
-    def _extract_metadata(cls, annotation: type[Any] | None, default: Any) -> tuple[type[Any] | None, list[Any]]:
+    def _extract_metadata(cls, annotation: type[Any] | None) -> tuple[type[Any] | None, list[Any]]:
         """Tries to extract metadata/constraints from an annotation if it uses `Annotated`.
 
         Args:
             annotation: The type hint annotation for which metadata has to be extracted.
-            default: The default value for the field, used to check if `Field()` is used more than once.
 
         Returns:
             A tuple containing the extracted metadata type and the list of extra arguments.
-
-        Raises:
-            TypeError: If a `Field` is used twice on the same field.
         """
-        default_is_field = isinstance(default, FieldInfo)
         if annotation is not None:
             if _typing_extra.is_annotated(annotation):
                 first_arg, *extra_args = typing_extensions.get_args(annotation)
-                if sum(1 for a in extra_args if isinstance(a, FieldInfo)) + default_is_field > 1:
-                    raise TypeError('Field may not be used twice on the same field')
                 return first_arg, list(extra_args)
 
         return annotation, []
@@ -510,6 +514,8 @@ def __repr_args__(self) -> ReprArgs:
         yield 'required', self.is_required()
 
         for s in self.__slots__:
+            if s == '_attributes_set':
+                continue
             if s == 'annotation':
                 continue
             elif s == 'metadata' and not self.metadata:
@@ -588,38 +594,72 @@ class _EmptyKwargs(typing_extensions.TypedDict):
     """This class exists solely to ensure that type checking warns about passing `**extra` in `Field`."""
 
 
-def Field(  # C901
+_DefaultValues = dict(
+    default=...,
+    default_factory=None,
+    alias=None,
+    alias_priority=None,
+    validation_alias=None,
+    serialization_alias=None,
+    title=None,
+    description=None,
+    examples=None,
+    exclude=None,
+    include=None,
+    discriminator=None,
+    json_schema_extra=None,
+    frozen=None,
+    validate_default=None,
+    repr=True,
+    init_var=None,
+    kw_only=None,
+    pattern=None,
+    strict=None,
+    gt=None,
+    ge=None,
+    lt=None,
+    le=None,
+    multiple_of=None,
+    allow_inf_nan=None,
+    max_digits=None,
+    decimal_places=None,
+    min_length=None,
+    max_length=None,
+)
+
+
+def Field(  # noqa: C901
     default: Any = PydanticUndefined,
     *,
-    default_factory: typing.Callable[[], Any] | None = None,
-    alias: str | None = None,
-    alias_priority: int | None = None,
-    validation_alias: str | AliasPath | AliasChoices | None = None,
-    serialization_alias: str | None = None,
-    title: str | None = None,
-    description: str | None = None,
-    examples: list[Any] | None = None,
-    exclude: bool | None = None,
-    include: bool | None = None,
-    discriminator: str | None = None,
-    json_schema_extra: dict[str, Any] | None = None,
-    frozen: bool | None = None,
-    validate_default: bool | None = None,
-    repr: bool = True,
-    init_var: bool | None = None,
-    kw_only: bool | None = None,
-    pattern: str | None = None,
-    strict: bool | None = None,
-    gt: float | None = None,
-    ge: float | None = None,
-    lt: float | None = None,
-    le: float | None = None,
-    multiple_of: float | None = None,
-    allow_inf_nan: bool | None = None,
-    max_digits: int | None = None,
-    decimal_places: int | None = None,
-    min_length: int | None = None,
-    max_length: int | None = None,
+    default_factory: typing.Callable[[], Any] | None = _Unset,
+    alias: str | None = _Unset,
+    alias_priority: int | None = _Unset,
+    validation_alias: str | AliasPath | AliasChoices | None = _Unset,
+    serialization_alias: str | None = _Unset,
+    title: str | None = _Unset,
+    description: str | None = _Unset,
+    examples: list[Any] | None = _Unset,
+    exclude: bool | None = _Unset,
+    include: bool | None = _Unset,
+    discriminator: str | None = _Unset,
+    json_schema_extra: dict[str, Any] | None = _Unset,
+    frozen: bool | None = _Unset,
+    validate_default: bool | None = _Unset,
+    repr: bool = _Unset,
+    init_var: bool | None = _Unset,
+    kw_only: bool | None = _Unset,
+    pattern: str | None = _Unset,
+    strict: bool | None = _Unset,
+    gt: float | None = _Unset,
+    ge: float | None = _Unset,
+    lt: float | None = _Unset,
+    le: float | None = _Unset,
+    multiple_of: float | None = _Unset,
+    allow_inf_nan: bool | None = _Unset,
+    max_digits: int | None = _Unset,
+    decimal_places: int | None = _Unset,
+    min_length: int | None = _Unset,
+    max_length: int | None = _Unset,
     **extra: Unpack[_EmptyKwargs],
 ) -> Any:
     """Create a field for objects that can be configured.
@@ -677,13 +717,13 @@ def Field(  # C901
     min_items = extra.pop('min_items', None)  # type: ignore
     if min_items is not None:
         warn('`min_items` is deprecated and will be removed, use `min_length` instead', DeprecationWarning)
-        if min_length is None:
+        if min_length in (None, _Unset):
             min_length = min_items  # type: ignore
 
     max_items = extra.pop('max_items', None)  # type: ignore
     if max_items is not None:
         warn('`max_items` is deprecated and will be removed, use `max_length` instead', DeprecationWarning)
-        if max_length is None:
+        if max_length in (None, _Unset):
             max_length = max_items  # type: ignore
 
     unique_items = extra.pop('unique_items', None)  # type: ignore
@@ -711,21 +751,28 @@ def Field(  # C901
             'Extra keyword arguments on `Field` is deprecated and will be removed. use `json_schema_extra` instead',
             DeprecationWarning,
         )
-        if not json_schema_extra:
+        if not json_schema_extra or json_schema_extra is _Unset:
             json_schema_extra = extra  # type: ignore
 
-    if validation_alias and not isinstance(validation_alias, (str, AliasChoices, AliasPath)):
+    if (
+        validation_alias
+        and validation_alias is not _Unset
+        and not isinstance(validation_alias, (str, AliasChoices, AliasPath))
+    ):
         raise TypeError('Invalid `validation_alias` type. it should be `str`, `AliasChoices`, or `AliasPath`')
 
-    if serialization_alias is None and isinstance(alias, str):
+    if serialization_alias in (_Unset, None) and isinstance(alias, str):
         serialization_alias = alias
 
+    if validation_alias in (_Unset, None):
+        validation_alias = alias
+
     return FieldInfo.from_field(
         default,
         default_factory=default_factory,
         alias=alias,
         alias_priority=alias_priority,
-        validation_alias=validation_alias or alias,
+        validation_alias=validation_alias,
         serialization_alias=serialization_alias,
         title=title,
         description=description,


Test patch:
diff --git a/tests/test_annotated.py b/tests/test_annotated.py
--- a/tests/test_annotated.py
+++ b/tests/test_annotated.py
@@ -6,7 +6,7 @@
 from pydantic_core import PydanticUndefined, core_schema
 from typing_extensions import Annotated
 
-from pydantic import BaseModel, Field, GetCoreSchemaHandler
+from pydantic import BaseModel, Field, GetCoreSchemaHandler, TypeAdapter
 from pydantic.errors import PydanticSchemaGenerationError
 
 NO_VALUE = object()
@@ -88,34 +88,6 @@ class M(BaseModel):
     assert repr(M.model_fields['x']) == expected_repr
 
 
-@pytest.mark.parametrize(
-    'hint_fn,value,exc_handler',
-    [
-        (
-            lambda: Annotated[int, Field(lt=10), Field(ge=0)],
-            0,
-            pytest.raises(TypeError, match='Field may not be used twice on the same field'),
-        ),
-        (
-            lambda: Annotated[int, Field(lt=10)],
-            Field(ge=0),
-            pytest.raises(TypeError, match='Field may not be used twice on the same field'),
-        ),
-        (
-            lambda: Annotated[int, Field(0)],
-            5,
-            pytest.raises(TypeError, match='Default may not be specified twice on the same field'),
-        ),
-    ],
-)
-def test_annotated_model_exceptions(hint_fn, value, exc_handler):
-    hint = hint_fn()
-    with exc_handler:
-
-        class M(BaseModel):
-            x: hint = value
-
-
 @pytest.mark.parametrize('metadata', [0, 'foo'])
 def test_annotated_allows_unknown(metadata):
     class M(BaseModel):
@@ -285,3 +257,33 @@ class _(BaseModel):
 
     assert types == {GenericModel[int]}
     types.clear()
+
+
+def test_merge_field_infos_type_adapter() -> None:
+    ta = TypeAdapter(
+        Annotated[
+            int, Field(gt=0), Field(lt=100), Field(gt=1), Field(description='abc'), Field(3), Field(description=None)
+        ]
+    )
+
+    # insert_assert(ta.core_schema)
+    assert ta.core_schema == {'type': 'default', 'schema': {'type': 'int', 'gt': 1, 'lt': 100}, 'default': 3}
+
+    # insert_assert(ta.json_schema())
+    assert ta.json_schema() == {'default': 3, 'exclusiveMaximum': 100, 'exclusiveMinimum': 1, 'type': 'integer'}
+
+
+def test_merge_field_infos_model() -> None:
+    class Model(BaseModel):
+        x: Annotated[
+            int, Field(gt=0), Field(lt=100), Field(gt=1), Field(description='abc'), Field(3), Field(description=None)
+        ] = Field(5)
+
+    # insert_assert(Model.model_json_schema())
+    assert Model.model_json_schema() == {
+        'properties': {
+            'x': {'default': 5, 'exclusiveMaximum': 100, 'exclusiveMinimum': 1, 'title': 'X', 'type': 'integer'}
+        },
+        'title': 'Model',
+        'type': 'object',
+    }