Skip to content

Commit b3d6bf8

Browse files
authored
v0.30.0-skip-if (#147)
* Add "skip if" support for serializing dataclass fields * Looks good, just need to add tests and docs * Add test cases * Update docs * Update docs on Meta * Add `JSONPyWizard` and some condition functions * Add `IS_TRUTHY` and `IS_FALSY` conditions * Add `JSONPyWizard` to skip key transformation during serialization (dump) * update docs * update docs * re-order sections in docs * update docs * that should be v1 not v2 * Update HISTORY.rst * Update docs
1 parent 2fcf4cb commit b3d6bf8

File tree

19 files changed

+1374
-276
lines changed

19 files changed

+1374
-276
lines changed

HISTORY.rst

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,26 @@
22
History
33
=======
44

5+
0.30.0 (2024-11-25)
6+
-------------------
7+
8+
**Features and Improvements**
9+
10+
- **Conditional Field Skipping**: Omit fields during JSON serialization based on user-defined conditions.
11+
- Introduced new :class:`Meta` settings:
12+
- :attr:`skip_if` — Skips all fields matching a condition.
13+
- :attr:`skip_defaults_if` — Skips fields with default values matching a condition.
14+
- Added per-field controls using :func:`SkipIf()` annotations.
15+
- Introduced the :func:`skip_if_field` wrapper for maximum flexibility.
16+
17+
- **New Helper Class**: :class:`JSONPyWizard`
18+
- A ``JSONWizard`` helper to disable *camelCase* transformation and keep keys as-is.
19+
20+
- **Typing Improvements**: Added more ``*.pyi`` files for enhanced type checking and IDE support.
21+
22+
- **Documentation Updates**:
23+
- Added details about upcoming changes in the next major release, ``v1.0``.
24+
525
0.29.3 (2024-11-24)
626
-------------------
727

README.rst

Lines changed: 159 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,12 @@ Full documentation is available at `Read The Docs`_. (`Installation`_)
2727

2828

2929

30-
Dataclass Wizard offers simple, elegant, *wizarding* tools for
30+
**Dataclass Wizard** offers simple, elegant, *wizarding* 🪄 tools for
3131
interacting with Python's ``dataclasses``.
3232

33-
It excels at lightning-fast de/serialization, converting dataclass
34-
instances to/from JSON effortlessly -- perfect for *nested dataclass*
35-
models!
33+
It excels at ⚡️ lightning-fast de/serialization, effortlessly
34+
converting dataclass instances to/from JSON -- perfect for
35+
*nested dataclass* models!
3636

3737
-------------------
3838

@@ -106,8 +106,9 @@ Here are the key features that ``dataclass-wizard`` offers:
106106
Wizard Mixins
107107
-------------
108108

109-
In addition to the ``JSONWizard``, here are a few extra Mixin_ classes that might prove quite convenient to use.
109+
In addition to ``JSONWizard``, these handy Mixin_ classes simplify your workflow:
110110

111+
* `JSONPyWizard`_ — A ``JSONWizard`` helper to skip *camelCase* and keep keys as-is.
111112
* `JSONListWizard`_ -- Extends ``JSONWizard`` to return `Container`_ -- instead of *list* -- objects where possible.
112113
* `JSONFileWizard`_ -- Makes it easier to convert dataclass instances from/to JSON files on a local drive.
113114
* `TOMLWizard`_ -- Provides support to convert dataclass instances to/from TOML.
@@ -940,6 +941,130 @@ dataclasses in ``Union`` types. For more info, check out the
940941
# True
941942
assert c == c.from_json(c.to_json())
942943
944+
Conditional Field Skipping
945+
--------------------------
946+
947+
.. admonition:: **Added in v0.30.0**
948+
949+
Dataclass Wizard introduces `conditional skipping`_ to omit fields during JSON serialization based on user-defined conditions. This feature works seamlessly with:
950+
951+
- **Global rules** via ``Meta`` settings.
952+
- **Per-field controls** using ``SkipIf()`` `annotations`_.
953+
- **Field wrappers** for maximum flexibility.
954+
955+
Quick Examples
956+
~~~~~~~~~~~~~~
957+
958+
1. **Globally Skip Fields Matching a Condition**
959+
960+
Define a global skip rule using ``Meta.skip_if``:
961+
962+
.. code-block:: python3
963+
964+
from dataclasses import dataclass
965+
from dataclass_wizard import JSONWizard, IS_NOT
966+
967+
968+
@dataclass
969+
class Example(JSONWizard):
970+
class _(JSONWizard.Meta):
971+
skip_if = IS_NOT(True) # Skip fields if the value is not `True`
972+
973+
my_bool: bool
974+
my_str: 'str | None'
975+
976+
977+
print(Example(my_bool=True, my_str=None).to_dict())
978+
# Output: {'myBool': True}
979+
980+
2. **Skip Defaults Based on a Condition**
981+
982+
Skip fields with default values matching a specific condition using ``Meta.skip_defaults_if``:
983+
984+
.. code-block:: python3
985+
986+
from __future__ import annotations # Can remove in PY 3.10+
987+
988+
from dataclasses import dataclass
989+
from dataclass_wizard import JSONPyWizard, IS
990+
991+
992+
@dataclass
993+
class Example(JSONPyWizard):
994+
class _(JSONPyWizard.Meta):
995+
skip_defaults_if = IS(None) # Skip default `None` values.
996+
997+
str_with_no_default: str | None
998+
my_str: str | None = None
999+
my_bool: bool = False
1000+
1001+
1002+
print(Example(str_with_no_default=None, my_str=None).to_dict())
1003+
#> {'str_with_no_default': None, 'my_bool': False}
1004+
1005+
1006+
.. note::
1007+
Setting ``skip_defaults_if`` also enables ``skip_defaults=True`` automatically.
1008+
1009+
3. **Per-Field Conditional Skipping**
1010+
1011+
Apply skip rules to specific fields with `annotations`_ or ``skip_if_field``:
1012+
1013+
.. code-block:: python3
1014+
1015+
from __future__ import annotations # can be removed in Python 3.10+
1016+
1017+
from dataclasses import dataclass
1018+
from typing import Annotated
1019+
1020+
from dataclass_wizard import JSONWizard, SkipIfNone, skip_if_field, EQ
1021+
1022+
1023+
@dataclass
1024+
class Example(JSONWizard):
1025+
my_str: Annotated[str | None, SkipIfNone] # Skip if `None`.
1026+
other_str: str | None = skip_if_field(EQ(''), default=None) # Skip if empty.
1027+
1028+
print(Example(my_str=None, other_str='').to_dict())
1029+
# Output: {}
1030+
1031+
4. **Skip Fields Based on Truthy or Falsy Values**
1032+
1033+
Use the ``IS_TRUTHY`` and ``IS_FALSY`` helpers to conditionally skip fields based on their truthiness:
1034+
1035+
.. code-block:: python3
1036+
1037+
from dataclasses import dataclass, field
1038+
from dataclass_wizard import JSONWizard, IS_FALSY
1039+
1040+
1041+
@dataclass
1042+
class ExampleWithFalsy(JSONWizard):
1043+
class _(JSONWizard.Meta):
1044+
skip_if = IS_FALSY() # Skip fields if they evaluate as "falsy".
1045+
1046+
my_bool: bool
1047+
my_list: list = field(default_factory=list)
1048+
my_none: None = None
1049+
1050+
print(ExampleWithFalsy(my_bool=False, my_list=[], my_none=None).to_dict())
1051+
#> {}
1052+
1053+
.. note::
1054+
1055+
*Special Cases*
1056+
1057+
- **SkipIfNone**: Alias for ``SkipIf(IS(None))``, skips fields with a value of ``None``.
1058+
- **Condition Helpers**:
1059+
1060+
- ``IS``, ``IS_NOT``: Identity checks.
1061+
- ``EQ``, ``NE``, ``LT``, ``LE``, ``GT``, ``GE``: Comparison operators.
1062+
- ``IS_TRUTHY``, ``IS_FALSY``: Skip fields based on truthy or falsy values.
1063+
1064+
Combine these helpers for flexible serialization rules!
1065+
1066+
.. _conditional skipping: https://dataclass-wizard.readthedocs.io/en/latest/common_use_cases/serialization_options.html#skip-if-functionality
1067+
9431068
Serialization Options
9441069
---------------------
9451070

@@ -1064,6 +1189,33 @@ mixin class.
10641189
For more examples and important how-to's on properties with default values,
10651190
refer to the `Using Field Properties`_ section in the documentation.
10661191

1192+
What's New in v1.0
1193+
------------------
1194+
1195+
.. warning::
1196+
1197+
**Default Key Transformation Update**
1198+
1199+
Starting with ``v1.0.0``, the default key transformation for JSON serialization
1200+
will change to keep keys *as-is* instead of converting them to `camelCase`.
1201+
1202+
- **New Default Behavior**: ``key_transform='NONE'`` will be the standard setting.
1203+
1204+
**How to Prepare**:
1205+
You can enforce this future behavior right now by using the ``JSONPyWizard`` helper:
1206+
1207+
.. code-block:: python3
1208+
1209+
from dataclasses import dataclass
1210+
from dataclass_wizard import JSONPyWizard
1211+
1212+
@dataclass
1213+
class MyModel(JSONPyWizard):
1214+
my_field: str
1215+
1216+
print(MyModel(my_field="value").to_dict())
1217+
# Output: {'my_field': 'value'}
1218+
10671219
Contributing
10681220
------------
10691221

@@ -1089,6 +1241,7 @@ This package was created with Cookiecutter_ and the `rnag/cookiecutter-pypackage
10891241
.. _`rnag/cookiecutter-pypackage`: https://github.com/rnag/cookiecutter-pypackage
10901242
.. _`Contributing`: https://dataclass-wizard.readthedocs.io/en/latest/contributing.html
10911243
.. _`open an issue`: https://github.com/rnag/dataclass-wizard/issues
1244+
.. _`JSONPyWizard`: https://dataclass-wizard.readthedocs.io/en/latest/common_use_cases/wizard_mixins.html#jsonpywizard
10921245
.. _`JSONListWizard`: https://dataclass-wizard.readthedocs.io/en/latest/common_use_cases/wizard_mixins.html#jsonlistwizard
10931246
.. _`JSONFileWizard`: https://dataclass-wizard.readthedocs.io/en/latest/common_use_cases/wizard_mixins.html#jsonfilewizard
10941247
.. _`TOMLWizard`: https://dataclass-wizard.readthedocs.io/en/latest/common_use_cases/wizard_mixins.html#tomlwizard
@@ -1114,3 +1267,4 @@ This package was created with Cookiecutter_ and the `rnag/cookiecutter-pypackage
11141267
.. _Easier Debug Mode: https://dataclass-wizard.readthedocs.io/en/latest/common_use_cases/easier_debug_mode.html
11151268
.. _Handling Unknown JSON Keys: https://dataclass-wizard.readthedocs.io/en/latest/common_use_cases/handling_unknown_json_keys.html
11161269
.. _custom paths to access nested keys: https://dataclass-wizard.readthedocs.io/en/latest/common_use_cases/nested_key_paths.html
1270+
.. _annotations: https://docs.python.org/3/library/typing.html#typing.Annotated

dataclass_wizard/__init__.py

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@
7171
__all__ = [
7272
# Base exports
7373
'JSONSerializable',
74+
'JSONPyWizard',
7475
'JSONWizard',
7576
'LoadMixin',
7677
'DumpMixin',
@@ -90,34 +91,47 @@
9091
'json_field',
9192
'json_key',
9293
'path_field',
94+
'skip_if_field',
9395
'KeyPath',
9496
'Container',
9597
'Pattern',
9698
'DatePattern',
9799
'TimePattern',
98100
'DateTimePattern',
99101
'CatchAll',
102+
'SkipIf',
103+
'SkipIfNone',
104+
'EQ',
105+
'NE',
106+
'LT',
107+
'LE',
108+
'GT',
109+
'GE',
110+
'IS',
111+
'IS_NOT',
112+
'IS_TRUTHY',
113+
'IS_FALSY',
100114
]
101115

102116
import logging
103117

104118
from .bases_meta import LoadMeta, DumpMeta
105119
from .dumpers import DumpMixin, setup_default_dumper, asdict
106120
from .loaders import LoadMixin, setup_default_loader, fromlist, fromdict
107-
from .models import (json_field, json_key, path_field, KeyPath, Container,
108-
Pattern, DatePattern, TimePattern, DateTimePattern, CatchAll)
121+
from .models import (json_field, json_key, path_field, skip_if_field,
122+
KeyPath, Container,
123+
Pattern, DatePattern, TimePattern, DateTimePattern,
124+
CatchAll, SkipIf, SkipIfNone,
125+
EQ, NE, LT, LE, GT, GE, IS, IS_NOT, IS_TRUTHY, IS_FALSY)
109126
from .property_wizard import property_wizard
110-
from .serial_json import JSONSerializable
127+
from .serial_json import JSONWizard, JSONPyWizard, JSONSerializable
111128
from .wizard_mixins import JSONListWizard, JSONFileWizard, TOMLWizard, YAMLWizard
112129

113130

114131
# Set up logging to ``/dev/null`` like a library is supposed to.
115132
# http://docs.python.org/3.3/howto/logging.html#configuring-logging-for-a-library
116133
logging.getLogger('dataclass_wizard').addHandler(logging.NullHandler())
117134

118-
# A handy alias in case it comes in useful to anyone :)
119-
JSONWizard = JSONSerializable
120-
121135
# Setup the default type hooks to use when converting `str` (json) or a Python
122136
# `dict` object to a `dataclass` instance.
123137
setup_default_loader()

dataclass_wizard/bases.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from .constants import TAG
55
from .decorators import cached_class_property
66
from .enums import DateTimeTo, LetterCase
7+
from .models import Condition
78
from .type_def import FrozenKeys
89

910

@@ -113,6 +114,9 @@ class AbstractMeta(metaclass=ABCOrAndMeta):
113114

114115
# True to enable Debug mode for additional (more verbose) log output.
115116
#
117+
# The value can also be a `str` or `int` which specifies
118+
# the minimum level for logs in this library to show up.
119+
#
116120
# For example, a message is logged whenever an unknown JSON key is
117121
# encountered when `from_dict` or `from_json` is called.
118122
#
@@ -205,6 +209,15 @@ class AbstractMeta(metaclass=ABCOrAndMeta):
205209
# the :func:`dataclasses.field`) in the serialization process.
206210
skip_defaults: ClassVar[bool] = False
207211

212+
# Determines the :class:`Condition` to skip / omit dataclass
213+
# fields in the serialization process.
214+
skip_if: ClassVar[Condition] = None
215+
216+
# Determines the condition to skip / omit fields with default values
217+
# (based on the `default` or `default_factory` argument specified for
218+
# the :func:`dataclasses.field`) in the serialization process.
219+
skip_defaults_if: ClassVar[Condition] = None
220+
208221
# noinspection PyMethodParameters
209222
@cached_class_property
210223
def all_fields(cls) -> FrozenKeys:

dataclass_wizard/bases_meta.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from .abstractions import AbstractJSONWizard
1212
from .bases import AbstractMeta, META
1313
from .class_helper import (
14-
_META_INITIALIZER, _META,
14+
META_INITIALIZER, _META,
1515
get_outer_class_name, get_class_name, create_new_class,
1616
json_field_to_dataclass_field, dataclass_field_to_json_field
1717
)
@@ -22,6 +22,7 @@
2222
from .errors import ParseError
2323
from .loaders import get_loader
2424
from .log import LOG
25+
from .models import Condition
2526
from .type_def import E
2627
from .utils.type_conv import date_to_timestamp, as_enum
2728

@@ -59,7 +60,7 @@ def _init_subclass(cls):
5960
# `__init_subclass__` method of any inner classes are run before the
6061
# one for the outer class.
6162
if outer_cls_name is not None:
62-
_META_INITIALIZER[outer_cls_name] = cls.bind_to
63+
META_INITIALIZER[outer_cls_name] = cls.bind_to
6364
else:
6465
# The `Meta` class is defined as an outer class. Emit a warning
6566
# here, just so we can ensure awareness of this special case.
@@ -230,7 +231,10 @@ def DumpMeta(*, debug_enabled: 'bool | int | str' = False,
230231
marshal_date_time_as: Union[DateTimeTo, str] = None,
231232
key_transform: Union[LetterCase, str] = None,
232233
tag: str = None,
233-
skip_defaults: bool = False) -> META:
234+
skip_defaults: bool = False,
235+
skip_if: Condition = None,
236+
skip_defaults_if: Condition = None,
237+
) -> META:
234238
"""
235239
Helper function to setup the ``Meta`` Config for the JSON dump
236240
(serialization) process, which is intended for use alongside the
@@ -254,6 +258,8 @@ def DumpMeta(*, debug_enabled: 'bool | int | str' = False,
254258
'marshal_date_time_as': marshal_date_time_as,
255259
'key_transform_with_dump': key_transform,
256260
'skip_defaults': skip_defaults,
261+
'skip_if': skip_if,
262+
'skip_defaults_if': skip_defaults_if,
257263
'debug_enabled': debug_enabled,
258264
'recursive': recursive,
259265
'tag': tag,

0 commit comments

Comments
 (0)