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
Expand Up @@ -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]
Expand Down Expand Up @@ -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)})

Expand All @@ -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),
Expand Down Expand Up @@ -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)})

Expand All @@ -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),
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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),
Expand Down
3 changes: 3 additions & 0 deletions src/validators/arguments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
25 changes: 14 additions & 11 deletions src/validators/dataclass.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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"))?;

Expand All @@ -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),
Expand Down Expand Up @@ -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)?;
}
}};
}
Expand Down Expand Up @@ -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))) => {
Expand Down Expand Up @@ -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
Expand Down
32 changes: 28 additions & 4 deletions src/validators/function.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)?,))
Expand Down Expand Up @@ -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,))
Expand Down Expand Up @@ -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)?,))
Expand Down Expand Up @@ -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))
Expand Down
2 changes: 2 additions & 0 deletions src/validators/generator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
6 changes: 6 additions & 0 deletions src/validators/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -337,6 +338,7 @@ impl SchemaValidator {
data: None,
strict,
from_attributes: None,
field_name: None,
context,
self_instance: None,
cache_str: self.cache_str,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -705,6 +709,7 @@ impl<'a, 'py> Extra<'a, 'py> {
data: None,
strict,
from_attributes,
field_name: None,
context,
self_instance,
cache_str,
Expand All @@ -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,
Expand Down
14 changes: 11 additions & 3 deletions src/validators/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)?;
Expand Down Expand Up @@ -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 {
Expand All @@ -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)?;
Expand Down Expand Up @@ -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 {
Expand All @@ -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);
Expand Down
6 changes: 6 additions & 0 deletions src/validators/model_fields.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)?;
Expand Down Expand Up @@ -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
Expand Down
Loading
Loading