diff --git a/python/pydantic_core/core_schema.py b/python/pydantic_core/core_schema.py index dc8f55b7c..a04427e0c 100644 --- a/python/pydantic_core/core_schema.py +++ b/python/pydantic_core/core_schema.py @@ -1939,6 +1939,7 @@ class DictSchema(TypedDict, total=False): values_schema: CoreSchema # default: AnySchema min_length: int max_length: int + fail_fast: bool strict: bool ref: str metadata: dict[str, Any] @@ -1951,6 +1952,7 @@ def dict_schema( *, min_length: int | None = None, max_length: int | None = None, + fail_fast: bool | None = None, strict: bool | None = None, ref: str | None = None, metadata: dict[str, Any] | None = None, @@ -1974,6 +1976,7 @@ def dict_schema( values_schema: The value must be a dict with values that match this schema min_length: The value must be a dict with at least this many items max_length: The value must be a dict with at most this many items + fail_fast: Stop validation on the first error strict: Whether the keys and values should be validated with strict mode 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 @@ -1985,6 +1988,7 @@ def dict_schema( values_schema=values_schema, min_length=min_length, max_length=max_length, + fail_fast=fail_fast, strict=strict, ref=ref, metadata=metadata, diff --git a/src/validators/dict.rs b/src/validators/dict.rs index 77bcde983..48729e7ee 100644 --- a/src/validators/dict.rs +++ b/src/validators/dict.rs @@ -21,6 +21,7 @@ pub struct DictValidator { value_validator: Box, min_length: Option, max_length: Option, + fail_fast: bool, name: String, } @@ -53,6 +54,7 @@ impl BuildValidator for DictValidator { value_validator, min_length: schema.get_as(intern!(py, "min_length"))?, max_length: schema.get_as(intern!(py, "max_length"))?, + fail_fast: schema.get_as(intern!(py, "fail_fast"))?.unwrap_or(false), name, } .into()) @@ -78,6 +80,7 @@ impl Validator for DictValidator { input, min_length: self.min_length, max_length: self.max_length, + fail_fast: self.fail_fast, key_validator: &self.key_validator, value_validator: &self.value_validator, state, @@ -94,6 +97,7 @@ struct ValidateToDict<'a, 's, 'py, I: Input<'py> + ?Sized> { input: &'a I, min_length: Option, max_length: Option, + fail_fast: bool, key_validator: &'a CombinedValidator, value_validator: &'a CombinedValidator, state: &'a mut ValidationState<'s, 'py>, @@ -111,6 +115,12 @@ where let mut errors: Vec = Vec::new(); let allow_partial = self.state.allow_partial; + macro_rules! should_fail_fast { + () => { + self.fail_fast && !errors.is_empty() + }; + } + for (_, is_last_partial, item_result) in self.state.enumerate_last_partial(iterator) { self.state.allow_partial = false.into(); let (key, value) = item_result?; @@ -130,6 +140,11 @@ where true => allow_partial, false => false.into(), }; + + if should_fail_fast!() { + break; + } + let output_value = match self.value_validator.validate(self.py, value.borrow_input(), self.state) { Ok(value) => value, Err(ValError::LineErrors(line_errors)) => { @@ -141,6 +156,11 @@ where Err(ValError::Omit) => continue, Err(err) => return Err(err), }; + + if should_fail_fast!() { + break; + } + if let Some(key) = output_key { output.set_item(key, output_value)?; } diff --git a/tests/validators/test_dict.py b/tests/validators/test_dict.py index bff6cf5a3..939d089d9 100644 --- a/tests/validators/test_dict.py +++ b/tests/validators/test_dict.py @@ -255,3 +255,61 @@ def test_json_dict_complex_key(): assert v.validate_json('{"1+2j": 2, "infj": 4}') == {complex(1, 2): 2, complex(0, float('inf')): 4} with pytest.raises(ValidationError, match='Input should be a valid complex string'): v.validate_json('{"1+2j": 2, "": 4}') == {complex(1, 2): 2, complex(0, float('inf')): 4} + + +@pytest.mark.parametrize( + ('fail_fast', 'expected'), + [ + pytest.param( + True, + [ + { + 'type': 'int_parsing', + 'loc': ('a', '[key]'), + 'msg': 'Input should be a valid integer, unable to parse string as an integer', + 'input': 'a', + }, + ], + id='fail_fast', + ), + pytest.param( + False, + [ + { + 'type': 'int_parsing', + 'loc': ('a', '[key]'), + 'msg': 'Input should be a valid integer, unable to parse string as an integer', + 'input': 'a', + }, + { + 'type': 'int_parsing', + 'loc': ('a',), + 'msg': 'Input should be a valid integer, unable to parse string as an integer', + 'input': 'b', + }, + { + 'type': 'int_parsing', + 'loc': ('c', '[key]'), + 'msg': 'Input should be a valid integer, unable to parse string as an integer', + 'input': 'c', + }, + { + 'type': 'int_parsing', + 'loc': ('c',), + 'msg': 'Input should be a valid integer, unable to parse string as an integer', + 'input': 'd', + }, + ], + id='not_fail_fast', + ), + ], +) +def test_dict_fail_fast(fail_fast, expected): + v = SchemaValidator( + {'type': 'dict', 'keys_schema': {'type': 'int'}, 'values_schema': {'type': 'int'}, 'fail_fast': fail_fast} + ) + + with pytest.raises(ValidationError) as exc_info: + v.validate_python({'a': 'b', 'c': 'd'}) + + assert exc_info.value.errors(include_url=False) == expected