From eb2e412236aaf0138ee79a21c9397a9d364181c7 Mon Sep 17 00:00:00 2001 From: Andres Date: Fri, 8 Nov 2024 11:15:32 -0300 Subject: [PATCH 1/5] Support exclude_if callable at field level --- python/pydantic_core/core_schema.py | 12 ++++ src/serializers/fields.rs | 57 ++++++++++++------- src/serializers/type_serializers/dataclass.rs | 14 ++--- src/serializers/type_serializers/model.rs | 17 +++++- .../type_serializers/typed_dict.rs | 17 +++++- tests/serializers/test_dataclasses.py | 10 +++- tests/serializers/test_functions.py | 14 ++++- tests/serializers/test_model.py | 26 +++++++++ tests/serializers/test_typed_dict.py | 13 ++++- 9 files changed, 142 insertions(+), 38 deletions(-) diff --git a/python/pydantic_core/core_schema.py b/python/pydantic_core/core_schema.py index 0ab3dd947..6886b2f3a 100644 --- a/python/pydantic_core/core_schema.py +++ b/python/pydantic_core/core_schema.py @@ -2839,6 +2839,7 @@ class TypedDictField(TypedDict, total=False): serialization_alias: str serialization_exclude: bool # default: False metadata: dict[str, Any] + serialization_exclude_if: Callable[[Any], bool] # default None def typed_dict_field( @@ -2849,6 +2850,7 @@ def typed_dict_field( serialization_alias: str | None = None, serialization_exclude: bool | None = None, metadata: dict[str, Any] | None = None, + serialization_exclude_if: Callable[[Any], bool] | None = None, ) -> TypedDictField: """ Returns a schema that matches a typed dict field, e.g.: @@ -2865,6 +2867,7 @@ def typed_dict_field( validation_alias: The alias(es) to use to find the field in the validation data serialization_alias: The alias to use as a key when serializing serialization_exclude: Whether to exclude the field when serializing + serialization_exclude_if: A callable that determines whether to exclude the field when serializing based on its value. metadata: Any other information you want to include with the schema, not used by pydantic-core """ return _dict_not_none( @@ -2874,6 +2877,7 @@ def typed_dict_field( validation_alias=validation_alias, serialization_alias=serialization_alias, serialization_exclude=serialization_exclude, + serialization_exclude_if=serialization_exclude_if, metadata=metadata, ) @@ -2965,6 +2969,7 @@ class ModelField(TypedDict, total=False): validation_alias: Union[str, list[Union[str, int]], list[list[Union[str, int]]]] serialization_alias: str serialization_exclude: bool # default: False + serialization_exclude_if: Callable[[Any], bool] # default: None frozen: bool metadata: dict[str, Any] @@ -2975,6 +2980,7 @@ def model_field( validation_alias: str | list[str | int] | list[list[str | int]] | None = None, serialization_alias: str | None = None, serialization_exclude: bool | None = None, + exclude_if: Callable[[Any], bool] | None = None, frozen: bool | None = None, metadata: dict[str, Any] | None = None, ) -> ModelField: @@ -2992,6 +2998,7 @@ def model_field( validation_alias: The alias(es) to use to find the field in the validation data serialization_alias: The alias to use as a key when serializing serialization_exclude: Whether to exclude the field when serializing + exclude_if: Callable that determines whether to exclude a field during serialization based on its value. frozen: Whether the field is frozen metadata: Any other information you want to include with the schema, not used by pydantic-core """ @@ -3001,6 +3008,7 @@ def model_field( validation_alias=validation_alias, serialization_alias=serialization_alias, serialization_exclude=serialization_exclude, + exclude_if=exclude_if, frozen=frozen, metadata=metadata, ) @@ -3193,6 +3201,7 @@ class DataclassField(TypedDict, total=False): serialization_alias: str serialization_exclude: bool # default: False metadata: dict[str, Any] + serialization_exclude_if: Callable[[Any], bool] # default: None def dataclass_field( @@ -3206,6 +3215,7 @@ def dataclass_field( serialization_alias: str | None = None, serialization_exclude: bool | None = None, metadata: dict[str, Any] | None = None, + serialization_exclude_if: Callable[[Any], bool] | None = None, frozen: bool | None = None, ) -> DataclassField: """ @@ -3231,6 +3241,7 @@ def dataclass_field( validation_alias: The alias(es) to use to find the field in the validation data serialization_alias: The alias to use as a key when serializing serialization_exclude: Whether to exclude the field when serializing + serialization_exclude_if: A callable that determines whether to exclude the field when serializing based on its value. metadata: Any other information you want to include with the schema, not used by pydantic-core frozen: Whether the field is frozen """ @@ -3244,6 +3255,7 @@ def dataclass_field( validation_alias=validation_alias, serialization_alias=serialization_alias, serialization_exclude=serialization_exclude, + serialization_exclude_if=serialization_exclude_if, metadata=metadata, frozen=frozen, ) diff --git a/src/serializers/fields.rs b/src/serializers/fields.rs index b89468d4e..cf158574a 100644 --- a/src/serializers/fields.rs +++ b/src/serializers/fields.rs @@ -29,6 +29,7 @@ pub(super) struct SerField { pub serializer: Option, pub required: bool, pub serialize_by_alias: Option, + pub exclude_if: Option>, } impl_py_gc_traverse!(SerField { serializer }); @@ -41,6 +42,7 @@ impl SerField { serializer: Option, required: bool, serialize_by_alias: Option, + exclude_if: Option>, ) -> Self { let alias_py = alias.as_ref().map(|alias| PyString::new(py, alias.as_str()).into()); Self { @@ -50,6 +52,7 @@ impl SerField { serializer, required, serialize_by_alias, + exclude_if, } } @@ -72,6 +75,18 @@ impl SerField { } } +fn exclude_if(exclude_if_callable: &Option>, value: &Bound<'_, PyAny>) -> PyResult { + if let Some(exclude_if_callable) = exclude_if_callable { + let py = value.py(); + let result = exclude_if_callable.call1(py, (value,))?; + let exclude = result.extract::(py)?; + if exclude { + return Ok(true); + } + } + Ok(false) +} + fn exclude_default(value: &Bound<'_, PyAny>, extra: &Extra, serializer: &CombinedSerializer) -> PyResult { if extra.exclude_defaults { if let Some(default) = serializer.get_default(value.py())? { @@ -176,16 +191,16 @@ impl GeneralFieldsSerializer { if let Some((next_include, next_exclude)) = self.filter.key_filter(&key, include, exclude)? { if let Some(field) = op_field { if let Some(ref serializer) = field.serializer { - if !exclude_default(&value, &field_extra, serializer)? { - let value = serializer.to_python( - &value, - next_include.as_ref(), - next_exclude.as_ref(), - &field_extra, - )?; - let output_key = field.get_key_py(output_dict.py(), &field_extra); - output_dict.set_item(output_key, value)?; + if exclude_default(&value, &field_extra, serializer)? { + continue; } + if exclude_if(&field.exclude_if, &value)? { + continue; + } + let value = + serializer.to_python(&value, next_include.as_ref(), next_exclude.as_ref(), &field_extra)?; + let output_key = field.get_key_py(output_dict.py(), &field_extra); + output_dict.set_item(output_key, value)?; } if field.required { @@ -257,17 +272,21 @@ impl GeneralFieldsSerializer { if let Some((next_include, next_exclude)) = filter { if let Some(field) = self.fields.get(key_str) { if let Some(ref serializer) = field.serializer { - if !exclude_default(&value, &field_extra, serializer).map_err(py_err_se_err)? { - let s = PydanticSerializer::new( - &value, - serializer, - next_include.as_ref(), - next_exclude.as_ref(), - &field_extra, - ); - let output_key = field.get_key_json(key_str, &field_extra); - map.serialize_entry(&output_key, &s)?; + if exclude_default(&value, &field_extra, serializer).map_err(py_err_se_err)? { + continue; + } + if exclude_if(&field.exclude_if, &value).map_err(py_err_se_err)? { + continue; } + let s = PydanticSerializer::new( + &value, + serializer, + next_include.as_ref(), + next_exclude.as_ref(), + &field_extra, + ); + let output_key = field.get_key_json(key_str, &field_extra); + map.serialize_entry(&output_key, &s)?; } } else if self.mode == FieldsMode::TypedDictAllow { let output_key = infer_json_key(&key, &field_extra).map_err(py_err_se_err)?; diff --git a/src/serializers/type_serializers/dataclass.rs b/src/serializers/type_serializers/dataclass.rs index a080a9b4f..e0538f2b5 100644 --- a/src/serializers/type_serializers/dataclass.rs +++ b/src/serializers/type_serializers/dataclass.rs @@ -44,27 +44,27 @@ impl BuildSerializer for DataclassArgsBuilder { let name: String = field_info.get_as_req(intern!(py, "name"))?; let key_py: Py = PyString::new(py, &name).into(); - if !field_info.get_as(intern!(py, "init_only"))?.unwrap_or(false) { if field_info.get_as(intern!(py, "serialization_exclude"))? == Some(true) { - fields.insert(name, SerField::new(py, key_py, None, None, true, serialize_by_alias)); + fields.insert(name, SerField::new(py, key_py, None, None, true, None)); } else { let schema = field_info.get_as_req(intern!(py, "schema"))?; let serializer = CombinedSerializer::build(&schema, config, definitions) .map_err(|e| py_schema_error_type!("Field `{}`:\n {}", index, e))?; let alias = field_info.get_as(intern!(py, "serialization_alias"))?; + let exclude_if: Option> = field_info.get_as(intern!(py, "exclude_if"))?; fields.insert( name, - SerField::new(py, key_py, alias, Some(serializer), true, serialize_by_alias), + SerField::new(py, key_py, alias, Some(serializer), true, exclude_if), ); } - } - } + }; - let computed_fields = ComputedFields::new(schema, config, definitions)?; + let computed_fields = ComputedFields::new(schema, config, definitions)?; - Ok(GeneralFieldsSerializer::new(fields, fields_mode, None, computed_fields).into()) + Ok(GeneralFieldsSerializer::new(fields, fields_mode, None, computed_fields).into()) + } } } diff --git a/src/serializers/type_serializers/model.rs b/src/serializers/type_serializers/model.rs index 4bae243fc..a2f63f26e 100644 --- a/src/serializers/type_serializers/model.rs +++ b/src/serializers/type_serializers/model.rs @@ -57,17 +57,28 @@ impl BuildSerializer for ModelFieldsBuilder { let key_py: Py = key_py.into(); if field_info.get_as(intern!(py, "serialization_exclude"))? == Some(true) { - fields.insert(key, SerField::new(py, key_py, None, None, true, serialize_by_alias)); + fields.insert( + key, + SerField::new(py, key_py, None, None, true, serialize_by_alias, None), + ); } else { let alias: Option = field_info.get_as(intern!(py, "serialization_alias"))?; - + let exclude_if: Option> = field_info.get_as(intern!(py, "exclude_if"))?; let schema = field_info.get_as_req(intern!(py, "schema"))?; let serializer = CombinedSerializer::build(&schema, config, definitions) .map_err(|e| py_schema_error_type!("Field `{}`:\n {}", key, e))?; fields.insert( key, - SerField::new(py, key_py, alias, Some(serializer), true, serialize_by_alias), + SerField::new( + py, + key_py, + alias, + Some(serializer), + true, + serialize_by_alias, + exclude_if, + ), ); } } diff --git a/src/serializers/type_serializers/typed_dict.rs b/src/serializers/type_serializers/typed_dict.rs index d93a88550..21572a2f1 100644 --- a/src/serializers/type_serializers/typed_dict.rs +++ b/src/serializers/type_serializers/typed_dict.rs @@ -54,16 +54,27 @@ impl BuildSerializer for TypedDictBuilder { let required = field_info.get_as(intern!(py, "required"))?.unwrap_or(total); if field_info.get_as(intern!(py, "serialization_exclude"))? == Some(true) { - fields.insert(key, SerField::new(py, key_py, None, None, required, serialize_by_alias)); + fields.insert( + key, + SerField::new(py, key_py, None, None, required, serialize_by_alias, None), + ); } else { let alias: Option = field_info.get_as(intern!(py, "serialization_alias"))?; - + let exclude_if: Option> = field_info.get_as(intern!(py, "exclude_if"))?; let schema = field_info.get_as_req(intern!(py, "schema"))?; let serializer = CombinedSerializer::build(&schema, config, definitions) .map_err(|e| py_schema_error_type!("Field `{}`:\n {}", key, e))?; fields.insert( key, - SerField::new(py, key_py, alias, Some(serializer), required, serialize_by_alias), + SerField::new( + py, + key_py, + alias, + Some(serializer), + required, + serialize_by_alias, + exclude_if, + ), ); } } diff --git a/tests/serializers/test_dataclasses.py b/tests/serializers/test_dataclasses.py index e14ea268e..be5036fd5 100644 --- a/tests/serializers/test_dataclasses.py +++ b/tests/serializers/test_dataclasses.py @@ -54,7 +54,7 @@ def test_serialization_exclude(): core_schema.dataclass_args_schema( 'Foo', [ - core_schema.dataclass_field(name='a', schema=core_schema.str_schema()), + core_schema.dataclass_field(name='a', schema=core_schema.str_schema(), exclude_if=lambda x: x == 'bye'), core_schema.dataclass_field(name='b', schema=core_schema.bytes_schema(), serialization_exclude=True), ], ), @@ -63,12 +63,18 @@ def test_serialization_exclude(): s = SchemaSerializer(schema) assert s.to_python(Foo(a='hello', b=b'more')) == {'a': 'hello'} assert s.to_python(Foo(a='hello', b=b'more'), mode='json') == {'a': 'hello'} + # a = 'bye' excludes it + assert s.to_python(Foo(a='bye', b=b'more'), mode='json') == {} j = s.to_json(Foo(a='hello', b=b'more')) - if on_pypy: assert json.loads(j) == {'a': 'hello'} else: assert j == b'{"a":"hello"}' + j = s.to_json(Foo(a='bye', b=b'more')) + if on_pypy: + assert json.loads(j) == {} + else: + assert j == b'{}' def test_serialization_alias(): diff --git a/tests/serializers/test_functions.py b/tests/serializers/test_functions.py index 926749721..696cdab30 100644 --- a/tests/serializers/test_functions.py +++ b/tests/serializers/test_functions.py @@ -517,7 +517,9 @@ def __init__(self, **kwargs): MyModel, core_schema.typed_dict_schema( { - 'a': core_schema.typed_dict_field(core_schema.any_schema()), + 'a': core_schema.typed_dict_field( + core_schema.any_schema(), exclude_if=lambda x: isinstance(x, int) and x >= 2 + ), 'b': core_schema.typed_dict_field(core_schema.any_schema()), 'c': core_schema.typed_dict_field(core_schema.any_schema(), serialization_exclude=True), } @@ -541,6 +543,14 @@ def __init__(self, **kwargs): assert s.to_json(m, exclude={'b'}) == b'{"a":1}' assert calls == 6 + m = MyModel(a=2, b=b'foobar', c='excluded') + assert s.to_python(m) == {'b': b'foobar'} + assert calls == 7 + assert s.to_python(m, mode='json') == {'b': 'foobar'} + assert calls == 8 + assert s.to_json(m) == b'{"b":"foobar"}' + assert calls == 9 + def test_function_plain_model(): calls = 0 @@ -559,7 +569,7 @@ def __init__(self, **kwargs): MyModel, core_schema.typed_dict_schema( { - 'a': core_schema.typed_dict_field(core_schema.any_schema()), + 'a': core_schema.typed_dict_field(core_schema.any_schema(), exclude_if=lambda x: x == 100), 'b': core_schema.typed_dict_field(core_schema.any_schema()), 'c': core_schema.typed_dict_field(core_schema.any_schema(), serialization_exclude=True), } diff --git a/tests/serializers/test_model.py b/tests/serializers/test_model.py index 65871e050..05aaed632 100644 --- a/tests/serializers/test_model.py +++ b/tests/serializers/test_model.py @@ -203,6 +203,32 @@ def test_include_exclude_args(params): assert json.loads(s.to_json(value, include=include, exclude=exclude)) == expected +def test_exclude_if(): + s = SchemaSerializer( + core_schema.model_schema( + BasicModel, + core_schema.model_fields_schema( + { + 'a': core_schema.model_field(core_schema.int_schema(), exclude_if=lambda x: x > 1), + 'b': core_schema.model_field(core_schema.str_schema(), exclude_if=lambda x: 'foo' in x), + 'c': core_schema.model_field( + core_schema.str_schema(), serialization_exclude=True, exclude_if=lambda x: 'foo' in x + ), + } + ), + ) + ) + assert s.to_python(BasicModel(a=0, b='bar', c='bar')) == {'a': 0, 'b': 'bar'} + assert s.to_python(BasicModel(a=2, b='bar', c='bar')) == {'b': 'bar'} + assert s.to_python(BasicModel(a=0, b='foo', c='bar')) == {'a': 0} + assert s.to_python(BasicModel(a=2, b='foo', c='bar')) == {} + + assert s.to_json(BasicModel(a=0, b='bar', c='bar')) == b'{"a":0,"b":"bar"}' + assert s.to_json(BasicModel(a=2, b='bar', c='bar')) == b'{"b":"bar"}' + assert s.to_json(BasicModel(a=0, b='foo', c='bar')) == b'{"a":0}' + assert s.to_json(BasicModel(a=2, b='foo', c='bar')) == b'{}' + + def test_alias(): s = SchemaSerializer( core_schema.model_schema( diff --git a/tests/serializers/test_typed_dict.py b/tests/serializers/test_typed_dict.py index bd2016d18..eb006a9b2 100644 --- a/tests/serializers/test_typed_dict.py +++ b/tests/serializers/test_typed_dict.py @@ -92,8 +92,12 @@ def test_include_exclude_schema(): { '0': core_schema.typed_dict_field(core_schema.int_schema(), serialization_exclude=True), '1': core_schema.typed_dict_field(core_schema.int_schema()), - '2': core_schema.typed_dict_field(core_schema.int_schema(), serialization_exclude=True), - '3': core_schema.typed_dict_field(core_schema.int_schema(), serialization_exclude=False), + '2': core_schema.typed_dict_field( + core_schema.int_schema(), serialization_exclude=True, exclude_if=lambda x: x < 0 + ), + '3': core_schema.typed_dict_field( + core_schema.int_schema(), serialization_exclude=False, exclude_if=lambda x: x < 0 + ), } ) ) @@ -102,6 +106,11 @@ def test_include_exclude_schema(): assert s.to_python(value, mode='json') == {'1': 1, '3': 3} assert json.loads(s.to_json(value)) == {'1': 1, '3': 3} + value = {'0': 0, '1': 1, '2': 2, '3': -3} + assert s.to_python(value) == {'1': 1} + assert s.to_python(value, mode='json') == {'1': 1} + assert json.loads(s.to_json(value)) == {'1': 1} + def test_alias(): s = SchemaSerializer( From 6d746d8c6f35504793ae2d8498ce9b5872be3e38 Mon Sep 17 00:00:00 2001 From: Andres Date: Wed, 23 Jul 2025 15:45:08 -0400 Subject: [PATCH 2/5] rename exclude_if by serialization_exclude_if --- python/pydantic_core/core_schema.py | 6 ++--- src/serializers/fields.rs | 12 ++++----- src/serializers/type_serializers/dataclass.rs | 26 +++++++++++++------ src/serializers/type_serializers/model.rs | 4 +-- .../type_serializers/typed_dict.rs | 4 +-- tests/serializers/test_dataclasses.py | 2 +- tests/serializers/test_functions.py | 4 +-- tests/serializers/test_model.py | 6 ++--- tests/serializers/test_typed_dict.py | 4 +-- 9 files changed, 39 insertions(+), 29 deletions(-) diff --git a/python/pydantic_core/core_schema.py b/python/pydantic_core/core_schema.py index 6886b2f3a..0e1661a5e 100644 --- a/python/pydantic_core/core_schema.py +++ b/python/pydantic_core/core_schema.py @@ -2980,7 +2980,7 @@ def model_field( validation_alias: str | list[str | int] | list[list[str | int]] | None = None, serialization_alias: str | None = None, serialization_exclude: bool | None = None, - exclude_if: Callable[[Any], bool] | None = None, + serialization_exclude_if: Callable[[Any], bool] | None = None, frozen: bool | None = None, metadata: dict[str, Any] | None = None, ) -> ModelField: @@ -2998,7 +2998,7 @@ def model_field( validation_alias: The alias(es) to use to find the field in the validation data serialization_alias: The alias to use as a key when serializing serialization_exclude: Whether to exclude the field when serializing - exclude_if: Callable that determines whether to exclude a field during serialization based on its value. + serialization_exclude_if: A Callable that determines whether to exclude a field during serialization based on its value. frozen: Whether the field is frozen metadata: Any other information you want to include with the schema, not used by pydantic-core """ @@ -3008,7 +3008,7 @@ def model_field( validation_alias=validation_alias, serialization_alias=serialization_alias, serialization_exclude=serialization_exclude, - exclude_if=exclude_if, + serialization_exclude_if=serialization_exclude_if, frozen=frozen, metadata=metadata, ) diff --git a/src/serializers/fields.rs b/src/serializers/fields.rs index cf158574a..8d6b4b83e 100644 --- a/src/serializers/fields.rs +++ b/src/serializers/fields.rs @@ -29,7 +29,7 @@ pub(super) struct SerField { pub serializer: Option, pub required: bool, pub serialize_by_alias: Option, - pub exclude_if: Option>, + pub serialization_exclude_if: Option>, } impl_py_gc_traverse!(SerField { serializer }); @@ -42,7 +42,7 @@ impl SerField { serializer: Option, required: bool, serialize_by_alias: Option, - exclude_if: Option>, + serialization_exclude_if: Option>, ) -> Self { let alias_py = alias.as_ref().map(|alias| PyString::new(py, alias.as_str()).into()); Self { @@ -52,7 +52,7 @@ impl SerField { serializer, required, serialize_by_alias, - exclude_if, + serialization_exclude_if, } } @@ -75,7 +75,7 @@ impl SerField { } } -fn exclude_if(exclude_if_callable: &Option>, value: &Bound<'_, PyAny>) -> PyResult { +fn serialization_exclude_if(exclude_if_callable: &Option>, value: &Bound<'_, PyAny>) -> PyResult { if let Some(exclude_if_callable) = exclude_if_callable { let py = value.py(); let result = exclude_if_callable.call1(py, (value,))?; @@ -194,7 +194,7 @@ impl GeneralFieldsSerializer { if exclude_default(&value, &field_extra, serializer)? { continue; } - if exclude_if(&field.exclude_if, &value)? { + if serialization_exclude_if(&field.serialization_exclude_if, &value)? { continue; } let value = @@ -275,7 +275,7 @@ impl GeneralFieldsSerializer { if exclude_default(&value, &field_extra, serializer).map_err(py_err_se_err)? { continue; } - if exclude_if(&field.exclude_if, &value).map_err(py_err_se_err)? { + if serialization_exclude_if(&field.serialization_exclude_if, &value).map_err(py_err_se_err)? { continue; } let s = PydanticSerializer::new( diff --git a/src/serializers/type_serializers/dataclass.rs b/src/serializers/type_serializers/dataclass.rs index e0538f2b5..099ffcb82 100644 --- a/src/serializers/type_serializers/dataclass.rs +++ b/src/serializers/type_serializers/dataclass.rs @@ -46,25 +46,35 @@ impl BuildSerializer for DataclassArgsBuilder { let key_py: Py = PyString::new(py, &name).into(); if !field_info.get_as(intern!(py, "init_only"))?.unwrap_or(false) { if field_info.get_as(intern!(py, "serialization_exclude"))? == Some(true) { - fields.insert(name, SerField::new(py, key_py, None, None, true, None)); + fields.insert( + name, + SerField::new(py, key_py, None, None, true, serialize_by_alias, None), + ); } else { let schema = field_info.get_as_req(intern!(py, "schema"))?; let serializer = CombinedSerializer::build(&schema, config, definitions) .map_err(|e| py_schema_error_type!("Field `{}`:\n {}", index, e))?; let alias = field_info.get_as(intern!(py, "serialization_alias"))?; - let exclude_if: Option> = field_info.get_as(intern!(py, "exclude_if"))?; + let serialization_exclude_if: Option> = field_info.get_as(intern!(py, "serialization_exclude_if"))?; fields.insert( name, - SerField::new(py, key_py, alias, Some(serializer), true, exclude_if), + SerField::new( + py, + key_py, + alias, + Some(serializer), + true, + serialize_by_alias, + serialization_exclude_if, + ), ); } - }; - - let computed_fields = ComputedFields::new(schema, config, definitions)?; - - Ok(GeneralFieldsSerializer::new(fields, fields_mode, None, computed_fields).into()) + } } + let computed_fields = ComputedFields::new(schema, config, definitions)?; + + Ok(GeneralFieldsSerializer::new(fields, fields_mode, None, computed_fields).into()) } } diff --git a/src/serializers/type_serializers/model.rs b/src/serializers/type_serializers/model.rs index a2f63f26e..598dd732b 100644 --- a/src/serializers/type_serializers/model.rs +++ b/src/serializers/type_serializers/model.rs @@ -63,7 +63,7 @@ impl BuildSerializer for ModelFieldsBuilder { ); } else { let alias: Option = field_info.get_as(intern!(py, "serialization_alias"))?; - let exclude_if: Option> = field_info.get_as(intern!(py, "exclude_if"))?; + let serialization_exclude_if: Option> = field_info.get_as(intern!(py, "serialization_exclude_if"))?; let schema = field_info.get_as_req(intern!(py, "schema"))?; let serializer = CombinedSerializer::build(&schema, config, definitions) .map_err(|e| py_schema_error_type!("Field `{}`:\n {}", key, e))?; @@ -77,7 +77,7 @@ impl BuildSerializer for ModelFieldsBuilder { Some(serializer), true, serialize_by_alias, - exclude_if, + serialization_exclude_if, ), ); } diff --git a/src/serializers/type_serializers/typed_dict.rs b/src/serializers/type_serializers/typed_dict.rs index 21572a2f1..581d92e1e 100644 --- a/src/serializers/type_serializers/typed_dict.rs +++ b/src/serializers/type_serializers/typed_dict.rs @@ -60,7 +60,7 @@ impl BuildSerializer for TypedDictBuilder { ); } else { let alias: Option = field_info.get_as(intern!(py, "serialization_alias"))?; - let exclude_if: Option> = field_info.get_as(intern!(py, "exclude_if"))?; + let serialization_exclude_if: Option> = field_info.get_as(intern!(py, "serialization_exclude_if"))?; let schema = field_info.get_as_req(intern!(py, "schema"))?; let serializer = CombinedSerializer::build(&schema, config, definitions) .map_err(|e| py_schema_error_type!("Field `{}`:\n {}", key, e))?; @@ -73,7 +73,7 @@ impl BuildSerializer for TypedDictBuilder { Some(serializer), required, serialize_by_alias, - exclude_if, + serialization_exclude_if, ), ); } diff --git a/tests/serializers/test_dataclasses.py b/tests/serializers/test_dataclasses.py index be5036fd5..ed64d52c7 100644 --- a/tests/serializers/test_dataclasses.py +++ b/tests/serializers/test_dataclasses.py @@ -54,7 +54,7 @@ def test_serialization_exclude(): core_schema.dataclass_args_schema( 'Foo', [ - core_schema.dataclass_field(name='a', schema=core_schema.str_schema(), exclude_if=lambda x: x == 'bye'), + core_schema.dataclass_field(name='a', schema=core_schema.str_schema(), serialization_exclude_if=lambda x: x == 'bye'), core_schema.dataclass_field(name='b', schema=core_schema.bytes_schema(), serialization_exclude=True), ], ), diff --git a/tests/serializers/test_functions.py b/tests/serializers/test_functions.py index 696cdab30..7a07c2380 100644 --- a/tests/serializers/test_functions.py +++ b/tests/serializers/test_functions.py @@ -518,7 +518,7 @@ def __init__(self, **kwargs): core_schema.typed_dict_schema( { 'a': core_schema.typed_dict_field( - core_schema.any_schema(), exclude_if=lambda x: isinstance(x, int) and x >= 2 + core_schema.any_schema(), serialization_exclude_if=lambda x: isinstance(x, int) and x >= 2 ), 'b': core_schema.typed_dict_field(core_schema.any_schema()), 'c': core_schema.typed_dict_field(core_schema.any_schema(), serialization_exclude=True), @@ -569,7 +569,7 @@ def __init__(self, **kwargs): MyModel, core_schema.typed_dict_schema( { - 'a': core_schema.typed_dict_field(core_schema.any_schema(), exclude_if=lambda x: x == 100), + 'a': core_schema.typed_dict_field(core_schema.any_schema(), serialization_exclude_if=lambda x: x == 100), 'b': core_schema.typed_dict_field(core_schema.any_schema()), 'c': core_schema.typed_dict_field(core_schema.any_schema(), serialization_exclude=True), } diff --git a/tests/serializers/test_model.py b/tests/serializers/test_model.py index 05aaed632..0607ba713 100644 --- a/tests/serializers/test_model.py +++ b/tests/serializers/test_model.py @@ -209,10 +209,10 @@ def test_exclude_if(): BasicModel, core_schema.model_fields_schema( { - 'a': core_schema.model_field(core_schema.int_schema(), exclude_if=lambda x: x > 1), - 'b': core_schema.model_field(core_schema.str_schema(), exclude_if=lambda x: 'foo' in x), + 'a': core_schema.model_field(core_schema.int_schema(), serialization_exclude_if=lambda x: x > 1), + 'b': core_schema.model_field(core_schema.str_schema(), serialization_exclude_if=lambda x: 'foo' in x), 'c': core_schema.model_field( - core_schema.str_schema(), serialization_exclude=True, exclude_if=lambda x: 'foo' in x + core_schema.str_schema(), serialization_exclude=True, serialization_exclude_if=lambda x: 'foo' in x ), } ), diff --git a/tests/serializers/test_typed_dict.py b/tests/serializers/test_typed_dict.py index eb006a9b2..6626a3981 100644 --- a/tests/serializers/test_typed_dict.py +++ b/tests/serializers/test_typed_dict.py @@ -93,10 +93,10 @@ def test_include_exclude_schema(): '0': core_schema.typed_dict_field(core_schema.int_schema(), serialization_exclude=True), '1': core_schema.typed_dict_field(core_schema.int_schema()), '2': core_schema.typed_dict_field( - core_schema.int_schema(), serialization_exclude=True, exclude_if=lambda x: x < 0 + core_schema.int_schema(), serialization_exclude=True, serialization_exclude_if=lambda x: x < 0 ), '3': core_schema.typed_dict_field( - core_schema.int_schema(), serialization_exclude=False, exclude_if=lambda x: x < 0 + core_schema.int_schema(), serialization_exclude=False, serialization_exclude_if=lambda x: x < 0 ), } ) From 9b655e5cb8383dfa4e258330c17a3107cd4d6d33 Mon Sep 17 00:00:00 2001 From: Andres Date: Wed, 23 Jul 2025 16:02:36 -0400 Subject: [PATCH 3/5] fmt --- src/serializers/type_serializers/dataclass.rs | 3 ++- src/serializers/type_serializers/model.rs | 3 ++- src/serializers/type_serializers/typed_dict.rs | 3 ++- tests/serializers/test_dataclasses.py | 4 +++- tests/serializers/test_functions.py | 4 +++- tests/serializers/test_model.py | 8 ++++++-- 6 files changed, 18 insertions(+), 7 deletions(-) diff --git a/src/serializers/type_serializers/dataclass.rs b/src/serializers/type_serializers/dataclass.rs index 099ffcb82..c36bb805e 100644 --- a/src/serializers/type_serializers/dataclass.rs +++ b/src/serializers/type_serializers/dataclass.rs @@ -56,7 +56,8 @@ impl BuildSerializer for DataclassArgsBuilder { .map_err(|e| py_schema_error_type!("Field `{}`:\n {}", index, e))?; let alias = field_info.get_as(intern!(py, "serialization_alias"))?; - let serialization_exclude_if: Option> = field_info.get_as(intern!(py, "serialization_exclude_if"))?; + let serialization_exclude_if: Option> = + field_info.get_as(intern!(py, "serialization_exclude_if"))?; fields.insert( name, SerField::new( diff --git a/src/serializers/type_serializers/model.rs b/src/serializers/type_serializers/model.rs index 598dd732b..328cf265b 100644 --- a/src/serializers/type_serializers/model.rs +++ b/src/serializers/type_serializers/model.rs @@ -63,7 +63,8 @@ impl BuildSerializer for ModelFieldsBuilder { ); } else { let alias: Option = field_info.get_as(intern!(py, "serialization_alias"))?; - let serialization_exclude_if: Option> = field_info.get_as(intern!(py, "serialization_exclude_if"))?; + let serialization_exclude_if: Option> = + field_info.get_as(intern!(py, "serialization_exclude_if"))?; let schema = field_info.get_as_req(intern!(py, "schema"))?; let serializer = CombinedSerializer::build(&schema, config, definitions) .map_err(|e| py_schema_error_type!("Field `{}`:\n {}", key, e))?; diff --git a/src/serializers/type_serializers/typed_dict.rs b/src/serializers/type_serializers/typed_dict.rs index 581d92e1e..406b95779 100644 --- a/src/serializers/type_serializers/typed_dict.rs +++ b/src/serializers/type_serializers/typed_dict.rs @@ -60,7 +60,8 @@ impl BuildSerializer for TypedDictBuilder { ); } else { let alias: Option = field_info.get_as(intern!(py, "serialization_alias"))?; - let serialization_exclude_if: Option> = field_info.get_as(intern!(py, "serialization_exclude_if"))?; + let serialization_exclude_if: Option> = + field_info.get_as(intern!(py, "serialization_exclude_if"))?; let schema = field_info.get_as_req(intern!(py, "schema"))?; let serializer = CombinedSerializer::build(&schema, config, definitions) .map_err(|e| py_schema_error_type!("Field `{}`:\n {}", key, e))?; diff --git a/tests/serializers/test_dataclasses.py b/tests/serializers/test_dataclasses.py index ed64d52c7..4d8ef9ff6 100644 --- a/tests/serializers/test_dataclasses.py +++ b/tests/serializers/test_dataclasses.py @@ -54,7 +54,9 @@ def test_serialization_exclude(): core_schema.dataclass_args_schema( 'Foo', [ - core_schema.dataclass_field(name='a', schema=core_schema.str_schema(), serialization_exclude_if=lambda x: x == 'bye'), + core_schema.dataclass_field( + name='a', schema=core_schema.str_schema(), serialization_exclude_if=lambda x: x == 'bye' + ), core_schema.dataclass_field(name='b', schema=core_schema.bytes_schema(), serialization_exclude=True), ], ), diff --git a/tests/serializers/test_functions.py b/tests/serializers/test_functions.py index 7a07c2380..c31c4e6b6 100644 --- a/tests/serializers/test_functions.py +++ b/tests/serializers/test_functions.py @@ -569,7 +569,9 @@ def __init__(self, **kwargs): MyModel, core_schema.typed_dict_schema( { - 'a': core_schema.typed_dict_field(core_schema.any_schema(), serialization_exclude_if=lambda x: x == 100), + 'a': core_schema.typed_dict_field( + core_schema.any_schema(), serialization_exclude_if=lambda x: x == 100 + ), 'b': core_schema.typed_dict_field(core_schema.any_schema()), 'c': core_schema.typed_dict_field(core_schema.any_schema(), serialization_exclude=True), } diff --git a/tests/serializers/test_model.py b/tests/serializers/test_model.py index 0607ba713..2a7b759a4 100644 --- a/tests/serializers/test_model.py +++ b/tests/serializers/test_model.py @@ -210,9 +210,13 @@ def test_exclude_if(): core_schema.model_fields_schema( { 'a': core_schema.model_field(core_schema.int_schema(), serialization_exclude_if=lambda x: x > 1), - 'b': core_schema.model_field(core_schema.str_schema(), serialization_exclude_if=lambda x: 'foo' in x), + 'b': core_schema.model_field( + core_schema.str_schema(), serialization_exclude_if=lambda x: 'foo' in x + ), 'c': core_schema.model_field( - core_schema.str_schema(), serialization_exclude=True, serialization_exclude_if=lambda x: 'foo' in x + core_schema.str_schema(), + serialization_exclude=True, + serialization_exclude_if=lambda x: 'foo' in x, ), } ), From 93682aca82301bfc789564395a4ddec0b74d708b Mon Sep 17 00:00:00 2001 From: Victorien <65306057+Viicos@users.noreply.github.com> Date: Wed, 23 Jul 2025 22:32:45 +0200 Subject: [PATCH 4/5] Apply suggestions from code review --- src/serializers/fields.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/serializers/fields.rs b/src/serializers/fields.rs index 8d6b4b83e..baa1e053a 100644 --- a/src/serializers/fields.rs +++ b/src/serializers/fields.rs @@ -75,7 +75,7 @@ impl SerField { } } -fn serialization_exclude_if(exclude_if_callable: &Option>, value: &Bound<'_, PyAny>) -> PyResult { +fn serialization_exclude_if(exclude_if_callable: Option<&Py>, value: &Bound<'_, PyAny>) -> PyResult { if let Some(exclude_if_callable) = exclude_if_callable { let py = value.py(); let result = exclude_if_callable.call1(py, (value,))?; @@ -194,7 +194,7 @@ impl GeneralFieldsSerializer { if exclude_default(&value, &field_extra, serializer)? { continue; } - if serialization_exclude_if(&field.serialization_exclude_if, &value)? { + if serialization_exclude_if(field.serialization_exclude_if.as_ref(), &value)? { continue; } let value = @@ -275,7 +275,8 @@ impl GeneralFieldsSerializer { if exclude_default(&value, &field_extra, serializer).map_err(py_err_se_err)? { continue; } - if serialization_exclude_if(&field.serialization_exclude_if, &value).map_err(py_err_se_err)? { + if serialization_exclude_if(field.serialization_exclude_if.as_ref(), &value) + .map_err(py_err_se_err)? { continue; } let s = PydanticSerializer::new( From fc7535558e3a5056b5ddfb65d4a907c59c5e620d Mon Sep 17 00:00:00 2001 From: Victorien <65306057+Viicos@users.noreply.github.com> Date: Wed, 23 Jul 2025 22:34:18 +0200 Subject: [PATCH 5/5] Update src/serializers/fields.rs --- src/serializers/fields.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/serializers/fields.rs b/src/serializers/fields.rs index baa1e053a..f70dde05f 100644 --- a/src/serializers/fields.rs +++ b/src/serializers/fields.rs @@ -276,7 +276,8 @@ impl GeneralFieldsSerializer { continue; } if serialization_exclude_if(field.serialization_exclude_if.as_ref(), &value) - .map_err(py_err_se_err)? { + .map_err(py_err_se_err)? + { continue; } let s = PydanticSerializer::new(