Skip to content

Commit 537d0dd

Browse files
committed
Support exclude_if at field level
1 parent 15f693a commit 537d0dd

File tree

6 files changed

+70
-1
lines changed

6 files changed

+70
-1
lines changed

pydantic/_internal/_generate_schema.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1057,6 +1057,7 @@ def _generate_td_field_schema(
10571057
return core_schema.typed_dict_field(
10581058
common_field['schema'],
10591059
required=False if not field_info.is_required() else required,
1060+
exclude_if=field_info.exclude_if,
10601061
serialization_exclude=common_field['serialization_exclude'],
10611062
validation_alias=common_field['validation_alias'],
10621063
serialization_alias=common_field['serialization_alias'],
@@ -1073,6 +1074,7 @@ def _generate_md_field_schema(
10731074
common_field = self._common_field_schema(name, field_info, decorators)
10741075
return core_schema.model_field(
10751076
common_field['schema'],
1077+
exclude_if=field_info.exclude_if,
10761078
serialization_exclude=common_field['serialization_exclude'],
10771079
validation_alias=common_field['validation_alias'],
10781080
serialization_alias=common_field['serialization_alias'],
@@ -1094,6 +1096,7 @@ def _generate_dc_field_schema(
10941096
init=field_info.init,
10951097
init_only=field_info.init_var or None,
10961098
kw_only=None if field_info.kw_only else False,
1099+
exclude_if=field_info.exclude_if,
10971100
serialization_exclude=common_field['serialization_exclude'],
10981101
validation_alias=common_field['validation_alias'],
10991102
serialization_alias=common_field['serialization_alias'],

pydantic/fields.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ class _FromFieldInfoInputs(typing_extensions.TypedDict, total=False):
6060
description: str | None
6161
examples: list[Any] | None
6262
exclude: bool | None
63+
exclude_if: Callable[[Any], bool] | None
6364
gt: annotated_types.SupportsGt | None
6465
ge: annotated_types.SupportsGe | None
6566
lt: annotated_types.SupportsLt | None
@@ -116,6 +117,7 @@ class FieldInfo(_repr.Representation):
116117
description: The description of the field.
117118
examples: List of examples of the field.
118119
exclude: Whether to exclude the field from the model serialization.
120+
exclude_if: Callable that determines whether to exclude a field during serialization based on its value.
119121
discriminator: Field name or Discriminator for discriminating the type in a tagged union.
120122
deprecated: A deprecation message, an instance of `warnings.deprecated` or the `typing_extensions.deprecated` backport,
121123
or a boolean. If `True`, a default deprecation message will be emitted when accessing the field.
@@ -141,6 +143,7 @@ class FieldInfo(_repr.Representation):
141143
description: str | None
142144
examples: list[Any] | None
143145
exclude: bool | None
146+
exclude_if: Callable[[Any], bool] | None
144147
discriminator: str | types.Discriminator | None
145148
deprecated: Deprecated | str | bool | None
146149
json_schema_extra: JsonDict | Callable[[JsonDict], None] | None
@@ -166,6 +169,7 @@ class FieldInfo(_repr.Representation):
166169
'description',
167170
'examples',
168171
'exclude',
172+
'exclude_if',
169173
'discriminator',
170174
'deprecated',
171175
'json_schema_extra',
@@ -235,6 +239,7 @@ def __init__(self, **kwargs: Unpack[_FieldInfoInputs]) -> None:
235239
self.description = kwargs.pop('description', None)
236240
self.examples = kwargs.pop('examples', None)
237241
self.exclude = kwargs.pop('exclude', None)
242+
self.exclude_if = kwargs.pop('exclude_if', None)
238243
self.discriminator = kwargs.pop('discriminator', None)
239244
# For compatibility with FastAPI<=0.110.0, we preserve the existing value if it is not overridden
240245
self.deprecated = kwargs.pop('deprecated', getattr(self, 'deprecated', None))
@@ -703,6 +708,7 @@ class _EmptyKwargs(typing_extensions.TypedDict):
703708
'description': None,
704709
'examples': None,
705710
'exclude': None,
711+
'exclude_if': None,
706712
'discriminator': None,
707713
'json_schema_extra': None,
708714
'frozen': None,
@@ -745,6 +751,7 @@ def Field(
745751
description: str | None = _Unset,
746752
examples: list[Any] | None = _Unset,
747753
exclude: bool | None = _Unset,
754+
exclude_if: Callable[[Any], bool] | None = _Unset,
748755
discriminator: str | types.Discriminator | None = _Unset,
749756
deprecated: Deprecated | str | bool | None = _Unset,
750757
json_schema_extra: JsonDict | Callable[[JsonDict], None] | None = _Unset,
@@ -784,6 +791,7 @@ def Field(
784791
description: str | None = _Unset,
785792
examples: list[Any] | None = _Unset,
786793
exclude: bool | None = _Unset,
794+
exclude_if: Callable[[Any], bool] | None = _Unset,
787795
discriminator: str | types.Discriminator | None = _Unset,
788796
deprecated: Deprecated | str | bool | None = _Unset,
789797
json_schema_extra: JsonDict | Callable[[JsonDict], None] | None = _Unset,
@@ -823,6 +831,7 @@ def Field(
823831
description: str | None = _Unset,
824832
examples: list[Any] | None = _Unset,
825833
exclude: bool | None = _Unset,
834+
exclude_if: Callable[[Any], bool] | None = _Unset,
826835
discriminator: str | types.Discriminator | None = _Unset,
827836
deprecated: Deprecated | str | bool | None = _Unset,
828837
json_schema_extra: JsonDict | Callable[[JsonDict], None] | None = _Unset,
@@ -861,6 +870,7 @@ def Field( # No default set
861870
description: str | None = _Unset,
862871
examples: list[Any] | None = _Unset,
863872
exclude: bool | None = _Unset,
873+
exclude_if: Callable[[Any], bool] | None = _Unset,
864874
discriminator: str | types.Discriminator | None = _Unset,
865875
deprecated: Deprecated | str | bool | None = _Unset,
866876
json_schema_extra: JsonDict | Callable[[JsonDict], None] | None = _Unset,
@@ -900,6 +910,7 @@ def Field( # noqa: C901
900910
description: str | None = _Unset,
901911
examples: list[Any] | None = _Unset,
902912
exclude: bool | None = _Unset,
913+
exclude_if: Callable[[Any], bool] | None = _Unset,
903914
discriminator: str | types.Discriminator | None = _Unset,
904915
deprecated: Deprecated | str | bool | None = _Unset,
905916
json_schema_extra: JsonDict | Callable[[JsonDict], None] | None = _Unset,
@@ -950,6 +961,7 @@ def Field( # noqa: C901
950961
description: Human-readable description.
951962
examples: Example values for this field.
952963
exclude: Whether to exclude the field from the model serialization.
964+
exclude_if: Callable that determines whether to exclude a field during serialization based on its value.
953965
discriminator: Field name or Discriminator for discriminating the type in a tagged union.
954966
deprecated: A deprecation message, an instance of `warnings.deprecated` or the `typing_extensions.deprecated` backport,
955967
or a boolean. If `True`, a default deprecation message will be emitted when accessing the field.
@@ -1067,6 +1079,7 @@ def Field( # noqa: C901
10671079
description=description,
10681080
examples=examples,
10691081
exclude=exclude,
1082+
exclude_if=exclude_if,
10701083
discriminator=discriminator,
10711084
deprecated=deprecated,
10721085
json_schema_extra=json_schema_extra,

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ requires-python = '>=3.8'
4646
dependencies = [
4747
'typing-extensions>=4.12.2',
4848
'annotated-types>=0.6.0',
49-
'pydantic-core==2.27.1',
49+
'pydantic-core @ git+https://github.com/andresliszt/pydantic-core.git@support/exclude-if-at-field-level'
5050
]
5151
dynamic = ['version', 'readme']
5252

tests/test_dataclasses.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2857,6 +2857,25 @@ class Foo:
28572857
bar: str = Field(init=False, init_var=True)
28582858

28592859

2860+
def test_exclude() -> None:
2861+
@pydantic.dataclasses.dataclass
2862+
class Foo:
2863+
bar: str = Field(exclude=True)
2864+
foobaz: int = Field(exclude_if=lambda x: x > 1)
2865+
2866+
class Model(BaseModel):
2867+
foo: Foo
2868+
other_foo: Foo = Field(default=Foo(bar = 'bar', foobaz = 0), exclude = True)
2869+
2870+
assert Model(foo = Foo(bar = 'bar', foobaz=1)).model_dump() == {'foo': {'foobaz': 1}}
2871+
assert Model(foo = Foo(bar = 'bar', foobaz=2)).model_dump() == {'foo': {}}
2872+
assert Model(foo = Foo(bar = 'bar', foobaz=2)).model_dump(exclude={'foo'}) == {}
2873+
2874+
assert Model(foo = Foo(bar = 'bar', foobaz=1)).model_dump_json() == '{"foo":{"foobaz":1}}'
2875+
assert Model(foo = Foo(bar = 'bar', foobaz=2)).model_dump_json() == '{"foo":{}}'
2876+
assert Model(foo = Foo(bar = 'bar', foobaz=2)).model_dump_json(exclude={'foo'}) == '{}'
2877+
2878+
28602879
def test_annotations_valid_for_field_inheritance() -> None:
28612880
# testing https://github.com/pydantic/pydantic/issues/8670
28622881

tests/test_json_schema.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2571,6 +2571,24 @@ class Model(TypedDict):
25712571
}
25722572

25732573

2574+
def test_typeddict_exclude_if():
2575+
class Model(TypedDict):
2576+
a: Annotated[int, Field(exclude_if=lambda x: x > 1)]
2577+
b: Annotated[str, Field(exclude_if=lambda x: 'foo' in x)]
2578+
2579+
ta = TypeAdapter(Model)
2580+
2581+
assert ta.dump_python(Model(a=0, b='bar')) == {'a': 0, 'b': 'bar'}
2582+
assert ta.dump_python(Model(a=2, b='bar')) == {'b': 'bar'}
2583+
assert ta.dump_python(Model(a=0, b='foo')) == {'a': 0}
2584+
assert ta.dump_python(Model(a=2, b='foo')) == {}
2585+
2586+
assert ta.dump_json(Model(a=0, b='bar')) == b'{"a":0,"b":"bar"}'
2587+
assert ta.dump_json(Model(a=2, b='bar')) == b'{"b":"bar"}'
2588+
assert ta.dump_json(Model(a=0, b='foo')) == b'{"a":0}'
2589+
assert ta.dump_json(Model(a=2, b='foo')) == b'{}'
2590+
2591+
25742592
def test_typeddict_with_extra_forbid():
25752593
@pydantic.dataclasses.dataclass
25762594
class Model:

tests/test_main.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1139,6 +1139,22 @@ class Model(BaseModel):
11391139
}
11401140

11411141

1142+
def test_exclude_if():
1143+
class Model(BaseModel):
1144+
a: int = Field(exclude_if=lambda x: x > 1)
1145+
b: str = Field(exclude_if=lambda x: "foo" in x)
1146+
1147+
assert Model(a=0, b='bar').model_dump() == {'a': 0, 'b': 'bar'}
1148+
assert Model(a=2, b='bar').model_dump() == {'b': 'bar'}
1149+
assert Model(a=0, b='foo').model_dump() == {'a': 0}
1150+
assert Model(a=2, b='foo').model_dump() == {}
1151+
1152+
assert Model(a=0, b='bar').model_dump_json() == '{"a":0,"b":"bar"}'
1153+
assert Model(a=2, b='bar').model_dump_json() == '{"b":"bar"}'
1154+
assert Model(a=0, b='foo').model_dump_json() == '{"a":0}'
1155+
assert Model(a=2, b='foo').model_dump_json() == '{}'
1156+
1157+
11421158
def test_dir_fields():
11431159
class MyModel(BaseModel):
11441160
attribute_a: int

0 commit comments

Comments
 (0)