Repository: pydantic/pydantic
Issue category: Feature Request

Issue:
Support for `annotated_types.Not`
### Initial Checks

- [X] I have searched Google & GitHub for similar requests and couldn't find anything
- [X] I have read and followed [the docs](https://docs.pydantic.dev) and still think this feature is missing

### Description

I need to use a `PositiveOddInt` type. Such a type can be generalized to `PositiveInt` and `NotMultipleOf(2)`.
json-schema for `NotMultipleOf(2)` would look like `"not": {"multipleOf": 2}`

Adding support for `annotated_types.Not` would make possible something like this:
```python
class PositiveOddIntRootModel(pydantic.RootModel):
    root: Annotated[int, annotated_types.Not(annotated_types.MultipleOf(2)), annotated_types.Ge(0)]
```

I have a separate implementation of NotMultipleOf and PositiveOddInt [here](https://github.com/h2020charisma/ramanchada2/blob/main/src/ramanchada2/misc/types/positive_not_multiple.py) but it uses pydantic1. Now as i am trying to migrate to pydantic2, [here](https://docs.pydantic.dev/latest/concepts/types/#customizing_validation_with_get_pydantic_core_schema) i have seen that future changes are possible and that annotated-types and native types are preferred. Would it be possible to add support for `annotated_types.Not` or could you suggest an alternative?

### Affected Components

- [ ] [Compatibility between releases](https://docs.pydantic.dev/changelog/)
- [X] [Data validation/parsing](https://docs.pydantic.dev/concepts/models/#basic-model-usage)
- [X] [Data serialization](https://docs.pydantic.dev/concepts/serialization/) - `.model_dump()` and `.model_dump_json()`
- [X] [JSON Schema](https://docs.pydantic.dev/concepts/json_schema/)
- [ ] [Dataclasses](https://docs.pydantic.dev/concepts/dataclasses/)
- [ ] [Model Config](https://docs.pydantic.dev/concepts/config/)
- [X] [Field Types](https://docs.pydantic.dev/api/types/) - adding or changing a particular data type
- [ ] [Function validation decorator](https://docs.pydantic.dev/concepts/validation_decorator/)
- [ ] [Generic Models](https://docs.pydantic.dev/concepts/models/#generic-models)
- [ ] [Other Model behaviour](https://docs.pydantic.dev/concepts/models/) - `model_construct()`, pickling, private attributes, ORM mode
- [ ] [Plugins](https://docs.pydantic.dev/) and integration with other tools - mypy, FastAPI, python-devtools, Hypothesis, VS Code, PyCharm, etc.


Hints:
We'd welcome support for this. PRs welcome!
Hi @sydney-runkle, Could you please assign this to me ?
Hi @georgievgeorgi @sydney-runkle 

Can we use something like this ?

```
from functools import partial
from typing_extensions import Annotated
import annotated_types

from pydantic import AfterValidator, TypeAdapter, WithJsonSchema


def validate_not_multiple_of(x: int, y: int) -> int:
    if x % y == 0:
        raise ValueError(f"value must not be multiple of {y}")
    return x


PositiveOddInt = Annotated[
    int,
    annotated_types.Gt(0),
    AfterValidator(partial(validate_not_multiple_of, y=2)),
    WithJsonSchema({"type": "positive_odd_int"}),
]

ta = TypeAdapter(PositiveOddInt)

assert ta.validate_python(3) == 3
assert ta.dump_json(3) == b"3"
assert ta.json_schema() == {"type": "positive_odd_int"}
```

If I am not mistaken, supporting `annotated_types.Not` would require changes in `pydantic-core`. The implementation would differ from other annotated types, such as `annotated_types.Le`, as `annotated_types.Not` takes a `Callable` as input. I would like to discuss the solution first before proceeding with the implementation, as it might require significant changes.

@sydney-runkle What do you think ?
@aditkumar72, good question. I think we can handle this similarly to `annotated_types.Predicate`:

See this as a reference: https://github.com/pydantic/pydantic/blob/8cd7557df17829eb0d0f7eb2fbfd0793364fd8a9/pydantic/_internal/_known_annotated_metadata.py#L302-L314

Patch:
diff --git a/pydantic/_internal/_known_annotated_metadata.py b/pydantic/_internal/_known_annotated_metadata.py
index 0f1274090f..efdb8924c7 100644
--- a/pydantic/_internal/_known_annotated_metadata.py
+++ b/pydantic/_internal/_known_annotated_metadata.py
@@ -299,17 +299,25 @@ def _apply_constraint_with_incompatibility_info(
                 partial(get_constraint_validator(constraint), {constraint: getattr(annotation, constraint)}), schema
             )
             continue
-        elif isinstance(annotation, at.Predicate):
-            predicate_name = f'{annotation.func.__qualname__} ' if hasattr(annotation.func, '__qualname__') else ''
+        elif isinstance(annotation, (at.Predicate, at.Not)):
+            predicate_name = f'{annotation.func.__qualname__}' if hasattr(annotation.func, '__qualname__') else ''
 
             def val_func(v: Any) -> Any:
+                predicate_satisfied = annotation.func(v)  # noqa: B023
+
                 # annotation.func may also raise an exception, let it pass through
-                if not annotation.func(v):  # noqa: B023
-                    raise PydanticCustomError(
-                        'predicate_failed',
-                        f'Predicate {predicate_name}failed',  # type: ignore  # noqa: B023
-                    )
-                return v
+                if isinstance(annotation, at.Predicate):  # noqa: B023
+                    if not predicate_satisfied:
+                        raise PydanticCustomError(
+                            'predicate_failed',
+                            f'Predicate {predicate_name} failed',  # type: ignore  # noqa: B023
+                        )
+                else:
+                    if predicate_satisfied:
+                        raise PydanticCustomError(
+                            'not_operation_failed',
+                            f'Not of {predicate_name} failed',  # type: ignore  # noqa: B023
+                        )
 
             schema = cs.no_info_after_validator_function(val_func, schema)
         else:


Test patch:
diff --git a/tests/test_annotated.py b/tests/test_annotated.py
index fec98002a8..e0d3da7996 100644
--- a/tests/test_annotated.py
+++ b/tests/test_annotated.py
@@ -5,7 +5,7 @@
 
 import pytest
 import pytz
-from annotated_types import BaseMetadata, GroupedMetadata, Gt, Lt, Predicate
+from annotated_types import BaseMetadata, GroupedMetadata, Gt, Lt, Not, Predicate
 from pydantic_core import CoreSchema, PydanticUndefined, core_schema
 from typing_extensions import Annotated
 
@@ -402,6 +402,23 @@ def test_predicate_error_python() -> None:
     ]
 
 
+def test_not_operation_error_python() -> None:
+    ta = TypeAdapter(Annotated[int, Not(lambda x: x > 5)])
+
+    with pytest.raises(ValidationError) as exc_info:
+        ta.validate_python(6)
+
+    # insert_assert(exc_info.value.errors(include_url=False))
+    assert exc_info.value.errors(include_url=False) == [
+        {
+            'type': 'not_operation_failed',
+            'loc': (),
+            'msg': 'Not of test_not_operation_error_python.<locals>.<lambda> failed',
+            'input': 6,
+        }
+    ]
+
+
 def test_annotated_field_info_not_lost_from_forwardref():
     from pydantic import BaseModel
 
diff --git a/tests/test_types.py b/tests/test_types.py
index 47a3adc78f..88361f3399 100644
--- a/tests/test_types.py
+++ b/tests/test_types.py
@@ -6275,6 +6275,7 @@ class Model(BaseModel):
         min_length: Annotated[CustomType, annotated_types.MinLen(1)]
         max_length: Annotated[CustomType, annotated_types.MaxLen(1)]
         predicate: Annotated[CustomType, annotated_types.Predicate(lambda x: x > 0)]
+        not_multiple_of_3: Annotated[CustomType, annotated_types.Not(lambda x: x % 3 == 0)]
 
         model_config = ConfigDict(arbitrary_types_allowed=True)
 
@@ -6287,6 +6288,7 @@ class Model(BaseModel):
         max_length=CustomType([1]),
         multiple_of=CustomType(4),
         predicate=CustomType(1),
+        not_multiple_of_3=CustomType(4),
     )
 
     with pytest.raises(ValidationError) as exc_info:
@@ -6299,6 +6301,7 @@ class Model(BaseModel):
             max_length=CustomType([1, 2, 3]),
             multiple_of=CustomType(3),
             predicate=CustomType(-1),
+            not_multiple_of_3=CustomType(6),
         )
     # insert_assert(exc_info.value.errors(include_url=False))
     assert exc_info.value.errors(include_url=False) == [
@@ -6357,6 +6360,12 @@ class Model(BaseModel):
             'msg': 'Predicate test_constraints_arbitrary_type.<locals>.Model.<lambda> failed',
             'input': CustomType(-1),
         },
+        {
+            'type': 'not_operation_failed',
+            'loc': ('not_multiple_of_3',),
+            'msg': 'Not of test_constraints_arbitrary_type.<locals>.Model.<lambda> failed',
+            'input': CustomType(6),
+        },
     ]