Skip to content

Ensure ValidationInfo.field_name is correct on validator reuse #1692

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Apr 17, 2025
44 changes: 36 additions & 8 deletions python/pydantic_core/core_schema.py
Original file line number Diff line number Diff line change
@@ -1954,7 +1954,7 @@ class NoInfoValidatorFunctionSchema(TypedDict):
class WithInfoValidatorFunctionSchema(TypedDict, total=False):
type: Required[Literal['with-info']]
function: Required[WithInfoValidatorFunction]
field_name: str
field_name: str # deprecated
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think this TypedDict is only used for type hinting -- let me know if this is sufficient or if we should force a deprecation warning here as well somehow.



ValidationFunction = Union[NoInfoValidatorFunctionSchema, WithInfoValidatorFunctionSchema]
@@ -2042,7 +2042,7 @@ def fn(v: bytes, info: core_schema.ValidationInfo) -> str:
return v.decode() + 'world'

func_schema = core_schema.with_info_before_validator_function(
function=fn, schema=core_schema.str_schema(), field_name='a'
function=fn, schema=core_schema.str_schema()
)
schema = core_schema.typed_dict_schema({'a': core_schema.typed_dict_field(func_schema)})

@@ -2052,13 +2052,20 @@ def fn(v: bytes, info: core_schema.ValidationInfo) -> str:

Args:
function: The validator function to call
field_name: The name of the field
field_name: The name of the field this validator is applied to, if any (deprecated)
schema: The schema to validate the output of the validator function
ref: optional unique identifier of the schema, used to reference the schema in other places
json_schema_input_schema: The core schema to be used to generate the corresponding JSON Schema input type
metadata: Any other information you want to include with the schema, not used by pydantic-core
serialization: Custom serialization schema
"""
if field_name is not None:
warnings.warn(
'The `field_name` argument on `with_info_before_validator_function` is deprecated, it will be passed to the function through `ValidationState` instead.',
DeprecationWarning,
stacklevel=2,
)

return _dict_not_none(
type='function-before',
function=_dict_not_none(type='with-info', function=function, field_name=field_name),
@@ -2140,7 +2147,7 @@ def fn(v: str, info: core_schema.ValidationInfo) -> str:
return v + 'world'

func_schema = core_schema.with_info_after_validator_function(
function=fn, schema=core_schema.str_schema(), field_name='a'
function=fn, schema=core_schema.str_schema()
)
schema = core_schema.typed_dict_schema({'a': core_schema.typed_dict_field(func_schema)})

@@ -2151,11 +2158,18 @@ def fn(v: str, info: core_schema.ValidationInfo) -> str:
Args:
function: The validator function to call after the schema is validated
schema: The schema to validate before the validator function
field_name: The name of the field this validators is applied to, if any
field_name: The name of the field this validator is applied to, if any (deprecated)
ref: optional unique identifier of the schema, used to reference the schema in other places
metadata: Any other information you want to include with the schema, not used by pydantic-core
serialization: Custom serialization schema
"""
if field_name is not None:
warnings.warn(
'The `field_name` argument on `with_info_after_validator_function` is deprecated, it will be passed to the function through `ValidationState` instead.',
DeprecationWarning,
stacklevel=2,
)

return _dict_not_none(
type='function-after',
function=_dict_not_none(type='with-info', function=function, field_name=field_name),
@@ -2187,7 +2201,7 @@ class NoInfoWrapValidatorFunctionSchema(TypedDict):
class WithInfoWrapValidatorFunctionSchema(TypedDict, total=False):
type: Required[Literal['with-info']]
function: Required[WithInfoWrapValidatorFunction]
field_name: str
field_name: str # deprecated


WrapValidatorFunction = Union[NoInfoWrapValidatorFunctionSchema, WithInfoWrapValidatorFunctionSchema]
@@ -2287,12 +2301,19 @@ def fn(
Args:
function: The validator function to call
schema: The schema to validate the output of the validator function
field_name: The name of the field this validators is applied to, if any
field_name: The name of the field this validator is applied to, if any (deprecated)
json_schema_input_schema: The core schema to be used to generate the corresponding JSON Schema input type
ref: optional unique identifier of the schema, used to reference the schema in other places
metadata: Any other information you want to include with the schema, not used by pydantic-core
serialization: Custom serialization schema
"""
if field_name is not None:
warnings.warn(
'The `field_name` argument on `with_info_wrap_validator_function` is deprecated, it will be passed to the function through `ValidationState` instead.',
DeprecationWarning,
stacklevel=2,
)

return _dict_not_none(
type='function-wrap',
function=_dict_not_none(type='with-info', function=function, field_name=field_name),
@@ -2379,12 +2400,19 @@ def fn(v: str, info: core_schema.ValidationInfo) -> str:

Args:
function: The validator function to call
field_name: The name of the field this validators is applied to, if any
field_name: The name of the field this validator is applied to, if any (deprecated)
ref: optional unique identifier of the schema, used to reference the schema in other places
json_schema_input_schema: The core schema to be used to generate the corresponding JSON Schema input type
metadata: Any other information you want to include with the schema, not used by pydantic-core
serialization: Custom serialization schema
"""
if field_name is not None:
warnings.warn(
'The `field_name` argument on `with_info_plain_validator_function` is deprecated, it will be passed to the function through `ValidationState` instead.',
DeprecationWarning,
stacklevel=2,
)

return _dict_not_none(
type='function-plain',
function=_dict_not_none(type='with-info', function=function, field_name=field_name),
3 changes: 3 additions & 0 deletions src/validators/arguments.rs
Original file line number Diff line number Diff line change
@@ -229,6 +229,9 @@ impl Validator for ArgumentsValidator {
}
}

let state =
&mut state.rebind_extra(|extra| extra.field_name = Some(PyString::new(py, parameter.name.as_str())));

match (pos_value, kw_value) {
(Some(_), Some((_, kw_value))) => {
errors.push(ValLineError::new_with_loc(
25 changes: 14 additions & 11 deletions src/validators/dataclass.rs
Original file line number Diff line number Diff line change
@@ -24,7 +24,7 @@ use super::{build_validator, BuildValidator, CombinedValidator, DefinitionsBuild
struct Field {
kw_only: bool,
name: String,
py_name: Py<PyString>,
name_py: Py<PyString>,
init: bool,
init_only: bool,
lookup_key_collection: LookupKeyCollection,
@@ -72,8 +72,8 @@ impl BuildValidator for DataclassArgsValidator {
for field in fields_schema {
let field = field.downcast::<PyDict>()?;

let py_name: Bound<'_, PyString> = field.get_as_req(intern!(py, "name"))?;
let name: String = py_name.extract()?;
let name_py: Bound<'_, PyString> = field.get_as_req(intern!(py, "name"))?;
let name: String = name_py.extract()?;

let schema = field.get_as_req(intern!(py, "schema"))?;

@@ -99,7 +99,7 @@ impl BuildValidator for DataclassArgsValidator {
fields.push(Field {
kw_only,
name,
py_name: py_name.into(),
name_py: name_py.into(),
lookup_key_collection,
validator,
init: field.get_as(intern!(py, "init"))?.unwrap_or(true),
@@ -163,13 +163,13 @@ impl Validator for DataclassArgsValidator {

macro_rules! set_item {
($field:ident, $value:expr) => {{
let py_name = $field.py_name.bind(py);
let name_py = $field.name_py.bind(py);
if $field.init_only {
if let Some(ref mut init_only_args) = init_only_args {
init_only_args.push($value);
}
} else {
output_dict.set_item(py_name, $value)?;
output_dict.set_item(name_py, $value)?;
}
}};
}
@@ -214,6 +214,8 @@ impl Validator for DataclassArgsValidator {
}
let kw_value = kw_value.as_ref().map(|(path, value)| (path, value.borrow_input()));

let state = &mut state.rebind_extra(|extra| extra.field_name = Some(field.name_py.bind(py).clone()));

match (pos_value, kw_value) {
// found both positional and keyword arguments, error
(Some(_), Some((_, kw_value))) => {
@@ -404,11 +406,12 @@ impl Validator for DataclassArgsValidator {
}
}

match field.validator.validate(
py,
field_value,
&mut state.rebind_extra(|extra| extra.data = Some(data_dict.clone())),
) {
let state = &mut state.rebind_extra(|extra| {
extra.data = Some(data_dict.clone());
extra.field_name = Some(field.name_py.bind(py).clone());
});

match field.validator.validate(py, field_value, state) {
Ok(output) => ok(output),
Err(ValError::LineErrors(line_errors)) => {
let errors = line_errors
32 changes: 28 additions & 4 deletions src/validators/function.rs
Original file line number Diff line number Diff line change
@@ -100,7 +100,13 @@ impl FunctionBeforeValidator {
state: &'s mut ValidationState<'_, 'py>,
) -> ValResult<PyObject> {
let r = if self.info_arg {
let info = ValidationInfo::new(py, state.extra(), &self.config, self.field_name.clone());
let field_name = state
.extra()
.field_name
.clone()
.map(Bound::unbind)
.or_else(|| self.field_name.clone());
let info = ValidationInfo::new(py, state.extra(), &self.config, field_name);
self.func.call1(py, (input.to_object(py)?, info))
} else {
self.func.call1(py, (input.to_object(py)?,))
@@ -169,7 +175,13 @@ impl FunctionAfterValidator {
) -> ValResult<PyObject> {
let v = call(input, state)?;
let r = if self.info_arg {
let info = ValidationInfo::new(py, state.extra(), &self.config, self.field_name.clone());
let field_name = state
.extra()
.field_name
.clone()
.map(Bound::unbind)
.or_else(|| self.field_name.clone());
let info = ValidationInfo::new(py, state.extra(), &self.config, field_name);
self.func.call1(py, (v, info))
} else {
self.func.call1(py, (v,))
@@ -258,7 +270,13 @@ impl Validator for FunctionPlainValidator {
state: &mut ValidationState<'_, 'py>,
) -> ValResult<PyObject> {
let r = if self.info_arg {
let info = ValidationInfo::new(py, state.extra(), &self.config, self.field_name.clone());
let field_name = state
.extra()
.field_name
.clone()
.map(Bound::unbind)
.or_else(|| self.field_name.clone());
let info = ValidationInfo::new(py, state.extra(), &self.config, field_name);
self.func.call1(py, (input.to_object(py)?, info))
} else {
self.func.call1(py, (input.to_object(py)?,))
@@ -322,7 +340,13 @@ impl FunctionWrapValidator {
state: &mut ValidationState<'_, 'py>,
) -> ValResult<PyObject> {
let r = if self.info_arg {
let info = ValidationInfo::new(py, state.extra(), &self.config, self.field_name.clone());
let field_name = state
.extra()
.field_name
.clone()
.map(Bound::unbind)
.or_else(|| self.field_name.clone());
let info = ValidationInfo::new(py, state.extra(), &self.config, field_name);
self.func.call1(py, (input.to_object(py)?, handler, info))
} else {
self.func.call1(py, (input.to_object(py)?, handler))
2 changes: 2 additions & 0 deletions src/validators/generator.rs
Original file line number Diff line number Diff line change
@@ -276,6 +276,7 @@ impl InternalValidator {
data: self.data.as_ref().map(|data| data.bind(py).clone()),
strict: self.strict,
from_attributes: self.from_attributes,
field_name: Some(PyString::new(py, field_name)),
context: self.context.as_ref().map(|data| data.bind(py)),
self_instance: self.self_instance.as_ref().map(|data| data.bind(py)),
cache_str: self.cache_str,
@@ -313,6 +314,7 @@ impl InternalValidator {
data: self.data.as_ref().map(|data| data.bind(py).clone()),
strict: self.strict,
from_attributes: self.from_attributes,
field_name: None,
context: self.context.as_ref().map(|data| data.bind(py)),
self_instance: self.self_instance.as_ref().map(|data| data.bind(py)),
cache_str: self.cache_str,
6 changes: 6 additions & 0 deletions src/validators/mod.rs
Original file line number Diff line number Diff line change
@@ -311,6 +311,7 @@ impl SchemaValidator {
data: None,
strict,
from_attributes,
field_name: Some(PyString::new(py, field_name)),
context,
self_instance: None,
cache_str: self.cache_str,
@@ -337,6 +338,7 @@ impl SchemaValidator {
data: None,
strict,
from_attributes: None,
field_name: None,
context,
self_instance: None,
cache_str: self.cache_str,
@@ -678,6 +680,8 @@ pub struct Extra<'a, 'py> {
pub from_attributes: Option<bool>,
/// context used in validator functions
pub context: Option<&'a Bound<'py, PyAny>>,
/// The name of the field being validated, if applicable
pub field_name: Option<Bound<'py, PyString>>,
/// This is an instance of the model or dataclass being validated, when validation is performed from `__init__`
self_instance: Option<&'a Bound<'py, PyAny>>,
/// Whether to use a cache of short strings to accelerate python string construction
@@ -705,6 +709,7 @@ impl<'a, 'py> Extra<'a, 'py> {
data: None,
strict,
from_attributes,
field_name: None,
context,
self_instance,
cache_str,
@@ -721,6 +726,7 @@ impl Extra<'_, '_> {
data: self.data.clone(),
strict: Some(true),
from_attributes: self.from_attributes,
field_name: self.field_name.clone(),
context: self.context,
self_instance: self.self_instance,
cache_str: self.cache_str,
14 changes: 11 additions & 3 deletions src/validators/model.rs
Original file line number Diff line number Diff line change
@@ -202,6 +202,7 @@ impl Validator for ModelValidator {
field_name.to_string(),
))
} else {
let state = &mut state.rebind_extra(|extra| extra.field_name = Some(PyString::new(py, ROOT_FIELD)));
let output = self.validator.validate(py, field_value, state)?;

force_setattr(py, model, intern!(py, ROOT_FIELD), output)?;
@@ -255,9 +256,11 @@ impl ModelValidator {
// we need to set `self_instance` to None for nested validators as we don't want to operate on self_instance
// anymore
let state = &mut state.rebind_extra(|extra| extra.self_instance = None);
let output = self.validator.validate(py, input, state)?;

if self.root_model {
let state = &mut state.rebind_extra(|extra| extra.field_name = Some(PyString::new(py, ROOT_FIELD)));
let output = self.validator.validate(py, input, state)?;

let fields_set = if input.as_python().is_some_and(|py_input| py_input.is(&self.undefined)) {
PySet::empty(py)?
} else {
@@ -266,6 +269,8 @@ impl ModelValidator {
force_setattr(py, self_instance, intern!(py, DUNDER_FIELDS_SET_KEY), &fields_set)?;
force_setattr(py, self_instance, intern!(py, ROOT_FIELD), &output)?;
} else {
let output = self.validator.validate(py, input, state)?;

let (model_dict, model_extra, fields_set): (Bound<PyAny>, Bound<PyAny>, Bound<PyAny>) =
output.extract(py)?;
set_model_attrs(self_instance, &model_dict, &model_extra, &fields_set)?;
@@ -294,11 +299,12 @@ impl ModelValidator {
}
}

let output = self.validator.validate(py, input, state)?;

let instance = create_class(self.class.bind(py))?;

if self.root_model {
let state = &mut state.rebind_extra(|extra| extra.field_name = Some(PyString::new(py, ROOT_FIELD)));
let output = self.validator.validate(py, input, state)?;

let fields_set = if input.as_python().is_some_and(|py_input| py_input.is(&self.undefined)) {
PySet::empty(py)?
} else {
@@ -307,6 +313,8 @@ impl ModelValidator {
force_setattr(py, &instance, intern!(py, DUNDER_FIELDS_SET_KEY), &fields_set)?;
force_setattr(py, &instance, intern!(py, ROOT_FIELD), output)?;
} else {
let output = self.validator.validate(py, input, state)?;

let (model_dict, model_extra, val_fields_set): (Bound<PyAny>, Bound<PyAny>, Bound<PyAny>) =
output.extract(py)?;
let fields_set = existing_fields_set.unwrap_or(&val_fields_set);
6 changes: 6 additions & 0 deletions src/validators/model_fields.rs
Original file line number Diff line number Diff line change
@@ -197,6 +197,10 @@ impl Validator for ModelFieldsValidator {
// extra logic either way
used_keys.insert(lookup_path.first_key());
}

let state =
&mut state.rebind_extra(|extra| extra.field_name = Some(field.name_py.bind(py).clone()));

match field.validator.validate(py, value.borrow_input(), state) {
Ok(value) => {
model_dict.set_item(&field.name_py, value)?;
@@ -422,6 +426,8 @@ impl Validator for ModelFieldsValidator {
));
}

let state = &mut state.rebind_extra(|extra| extra.field_name = Some(field.name_py.bind(py).clone()));

prepare_result(field.validator.validate(py, field_value, state))?
} else {
// Handle extra (unknown) field
Loading