diff --git a/src/validators/dataclass.rs b/src/validators/dataclass.rs index dff7735a3..2706cadef 100644 --- a/src/validators/dataclass.rs +++ b/src/validators/dataclass.rs @@ -232,19 +232,31 @@ impl Validator for DataclassArgsValidator { } // found neither, check if there is a default value, otherwise error (None, None) => { - if let Some(value) = - field - .validator - .default_value(py, Some(field.name.as_str()), state)? - { - set_item!(field, value); - } else { - errors.push(field.lookup_key.error( - ErrorTypeDefaults::Missing, - input, - self.loc_by_alias, - &field.name, - )); + match field.validator.default_value(py, Some(field.name.as_str()), state) { + Ok(Some(value)) => { + // Default value exists, and passed validation if required + set_item!(field, value); + }, + Ok(None) => { + // This means there was no default value + errors.push(field.lookup_key.error( + ErrorTypeDefaults::Missing, + input, + self.loc_by_alias, + &field.name + )); + }, + Err(ValError::Omit) => continue, + Err(ValError::LineErrors(line_errors)) => { + for err in line_errors { + // Note: this will always use the field name even if there is an alias + // However, we don't mind so much because this error can only happen if the + // default value fails validation, which is arguably a developer error. + // We could try to "fix" this in the future if desired. + errors.push(err); + } + } + Err(err) => return Err(err), } } } diff --git a/src/validators/model_fields.rs b/src/validators/model_fields.rs index 30d937cb8..774d3eef9 100644 --- a/src/validators/model_fields.rs +++ b/src/validators/model_fields.rs @@ -211,15 +211,33 @@ impl Validator for ModelFieldsValidator { Err(err) => return ControlFlow::Break(err.into_owned(py)), } continue; - } else if let Some(value) = control_flow!(field.validator.default_value(py, Some(field.name.as_str()), state))? { - control_flow!(model_dict.set_item(&field.name_py, value))?; - } else { - errors.push(field.lookup_key.error( - ErrorTypeDefaults::Missing, - input, - self.loc_by_alias, - &field.name - )); + } + + match field.validator.default_value(py, Some(field.name.as_str()), state) { + Ok(Some(value)) => { + // Default value exists, and passed validation if required + control_flow!(model_dict.set_item(&field.name_py, value))?; + }, + Ok(None) => { + // This means there was no default value + errors.push(field.lookup_key.error( + ErrorTypeDefaults::Missing, + input, + self.loc_by_alias, + &field.name + )); + }, + Err(ValError::Omit) => continue, + Err(ValError::LineErrors(line_errors)) => { + for err in line_errors { + // Note: this will always use the field name even if there is an alias + // However, we don't mind so much because this error can only happen if the + // default value fails validation, which is arguably a developer error. + // We could try to "fix" this in the future if desired. + errors.push(err); + } + } + Err(err) => return ControlFlow::Break(err), } } ControlFlow::Continue(()) diff --git a/src/validators/typed_dict.rs b/src/validators/typed_dict.rs index 1a5b52dc6..118d992a2 100644 --- a/src/validators/typed_dict.rs +++ b/src/validators/typed_dict.rs @@ -212,15 +212,35 @@ impl Validator for TypedDictValidator { Err(err) => return ControlFlow::Break(err.into_owned(py)), } continue; - } else if let Some(value) = control_flow!(field.validator.default_value(py, Some(field.name.as_str()), state))? { - control_flow!(output_dict.set_item(&field.name_py, value))?; - } else if field.required { - errors.push(field.lookup_key.error( - ErrorTypeDefaults::Missing, - input, - self.loc_by_alias, - &field.name - )); + } + + match field.validator.default_value(py, Some(field.name.as_str()), state) { + Ok(Some(value)) => { + // Default value exists, and passed validation if required + control_flow!(output_dict.set_item(&field.name_py, value))?; + }, + Ok(None) => { + // This means there was no default value + if (field.required) { + errors.push(field.lookup_key.error( + ErrorTypeDefaults::Missing, + input, + self.loc_by_alias, + &field.name + )); + } + }, + Err(ValError::Omit) => continue, + Err(ValError::LineErrors(line_errors)) => { + for err in line_errors { + // Note: this will always use the field name even if there is an alias + // However, we don't mind so much because this error can only happen if the + // default value fails validation, which is arguably a developer error. + // We could try to "fix" this in the future if desired. + errors.push(err); + } + } + Err(err) => return ControlFlow::Break(err), } } ControlFlow::Continue(()) diff --git a/tests/validators/test_with_default.py b/tests/validators/test_with_default.py index 808e4807d..7ca0d9f54 100644 --- a/tests/validators/test_with_default.py +++ b/tests/validators/test_with_default.py @@ -654,3 +654,153 @@ def _validator(cls, v, info): gc.collect() assert ref() is None + + +validate_default_raises_examples = [ + ( + {}, + [ + {'type': 'assertion_error', 'loc': ('x',), 'msg': 'Assertion failed, ', 'input': None}, + {'type': 'assertion_error', 'loc': ('y',), 'msg': 'Assertion failed, ', 'input': None}, + {'type': 'missing', 'loc': ('z',), 'msg': 'Field required', 'input': {}}, + ], + ), + ( + {'z': 'some str'}, + [ + {'type': 'assertion_error', 'loc': ('x',), 'msg': 'Assertion failed, ', 'input': None}, + {'type': 'assertion_error', 'loc': ('y',), 'msg': 'Assertion failed, ', 'input': None}, + ], + ), + ( + {'x': None}, + [ + {'type': 'assertion_error', 'loc': ('x',), 'msg': 'Assertion failed, ', 'input': None}, + {'type': 'assertion_error', 'loc': ('y',), 'msg': 'Assertion failed, ', 'input': None}, + {'type': 'missing', 'loc': ('z',), 'msg': 'Field required', 'input': {'x': None}}, + ], + ), + ( + {'x': None, 'z': 'some str'}, + [ + {'type': 'assertion_error', 'loc': ('x',), 'msg': 'Assertion failed, ', 'input': None}, + {'type': 'assertion_error', 'loc': ('y',), 'msg': 'Assertion failed, ', 'input': None}, + ], + ), + ( + {'y': None}, + [ + {'type': 'assertion_error', 'loc': ('x',), 'msg': 'Assertion failed, ', 'input': None}, + {'type': 'assertion_error', 'loc': ('y',), 'msg': 'Assertion failed, ', 'input': None}, + {'type': 'missing', 'loc': ('z',), 'msg': 'Field required', 'input': {'y': None}}, + ], + ), + ( + {'y': None, 'z': 'some str'}, + [ + {'type': 'assertion_error', 'loc': ('x',), 'msg': 'Assertion failed, ', 'input': None}, + {'type': 'assertion_error', 'loc': ('y',), 'msg': 'Assertion failed, ', 'input': None}, + ], + ), + ( + {'x': None, 'y': None}, + [ + {'type': 'assertion_error', 'loc': ('x',), 'msg': 'Assertion failed, ', 'input': None}, + {'type': 'assertion_error', 'loc': ('y',), 'msg': 'Assertion failed, ', 'input': None}, + {'type': 'missing', 'loc': ('z',), 'msg': 'Field required', 'input': {'x': None, 'y': None}}, + ], + ), + ( + {'x': None, 'y': None, 'z': 'some str'}, + [ + {'type': 'assertion_error', 'loc': ('x',), 'msg': 'Assertion failed, ', 'input': None}, + {'type': 'assertion_error', 'loc': ('y',), 'msg': 'Assertion failed, ', 'input': None}, + ], + ), + ( + {'x': 1, 'y': None, 'z': 'some str'}, + [ + {'type': 'assertion_error', 'loc': ('x',), 'msg': 'Assertion failed, ', 'input': 1}, + {'type': 'assertion_error', 'loc': ('y',), 'msg': 'Assertion failed, ', 'input': None}, + ], + ), + ( + {'x': None, 'y': 1, 'z': 'some str'}, + [ + {'type': 'assertion_error', 'loc': ('x',), 'msg': 'Assertion failed, ', 'input': None}, + {'type': 'assertion_error', 'loc': ('y',), 'msg': 'Assertion failed, ', 'input': 1}, + ], + ), + ( + {'x': 1, 'y': 1, 'z': 'some str'}, + [ + {'type': 'assertion_error', 'loc': ('x',), 'msg': 'Assertion failed, ', 'input': 1}, + {'type': 'assertion_error', 'loc': ('y',), 'msg': 'Assertion failed, ', 'input': 1}, + ], + ), +] + + +@pytest.mark.parametrize( + 'core_schema_constructor,field_constructor', + [ + (core_schema.model_fields_schema, core_schema.model_field), + (core_schema.typed_dict_schema, core_schema.typed_dict_field), + ], +) +@pytest.mark.parametrize('input_value,expected', validate_default_raises_examples) +def test_validate_default_raises( + core_schema_constructor: Union[core_schema.ModelFieldsSchema, core_schema.TypedDictSchema], + field_constructor: Union[core_schema.model_field, core_schema.typed_dict_field], + input_value: dict, + expected: Any, +) -> None: + def _raise(ex: Exception) -> None: + raise ex() + + inner_schema = core_schema.no_info_after_validator_function( + lambda x: _raise(AssertionError), core_schema.nullable_schema(core_schema.int_schema()) + ) + + v = SchemaValidator( + core_schema_constructor( + { + 'x': field_constructor( + core_schema.with_default_schema(inner_schema, default=None, validate_default=True) + ), + 'y': field_constructor( + core_schema.with_default_schema(inner_schema, default=None, validate_default=True) + ), + 'z': field_constructor(core_schema.str_schema()), + } + ) + ) + + with pytest.raises(ValidationError) as exc_info: + v.validate_python(input_value) + assert exc_info.value.errors(include_url=False, include_context=False) == expected + + +@pytest.mark.parametrize('input_value,expected', validate_default_raises_examples) +def test_validate_default_raises_dataclass(input_value: dict, expected: Any) -> None: + def _raise(ex: Exception) -> None: + raise ex() + + inner_schema = core_schema.no_info_after_validator_function( + lambda x: _raise(AssertionError), core_schema.nullable_schema(core_schema.int_schema()) + ) + + x = core_schema.dataclass_field( + name='x', schema=core_schema.with_default_schema(inner_schema, default=None, validate_default=True) + ) + y = core_schema.dataclass_field( + name='y', schema=core_schema.with_default_schema(inner_schema, default=None, validate_default=True) + ) + z = core_schema.dataclass_field(name='z', schema=core_schema.str_schema()) + + v = SchemaValidator(core_schema.dataclass_args_schema('XYZ', [x, y, z])) + + with pytest.raises(ValidationError) as exc_info: + v.validate_python(input_value) + + assert exc_info.value.errors(include_url=False, include_context=False) == expected