]> git.ipfire.org Git - thirdparty/fastapi/sqlmodel.git/commitdiff
Merge remote-tracking branch 'upstream/main' into add-missing-parameters-to-field
authorYurii Motov <yurii.motov.monte@gmail.com>
Fri, 20 Feb 2026 22:35:29 +0000 (23:35 +0100)
committerYurii Motov <yurii.motov.monte@gmail.com>
Fri, 20 Feb 2026 22:35:29 +0000 (23:35 +0100)
1  2 
sqlmodel/main.py
tests/test_pydantic/test_field.py

index 2628dd79696916b140f7daa9505896bb3584e21e,300031de8bd2783f5eeb7b33a2d5b77e755fc06e..f72d6045e30b59b25954df688d782ac9219992ea
@@@ -3,9 -3,9 +3,10 @@@ from __future__ import annotation
  import builtins
  import ipaddress
  import uuid
 +import warnings
  import weakref
- from collections.abc import Mapping, Sequence, Set
+ from collections.abc import Callable, Mapping, Sequence, Set
+ from dataclasses import dataclass
  from datetime import date, datetime, time, timedelta
  from decimal import Decimal
  from enum import Enum
@@@ -207,45 -238,40 +240,45 @@@ def _get_sqlmodel_field_value
  def Field(
      default: Any = Undefined,
      *,
-     default_factory: Optional[NoArgAnyCallable] = None,
-     alias: Optional[str] = None,
-     validation_alias: Optional[str] = None,
-     serialization_alias: Optional[str] = None,
-     title: Optional[str] = None,
-     field_title_generator: Optional[Callable[[str, PydanticFieldInfo], str]] = None,
-     description: Optional[str] = None,
-     examples: Optional[list[Any]] = None,
-     deprecated: Union[Deprecated, str, bool, None] = None,
-     exclude: Union[Set[Union[int, str]], Mapping[Union[int, str], Any], Any] = None,
-     exclude_if: Optional[Callable[[Any], bool]] = None,
-     include: Union[Set[Union[int, str]], Mapping[Union[int, str], Any], Any] = None,
-     const: Optional[bool] = None,
-     gt: Optional[float] = None,
-     ge: Optional[float] = None,
-     lt: Optional[float] = None,
-     le: Optional[float] = None,
-     multiple_of: Optional[float] = None,
-     max_digits: Optional[int] = None,
-     decimal_places: Optional[int] = None,
-     min_items: Optional[int] = None,
-     max_items: Optional[int] = None,
-     unique_items: Optional[bool] = None,
-     min_length: Optional[int] = None,
-     max_length: Optional[int] = None,
+     default_factory: NoArgAnyCallable | None = None,
+     alias: str | None = None,
+     validation_alias: str | None = None,
+     serialization_alias: str | None = None,
+     title: str | None = None,
++    field_title_generator: Callable[[str, PydanticFieldInfo], str] | None = None,
+     description: str | None = None,
++    examples: list[Any] | None = None,
++    deprecated: Deprecated | str | bool | None = None,
+     exclude: Set[int | str] | Mapping[int | str, Any] | Any = None,
++    exclude_if: Callable[[Any], bool] | None = None,
+     include: Set[int | str] | Mapping[int | str, Any] | Any = None,
+     const: bool | None = None,
+     gt: float | None = None,
+     ge: float | None = None,
+     lt: float | None = None,
+     le: float | None = None,
+     multiple_of: float | None = None,
+     max_digits: int | None = None,
+     decimal_places: int | None = None,
+     min_items: int | None = None,
+     max_items: int | None = None,
+     unique_items: bool | None = None,
+     min_length: int | None = None,
+     max_length: int | None = None,
      allow_mutation: bool = True,
-     regex: Optional[str] = None,
-     strict: Optional[bool] = None,
-     discriminator: Optional[str] = None,
+     regex: str | None = None,
++    strict: bool | None = None,
+     discriminator: str | None = None,
      repr: bool = True,
-     primary_key: Union[bool, UndefinedType] = Undefined,
+     primary_key: bool | UndefinedType = Undefined,
      foreign_key: Any = Undefined,
-     unique: Union[bool, UndefinedType] = Undefined,
-     nullable: Union[bool, UndefinedType] = Undefined,
-     index: Union[bool, UndefinedType] = Undefined,
-     sa_type: Union[type[Any], UndefinedType] = Undefined,
-     sa_column_args: Union[Sequence[Any], UndefinedType] = Undefined,
-     sa_column_kwargs: Union[Mapping[str, Any], UndefinedType] = Undefined,
-     schema_extra: Optional[dict[str, Any]] = None,
+     unique: bool | UndefinedType = Undefined,
+     nullable: bool | UndefinedType = Undefined,
+     index: bool | UndefinedType = Undefined,
+     sa_type: type[Any] | UndefinedType = Undefined,
+     sa_column_args: Sequence[Any] | UndefinedType = Undefined,
+     sa_column_kwargs: Mapping[str, Any] | UndefinedType = Undefined,
+     schema_extra: dict[str, Any] | None = None,
  ) -> Any: ...
  
  
  def Field(
      default: Any = Undefined,
      *,
-     default_factory: Optional[NoArgAnyCallable] = None,
-     alias: Optional[str] = None,
-     validation_alias: Optional[str] = None,
-     serialization_alias: Optional[str] = None,
-     title: Optional[str] = None,
-     field_title_generator: Optional[Callable[[str, PydanticFieldInfo], str]] = None,
-     description: Optional[str] = None,
-     examples: Optional[list[Any]] = None,
-     deprecated: Union[Deprecated, str, bool, None] = None,
-     exclude: Union[Set[Union[int, str]], Mapping[Union[int, str], Any], Any] = None,
-     exclude_if: Optional[Callable[[Any], bool]] = None,
-     include: Union[Set[Union[int, str]], Mapping[Union[int, str], Any], Any] = None,
-     const: Optional[bool] = None,
-     gt: Optional[float] = None,
-     ge: Optional[float] = None,
-     lt: Optional[float] = None,
-     le: Optional[float] = None,
-     multiple_of: Optional[float] = None,
-     max_digits: Optional[int] = None,
-     decimal_places: Optional[int] = None,
-     min_items: Optional[int] = None,
-     max_items: Optional[int] = None,
-     unique_items: Optional[bool] = None,
-     min_length: Optional[int] = None,
-     max_length: Optional[int] = None,
+     default_factory: NoArgAnyCallable | None = None,
+     alias: str | None = None,
+     validation_alias: str | None = None,
+     serialization_alias: str | None = None,
+     title: str | None = None,
++    field_title_generator: Callable[[str, PydanticFieldInfo], str] | None = None,
+     description: str | None = None,
++    examples: list[Any] | None = None,
++    deprecated: Deprecated | str | bool | None = None,
+     exclude: Set[int | str] | Mapping[int | str, Any] | Any = None,
++    exclude_if: Callable[[Any], bool] | None = None,
+     include: Set[int | str] | Mapping[int | str, Any] | Any = None,
+     const: bool | None = None,
+     gt: float | None = None,
+     ge: float | None = None,
+     lt: float | None = None,
+     le: float | None = None,
+     multiple_of: float | None = None,
+     max_digits: int | None = None,
+     decimal_places: int | None = None,
+     min_items: int | None = None,
+     max_items: int | None = None,
+     unique_items: bool | None = None,
+     min_length: int | None = None,
+     max_length: int | None = None,
      allow_mutation: bool = True,
-     regex: Optional[str] = None,
-     strict: Optional[bool] = None,
-     discriminator: Optional[str] = None,
+     regex: str | None = None,
++    strict: bool | None = None,
+     discriminator: str | None = None,
      repr: bool = True,
-     primary_key: Union[bool, UndefinedType] = Undefined,
+     primary_key: bool | UndefinedType = Undefined,
      foreign_key: str,
-     ondelete: Union[OnDeleteType, UndefinedType] = Undefined,
-     unique: Union[bool, UndefinedType] = Undefined,
-     nullable: Union[bool, UndefinedType] = Undefined,
-     index: Union[bool, UndefinedType] = Undefined,
-     sa_type: Union[type[Any], UndefinedType] = Undefined,
-     sa_column_args: Union[Sequence[Any], UndefinedType] = Undefined,
-     sa_column_kwargs: Union[Mapping[str, Any], UndefinedType] = Undefined,
-     schema_extra: Optional[dict[str, Any]] = None,
+     ondelete: OnDeleteType | UndefinedType = Undefined,
+     unique: bool | UndefinedType = Undefined,
+     nullable: bool | UndefinedType = Undefined,
+     index: bool | UndefinedType = Undefined,
+     sa_type: type[Any] | UndefinedType = Undefined,
+     sa_column_args: Sequence[Any] | UndefinedType = Undefined,
+     sa_column_kwargs: Mapping[str, Any] | UndefinedType = Undefined,
+     schema_extra: dict[str, Any] | None = None,
  ) -> Any: ...
  
  
  def Field(
      default: Any = Undefined,
      *,
-     default_factory: Optional[NoArgAnyCallable] = None,
-     alias: Optional[str] = None,
-     validation_alias: Optional[str] = None,
-     serialization_alias: Optional[str] = None,
-     title: Optional[str] = None,
-     field_title_generator: Optional[Callable[[str, PydanticFieldInfo], str]] = None,
-     description: Optional[str] = None,
-     examples: Optional[list[Any]] = None,
-     deprecated: Union[Deprecated, str, bool, None] = None,
-     exclude: Union[Set[Union[int, str]], Mapping[Union[int, str], Any], Any] = None,
-     exclude_if: Optional[Callable[[Any], bool]] = None,
-     include: Union[Set[Union[int, str]], Mapping[Union[int, str], Any], Any] = None,
-     const: Optional[bool] = None,
-     gt: Optional[float] = None,
-     ge: Optional[float] = None,
-     lt: Optional[float] = None,
-     le: Optional[float] = None,
-     multiple_of: Optional[float] = None,
-     max_digits: Optional[int] = None,
-     decimal_places: Optional[int] = None,
-     min_items: Optional[int] = None,
-     max_items: Optional[int] = None,
-     unique_items: Optional[bool] = None,
-     min_length: Optional[int] = None,
-     max_length: Optional[int] = None,
+     default_factory: NoArgAnyCallable | None = None,
+     alias: str | None = None,
+     validation_alias: str | None = None,
+     serialization_alias: str | None = None,
+     title: str | None = None,
++    field_title_generator: Callable[[str, PydanticFieldInfo], str] | None = None,
+     description: str | None = None,
++    examples: list[Any] | None = None,
++    deprecated: Deprecated | str | bool | None = None,
+     exclude: Set[int | str] | Mapping[int | str, Any] | Any = None,
++    exclude_if: Callable[[Any], bool] | None = None,
+     include: Set[int | str] | Mapping[int | str, Any] | Any = None,
+     const: bool | None = None,
+     gt: float | None = None,
+     ge: float | None = None,
+     lt: float | None = None,
+     le: float | None = None,
+     multiple_of: float | None = None,
+     max_digits: int | None = None,
+     decimal_places: int | None = None,
+     min_items: int | None = None,
+     max_items: int | None = None,
+     unique_items: bool | None = None,
+     min_length: int | None = None,
+     max_length: int | None = None,
      allow_mutation: bool = True,
-     regex: Optional[str] = None,
-     strict: Optional[bool] = None,
-     discriminator: Optional[str] = None,
+     regex: str | None = None,
++    strict: bool | None = None,
+     discriminator: str | None = None,
      repr: bool = True,
-     sa_column: Union[Column[Any], UndefinedType] = Undefined,
-     schema_extra: Optional[dict[str, Any]] = None,
+     sa_column: Column[Any] | UndefinedType = Undefined,
+     schema_extra: dict[str, Any] | None = None,
  ) -> Any: ...
  
  
  def Field(
      default: Any = Undefined,
      *,
-     default_factory: Optional[NoArgAnyCallable] = None,
-     alias: Optional[str] = None,
-     validation_alias: Optional[str] = None,
-     serialization_alias: Optional[str] = None,
-     title: Optional[str] = None,
-     field_title_generator: Optional[Callable[[str, PydanticFieldInfo], str]] = None,
-     description: Optional[str] = None,
-     examples: Optional[list[Any]] = None,
-     deprecated: Union[Deprecated, str, bool, None] = None,
-     exclude: Union[Set[Union[int, str]], Mapping[Union[int, str], Any], Any] = None,
-     exclude_if: Optional[Callable[[Any], bool]] = None,
-     include: Union[Set[Union[int, str]], Mapping[Union[int, str], Any], Any] = None,
-     const: Optional[bool] = None,
-     gt: Optional[float] = None,
-     ge: Optional[float] = None,
-     lt: Optional[float] = None,
-     le: Optional[float] = None,
-     multiple_of: Optional[float] = None,
-     max_digits: Optional[int] = None,
-     decimal_places: Optional[int] = None,
-     min_items: Optional[int] = None,
-     max_items: Optional[int] = None,
-     unique_items: Optional[bool] = None,
-     min_length: Optional[int] = None,
-     max_length: Optional[int] = None,
+     default_factory: NoArgAnyCallable | None = None,
+     alias: str | None = None,
+     validation_alias: str | None = None,
+     serialization_alias: str | None = None,
+     title: str | None = None,
++    field_title_generator: Callable[[str, PydanticFieldInfo], str] | None = None,
+     description: str | None = None,
++    examples: list[Any] | None = None,
++    deprecated: Deprecated | str | bool | None = None,
+     exclude: Set[int | str] | Mapping[int | str, Any] | Any = None,
++    exclude_if: Callable[[Any], bool] | None = None,
+     include: Set[int | str] | Mapping[int | str, Any] | Any = None,
+     const: bool | None = None,
+     gt: float | None = None,
+     ge: float | None = None,
+     lt: float | None = None,
+     le: float | None = None,
+     multiple_of: float | None = None,
+     max_digits: int | None = None,
+     decimal_places: int | None = None,
+     min_items: int | None = None,
+     max_items: int | None = None,
+     unique_items: bool | None = None,
+     min_length: int | None = None,
+     max_length: int | None = None,
      allow_mutation: bool = True,
-     regex: Optional[str] = None,
-     strict: Optional[bool] = None,
-     discriminator: Optional[str] = None,
+     regex: str | None = None,
++    strict: bool | None = None,
+     discriminator: str | None = None,
      repr: bool = True,
-     primary_key: Union[bool, UndefinedType] = Undefined,
+     primary_key: bool | UndefinedType = Undefined,
      foreign_key: Any = Undefined,
-     ondelete: Union[OnDeleteType, UndefinedType] = Undefined,
-     unique: Union[bool, UndefinedType] = Undefined,
-     nullable: Union[bool, UndefinedType] = Undefined,
-     index: Union[bool, UndefinedType] = Undefined,
-     sa_type: Union[type[Any], UndefinedType] = Undefined,
-     sa_column: Union[Column, UndefinedType] = Undefined,  # type: ignore
-     sa_column_args: Union[Sequence[Any], UndefinedType] = Undefined,
-     sa_column_kwargs: Union[Mapping[str, Any], UndefinedType] = Undefined,
-     schema_extra: Optional[dict[str, Any]] = None,
+     ondelete: OnDeleteType | UndefinedType = Undefined,
+     unique: bool | UndefinedType = Undefined,
+     nullable: bool | UndefinedType = Undefined,
+     index: bool | UndefinedType = Undefined,
+     sa_type: type[Any] | UndefinedType = Undefined,
+     sa_column: Column | UndefinedType = Undefined,  # type: ignore
+     sa_column_args: Sequence[Any] | UndefinedType = Undefined,
+     sa_column_kwargs: Mapping[str, Any] | UndefinedType = Undefined,
+     schema_extra: dict[str, Any] | None = None,
  ) -> Any:
      current_schema_extra = schema_extra or {}
 +
 +    for param_name in (
 +        "strict",
 +        "examples",
 +        "deprecated",
 +        "exclude_if",
 +        "field_title_generator",
 +    ):
 +        if param_name in current_schema_extra:
 +            msg = f"Pass `{param_name}` parameter directly to Field instead of passing it via `schema_extra`"
 +            warnings.warn(msg, DeprecationWarning, stacklevel=2)
 +
      # Extract possible alias settings from schema_extra so we can control precedence
      schema_validation_alias = current_schema_extra.pop("validation_alias", None)
      schema_serialization_alias = current_schema_extra.pop("serialization_alias", None)
index ab938b143b3ee5fd3db798d9da175769c2dadae3,11f4150d9847c0efc078a97ea862efde51b0cf3f..ff83ff24894ccccaa55627df5ccc607d5905c0ee
@@@ -1,5 -1,5 +1,5 @@@
  from decimal import Decimal
- from typing import Any, Literal, Optional, Union
 -from typing import Literal
++from typing import Any, Literal
  
  import pytest
  from pydantic import ValidationError
@@@ -54,200 -54,3 +54,200 @@@ def test_repr()
  
      instance = Model(id=123, foo="bar")
      assert "foo=" not in repr(instance)
-         id: Optional[int] = Field(default=None, primary_key=True)
 +
 +
 +def test_strict_true():
 +    class Model(SQLModel):
-         id: Optional[int] = Field(default=None, primary_key=True)
++        id: int | None = Field(default=None, primary_key=True)
 +        val: int
 +        val_strict: int = Field(strict=True)
 +
 +    class ModelDB(Model, table=True):
 +        pass
 +
 +    Model(val=123, val_strict=456)
 +    Model(val="123", val_strict=456)
 +
 +    with pytest.raises(ValidationError):
 +        Model(val=123, val_strict="456")
 +
 +    engine = create_engine("sqlite://", echo=True)
 +
 +    SQLModel.metadata.create_all(engine)
 +
 +    model = ModelDB(val=123, val_strict=456)
 +    with Session(engine) as session:
 +        session.add(model)
 +        session.commit()
 +        session.refresh(model)
 +
 +    assert model.val == 123
 +    assert model.val_strict == 456
 +
 +
 +def test_strict_table_model():
 +    class Model(SQLModel, table=True):
- def test_strict_false(strict: Optional[bool]):
++        id: int | None = Field(default=None, primary_key=True)
 +        val_strict: int = Field(strict=True)
 +
 +    engine = create_engine("sqlite://", echo=True)
 +
 +    SQLModel.metadata.create_all(engine)
 +
 +    model = Model(val_strict=456)
 +    with Session(engine) as session:
 +        session.add(model)
 +        session.commit()
 +        session.refresh(model)
 +
 +    assert model.val_strict == 456
 +
 +
 +@pytest.mark.parametrize("strict", [None, False])
++def test_strict_false(strict: int | None):
 +    class Model(SQLModel):
 +        val: int = Field(strict=strict)
 +
 +    Model(val=123)
 +    Model(val="123")
 +
 +
 +def test_strict_via_schema_extra():  # Current workaround. Remove after some time
 +    with pytest.warns(
 +        DeprecationWarning,
 +        match="Pass `strict` parameter directly to Field instead of passing it via `schema_extra`",
 +    ):
 +
 +        class Model(SQLModel):
 +            val: int
 +            val_strict: int = Field(schema_extra={"strict": True})
 +
 +    Model(val=123, val_strict=456)
 +    Model(val="123", val_strict=456)
 +
 +    with pytest.raises(ValidationError):
 +        Model(val=123, val_strict="456")
 +
 +
 +def test_examples():
 +    class Model(SQLModel):
 +        name: str = Field(examples=["Alice", "Bob"])
 +
 +    model_schema = Model.model_json_schema()
 +    assert model_schema["properties"]["name"]["examples"] == ["Alice", "Bob"]
 +
 +
 +def test_examples_via_schema_extra():  # Current workaround. Remove after some time
 +    with pytest.warns(
 +        DeprecationWarning,
 +        match="Pass `examples` parameter directly to Field instead of passing it via `schema_extra`",
 +    ):
 +
 +        class Model(SQLModel):
 +            name: str = Field(schema_extra={"examples": ["Alice", "Bob"]})
 +
 +    model_schema = Model.model_json_schema()
 +    assert model_schema["properties"]["name"]["examples"] == ["Alice", "Bob"]
 +
 +
 +def test_deprecated():
 +    class Model(SQLModel):
 +        old_field: str = Field(deprecated=True)
 +        another_old_field: str = Field(deprecated="This field is deprecated")
 +
 +    model_schema = Model.model_json_schema()
 +    assert model_schema["properties"]["old_field"]["deprecated"] is True
 +    assert model_schema["properties"]["another_old_field"]["deprecated"] is True
 +
 +
 +def test_deprecated_via_schema_extra():  # Current workaround. Remove after some time
 +    with pytest.warns(
 +        DeprecationWarning,
 +        match="Pass `deprecated` parameter directly to Field instead of passing it via `schema_extra`",
 +    ):
 +
 +        class Model(SQLModel):
 +            old_field: str = Field(schema_extra={"deprecated": True})
 +            another_old_field: str = Field(
 +                schema_extra={"deprecated": "This field is deprecated"}
 +            )
 +
 +    model_schema = Model.model_json_schema()
 +    assert model_schema["properties"]["old_field"]["deprecated"] is True
 +    assert model_schema["properties"]["another_old_field"]["deprecated"] is True
 +
 +
 +def test_exclude_if():
 +    def is_empty_string(value: Any) -> bool:
 +        return value == ""
 +
 +    class Model(SQLModel):
 +        name: str = Field(exclude_if=is_empty_string)
 +        age: int
 +
 +    model1 = Model(name="Alice", age=30)
 +    model2 = Model(name="", age=25)
 +
 +    dict1 = model1.model_dump()
 +    dict2 = model2.model_dump()
 +
 +    assert "name" in dict1
 +    assert dict1["name"] == "Alice"
 +
 +    assert "name" not in dict2
 +
 +
 +def test_exclude_if_via_schema_extra():
 +    def is_empty_string(value: Any) -> bool:
 +        return value == ""
 +
 +    with pytest.warns(
 +        DeprecationWarning,
 +        match="Pass `exclude_if` parameter directly to Field instead of passing it via `schema_extra`",
 +    ):
 +
 +        class Model(SQLModel):
 +            name: str = Field(schema_extra={"exclude_if": is_empty_string})
 +            age: int
 +
 +    model1 = Model(name="Alice", age=30)
 +    model2 = Model(name="", age=25)
 +
 +    dict1 = model1.model_dump()
 +    dict2 = model2.model_dump()
 +
 +    assert "name" in dict1
 +    assert dict1["name"] == "Alice"
 +
 +    assert "name" not in dict2
 +
 +
 +def test_field_title_generator():
 +    def upper(value: str, _: Any) -> str:
 +        return value.upper()
 +
 +    class Model(SQLModel):
 +        name: str = Field(field_title_generator=upper)
 +        age: int
 +
 +    model_schema = Model.model_json_schema()
 +    assert model_schema["properties"]["name"]["title"] == "NAME"
 +    assert model_schema["properties"]["age"]["title"] == "Age"
 +
 +
 +def test_field_title_generator_via_schema_extra():
 +    def upper(value: str, _: Any) -> str:
 +        return value.upper()
 +
 +    with pytest.warns(
 +        DeprecationWarning,
 +        match="Pass `field_title_generator` parameter directly to Field instead of passing it via `schema_extra`",
 +    ):
 +
 +        class Model(SQLModel):
 +            name: str = Field(schema_extra={"field_title_generator": upper})
 +            age: int
 +
 +    model_schema = Model.model_json_schema()
 +    assert model_schema["properties"]["name"]["title"] == "NAME"
 +    assert model_schema["properties"]["age"]["title"] == "Age"