diff --git a/python/pydantic_core/core_schema.py b/python/pydantic_core/core_schema.py index c023a5635..7aede0ef6 100644 --- a/python/pydantic_core/core_schema.py +++ b/python/pydantic_core/core_schema.py @@ -2944,6 +2944,7 @@ class ModelField(TypedDict, total=False): serialization_alias: str serialization_exclude: bool # default: False frozen: bool + deprecation_msg: str metadata: Dict[str, Any] @@ -2954,6 +2955,7 @@ def model_field( serialization_alias: str | None = None, serialization_exclude: bool | None = None, frozen: bool | None = None, + deprecation_msg: str | None = None, metadata: Dict[str, Any] | None = None, ) -> ModelField: """ @@ -2971,6 +2973,7 @@ def model_field( serialization_alias: The alias to use as a key when serializing serialization_exclude: Whether to exclude the field when serializing frozen: Whether the field is frozen + deprecation_msg: A deprecation message indicating that the field is deprecated. `None` means that the field is not deprecated. metadata: Any other information you want to include with the schema, not used by pydantic-core """ return _dict_not_none( @@ -2980,6 +2983,7 @@ def model_field( serialization_alias=serialization_alias, serialization_exclude=serialization_exclude, frozen=frozen, + deprecation_msg=deprecation_msg, metadata=metadata, ) diff --git a/src/validators/model_fields.rs b/src/validators/model_fields.rs index 7aba7c8e3..7f57931e0 100644 --- a/src/validators/model_fields.rs +++ b/src/validators/model_fields.rs @@ -1,7 +1,8 @@ -use pyo3::exceptions::PyKeyError; +use pyo3::exceptions::{PyDeprecationWarning, PyKeyError}; use pyo3::intern; use pyo3::prelude::*; use pyo3::types::{PyDict, PySet, PyString, PyType}; +use pyo3::PyTypeInfo; use ahash::AHashSet; @@ -23,6 +24,7 @@ struct Field { name_py: Py, validator: CombinedValidator, frozen: bool, + deprecation_msg: Option, } impl_py_gc_traverse!(Field { validator }); @@ -92,6 +94,7 @@ impl BuildValidator for ModelFieldsValidator { name_py: field_name_py.into(), validator, frozen: field_info.get_as::(intern!(py, "frozen"))?.unwrap_or(false), + deprecation_msg: field_info.get_as::(intern!(py, "deprecation_msg"))?, }); } @@ -184,6 +187,10 @@ impl Validator for ModelFieldsValidator { // extra logic either way used_keys.insert(lookup_path.first_key()); } + if let Some(msg) = &field.deprecation_msg { + let deprecation_warning_type = PyDeprecationWarning::type_object_bound(py); + PyErr::warn_bound(py, &deprecation_warning_type, msg, 2)?; + } match field.validator.validate(py, value.borrow_input(), state) { Ok(value) => { model_dict.set_item(&field.name_py, value)?; diff --git a/tests/validators/test_model_fields.py b/tests/validators/test_model_fields.py index 8a22d96c3..bc88239a2 100644 --- a/tests/validators/test_model_fields.py +++ b/tests/validators/test_model_fields.py @@ -1781,3 +1781,35 @@ def test_extra_behavior_ignore(config: Union[core_schema.CoreConfig, None], sche } ] assert 'not_f' not in m + + +def test_deprecation_msg(): + v = SchemaValidator( + { + 'type': 'model-fields', + 'fields': { + 'a': {'type': 'model-field', 'schema': {'type': 'int'}}, + 'b': { + 'type': 'model-field', + 'schema': {'type': 'default', 'schema': {'type': 'int'}, 'default': 2}, + 'deprecation_msg': 'foo', + }, + 'c': { + 'type': 'model-field', + 'schema': {'type': 'default', 'schema': {'type': 'int'}, 'default': 2}, + 'deprecation_msg': 'bar', + }, + }, + } + ) + + # not touching the deprecated field: no warning + v.validate_python({'a': 1}) + + # validating the deprecated field: raise warning + # ensure that we get two warnings + with pytest.warns(DeprecationWarning) as w: + v.validate_python({'a': 1, 'b': 1, 'c': 1}) + assert len(w) == 2 + assert str(w[0].message) == 'foo' + assert str(w[1].message) == 'bar'