From 0621ae2d109f30aa8d829a291ccf63451fa85eaa Mon Sep 17 00:00:00 2001 From: Yurii Motov Date: Wed, 28 Jan 2026 17:20:53 +0100 Subject: [PATCH] Add `fail_fast` param to Field, add tests --- sqlmodel/main.py | 7 +++++ tests/test_pydantic/test_field.py | 48 +++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+) diff --git a/sqlmodel/main.py b/sqlmodel/main.py index fe9eb4e6..07c836fb 100644 --- a/sqlmodel/main.py +++ b/sqlmodel/main.py @@ -229,6 +229,7 @@ def Field( min_length: Optional[int] = None, max_length: Optional[int] = None, union_mode: Optional[Literal["smart", "left_to_right"]] = None, + fail_fast: Optional[bool] = None, allow_mutation: bool = True, regex: Optional[str] = None, discriminator: Optional[str] = None, @@ -275,6 +276,7 @@ def Field( min_length: Optional[int] = None, max_length: Optional[int] = None, union_mode: Optional[Literal["smart", "left_to_right"]] = None, + fail_fast: Optional[bool] = None, allow_mutation: bool = True, regex: Optional[str] = None, discriminator: Optional[str] = None, @@ -330,6 +332,7 @@ def Field( min_length: Optional[int] = None, max_length: Optional[int] = None, union_mode: Optional[Literal["smart", "left_to_right"]] = None, + fail_fast: Optional[bool] = None, allow_mutation: bool = True, regex: Optional[str] = None, discriminator: Optional[str] = None, @@ -366,6 +369,7 @@ def Field( min_length: Optional[int] = None, max_length: Optional[int] = None, union_mode: Optional[Literal["smart", "left_to_right"]] = None, + fail_fast: Optional[bool] = None, allow_mutation: bool = True, regex: Optional[str] = None, discriminator: Optional[str] = None, @@ -389,6 +393,7 @@ def Field( "coerce_numbers_to_str", "validate_default", "union_mode", + "fail_fast", ): if param_name in current_schema_extra: msg = f"Pass `{param_name}` parameter directly to Field instead of passing it via `schema_extra`" @@ -403,6 +408,7 @@ def Field( current_validate_default = validate_default or current_schema_extra.pop( "validate_default", None ) + current_fail_fast = fail_fast or current_schema_extra.pop("fail_fast", None) field_info_kwargs = { "alias": alias, "title": title, @@ -424,6 +430,7 @@ def Field( "unique_items": unique_items, "min_length": min_length, "max_length": max_length, + "fail_fast": current_fail_fast, "allow_mutation": allow_mutation, "regex": regex, "discriminator": discriminator, diff --git a/tests/test_pydantic/test_field.py b/tests/test_pydantic/test_field.py index 0635ce73..284cb81e 100644 --- a/tests/test_pydantic/test_field.py +++ b/tests/test_pydantic/test_field.py @@ -195,3 +195,51 @@ def test_union_mode_via_schema_extra(): # Current workaround. Remove after some c = Model.model_validate({"val": 123.1}) assert isinstance(c.val, float) + + +def test_fail_fast_true(): + class Model(SQLModel): + val: list[int] = Field(fail_fast=True) + + with pytest.raises(ValidationError) as exc_info: + Model.model_validate({"val": [1.1, "not an int"]}) + + errors = exc_info.value.errors() + assert len(errors) == 1 + assert errors[0]["type"] == "int_from_float" + + +@pytest.mark.parametrize("fail_fast", [None, False]) +def test_fail_fast_false(fail_fast: Optional[bool]): + class Model(SQLModel): + val: list[int] = Field(fail_fast=fail_fast) + + with pytest.raises(ValidationError) as exc_info: + Model.model_validate({"val": [1.1, "not an int"]}) + + errors = exc_info.value.errors() + assert len(errors) == 2 + error_types = {error["type"] for error in errors} + + assert "int_from_float" in error_types + assert "int_parsing" in error_types + + +def test_fail_fast_via_schema_extra(): # Current workaround. Remove after some time + with pytest.warns( + UserWarning, + match=( + "Pass `fail_fast` parameter directly to Field instead of passing " + "it via `schema_extra`" + ), + ): + + class Model(SQLModel): + val: list[int] = Field(schema_extra={"fail_fast": True}) + + with pytest.raises(ValidationError) as exc_info: + Model.model_validate({"val": [1.1, "not an int"]}) + + errors = exc_info.value.errors() + assert len(errors) == 1 + assert errors[0]["type"] == "int_from_float" -- 2.47.3