Repository: pydantic/pydantic
Pull request: Add note to documentation of `PlainValidator` regarding JSON schemas
Closes https://github.com/pydantic/pydantic/issues/10528

Problem statement:
Annotated field schemas broken after v2.9.0
### Initial Checks

- [X] I confirm that I'm using Pydantic V2

### Description

Following example shows how `Annotated` fields lost their schema `type` and `format` after v2.9.0

I can not see a mention of a such breaking change in changelog.

### Example Code

```Python
from datetime import datetime
from typing import Annotated
from pydantic import BaseModel, PlainValidator

class Model(BaseModel):
    my_field: Annotated[datetime, PlainValidator(lambda v: v)]

# In v2.8.2 this is correct
# {'format': 'date-time', 'title': 'My Field', 'type': 'string'}
# In v2.9.0 type information is lost. Also in latest v2.9.2
# {'title': 'My Field'}
print(Model.model_json_schema()["properties"]["my_field"])
```


### Python, Pydantic & OS Version

```Text
pydantic version: 2.9.2
        pydantic-core version: 2.23.4
          pydantic-core build: profile=release pgo=false
               python version: 3.12.4 (main, Jun 18 2024, 13:47:41) [Clang 15.0.0 (clang-1500.3.9.4)]
                     platform: macOS-14.6.1-arm64-arm-64bit
             related packages: fastapi-0.115.0 mypy-1.11.2 typing_extensions-4.12.2
                       commit: unknown
```

Hints from issue discussion:
@MarkusSintonen,

Thanks for reporting this. We're looking into this now!
Hi @MarkusSintonen,

Turns out this was an oversight when we implemented https://github.com/pydantic/pydantic/issues/8208 through https://github.com/pydantic/pydantic/pull/10094.

We expected JSON Schemas for fields with `PlainValidator` to _not_ work until today. For instance, the following was failing (see https://github.com/pydantic/pydantic/issues/7957):

```python
from typing_extensions import Annotated

from pydantic import BaseModel, PlainValidator

MyInt = Annotated[int, PlainValidator(lambda v: int(v) + 1)]

class Model(BaseModel):
    a: MyInt

print(Model(a='1').a)
print(Model(a='1').model_json_schema())  # error
```

However, given your example, it seems that some specific types (`datetime`, probably `pathlib.Path`) had custom json schema update functions injected in the core schema, meaning it would not fail.

In 2.9, all the validator classes (except `AfterValidator`) gained a new `json_schema_input_type` parameter. Because `PlainValidator` is the only one not caring about the annotated type, this parameter defaults to `Any`, so that you can still generate a JSON Schema.

Given that Sydney also did changes in the json schema update functions for 2.9, trying to fix the breaking change is going to be tricky. I assume your plain validator function is doing more complex stuff than a noop operation; you can use the `json_schema_input_type` to describe the types it is supposed to take:

```python
class Model(BaseModel):
    my_field: Annotated[datetime, PlainValidator(lambda v: v, json_schema_input_type=datetime)]
``` 
> In 2.9, all the validator classes (except AfterValidator) gained a new json_schema_input_type parameter. Because PlainValidator is the only one not caring about the annotated type, this parameter defaults to Any, so that you can still generate a JSON Schema.

@Viicos can not it take the type from the Annotated type given by the user? So we wouldnt need to double define the types? Thats quite a boilerplate there. But interesting that datetime just happened to work....
Should be probably somehow better documented or warning-deprecated the implicit Any usage with the PlainValidator. I don't think that implicit behavior makes much sense
> @Viicos can not it take the type from the Annotated type given by the user? 

The idea here is that many users use plain validator functions to transform a type into the annotated type (think, custom `str` -> `datetime` logic), and thus it doesn't make sense to infer the input type if a before or wrap validator is used.

> But interesting that datetime just happened to work....

Yeah, we had some poor schema building practices internally for a few types that we attempted to clean up for v2.9. That came at the cost of this one unexpected JSON schema behavior, I suppose.

> Should be probably somehow better documented or warning-deprecated the implicit Any usage with the PlainValidator. I don't think that implicit behavior makes much sense

We can certainly add some more docs, and a more significant callout in the history. Thanks for the feedback!
> @Viicos can not it take the type from the Annotated type given by the user? So we wouldnt need to double define the types? Thats quite a boilerplate there. But interesting that datetime just happened to work....

Indeed although in most cases I assume you are not using plain validators with noop functions (as Sydney mentioned you usually cast from one type to another).
> Indeed although in most cases I assume you are not using plain validators with noop functions

Nooo, it was just an example. In reality there were additional checks eg to see that datetimes were nothing non sensical (eg data from 1970), etc etc checks
> Nooo, it was just an example. In reality there were additional checks eg to see that datetimes were nothing non sensical (eg data from 1970), etc etc checks

In that case, is there a reason not to use `AfterValidator`? You let Pydantic validate the type, and you perform additional validation (without having to worry about the fact that the input type might not be a `datetime`, or it might even be a string that will be coerced to a `datetime` by [`speedate`](https://github.com/pydantic/speedate), etc).

> In that case, is there a reason not to use AfterValidator?

It wouldnt work IIRC as Pydantic would allow dates (`YYYY-MM-DD`) which we dont want in this case

Commit message: docs formatting

Patch to review:
diff --git a/pydantic/functional_validators.py b/pydantic/functional_validators.py
index 603400c7978..cda3f28e507 100644
--- a/pydantic/functional_validators.py
+++ b/pydantic/functional_validators.py
@@ -163,6 +163,14 @@ class PlainValidator:
 
     A metadata class that indicates that a validation should be applied **instead** of the inner validation logic.
 
+    !!! note:
+        Before v2.9, `PlainValidator` wasn't always compatible with JSON Schema generation for `mode='validation'`.
+        You can now use the `json_schema_input_type` argument to specify the input type of the function
+        to be used in the JSON schema when `mode='validation'` (the default). See the example below for more details.
+
+        If you'd like the JSON Schema type for an annotated field to be populated automatically,
+        you might consider using `WrapValidator` instead.
+
     Attributes:
         func: The validator function.
         json_schema_input_type: The input type of the function. This is only used to generate the appropriate
@@ -170,18 +178,30 @@ class PlainValidator:
 
     Example:
         ```py
+        from typing import Union
         from typing_extensions import Annotated
 
         from pydantic import BaseModel, PlainValidator
 
-        MyInt = Annotated[int, PlainValidator(lambda v: int(v) + 1)]
+        MyInt = Annotated[
+            int,
+            PlainValidator(
+                lambda v: int(v) + 1, json_schema_input_type=Union[str, int]  # (1)!
+            ),
+        ]
 
         class Model(BaseModel):
             a: MyInt
 
         print(Model(a='1').a)
         #> 2
+
+        print(Model(a=1).a)
+        #> 2
         ```
+
+        1. In this example, we've specified the `json_schema_input_type` as `Union[str, int]` which indicates to the JSON schema
+        generator that in validation mode, the input type for the `a` field can be either a `str` or an `int`.
     """
 
     func: core_schema.NoInfoValidatorFunction | core_schema.WithInfoValidatorFunction


Code review comments:
diff_hunk: @@ -163,25 +163,45 @@ class PlainValidator:
 
     A metadata class that indicates that a validation should be applied **instead** of the inner validation logic.
 
+    !!! note:
+        Before v2.9, `PlainValidator` wasn't always compatible with JSON Schema generation for `mode='validation'`.
+        You can now use the `json_schema_input_type` argument to specify the input type of the function
+        to be used in the JSON schema when `mode='validation'` (the default). See the example below for more details.
+
+        If you'd like the JSON Schema type for an annotated field to be populated automatically,
+        you might consider using `WrapValidator` instead.
path: pydantic/functional_validators.py
text: @user1:
Maybe we shouldn't recommend a specific validator type. before and wrap validators both populate the JSON Schema from the annotated type, but the usage of one of the user will depend on the user