Repository: lidatong/dataclasses-json

Issue:
Omitting fields when serializing
Is it possible to omit fields when `to_dict` or `to_json` is invoked?  My use-case is I have fields with sensitive information that I only want available to the internal parts of my application, and would like to ensure they're never pushed to my end-users.

Right now, I'm accomplishing it by overriding `to_dict` as follows:

```python
@dataclass
class MyObject(DataClassJsonMixin):
    """An object."""

    id_: uuid.UUID = field(metadata=config(field_name="id"))
    name: str
    secret: str
    another_secret: str

    def to_dict(  # type: ignore[override]
        self, omit_sensitive=True, **kwargs
    ) -> Dict[str, "dataclasses_json.core.Json"]:
        """Serialize the dataclass, with an optional ability to omit sensitive/internal fields."""
        serialized = super().to_dict(**kwargs)
        if omit_sensitive:
            for field_name in list(serialized.keys()):
                if field_name in ("secret", "another_secret"):
                    del serialized[field_name]
        return serialized
```

Ideally, this would be something I could define as part of the `field()`.  Is this currently possible with dataclass-json, and, if not, would this be something that you would consider for future functionality?

Comments:

Patch:
diff --git a/dataclasses_json/__init__.py b/dataclasses_json/__init__.py
index f76ef77..2a001b1 100644
--- a/dataclasses_json/__init__.py
+++ b/dataclasses_json/__init__.py
@@ -2,5 +2,5 @@
 from dataclasses_json.api import (DataClassJsonMixin,
                                   LetterCase,
                                   dataclass_json)
-from dataclasses_json.cfg import config, global_config
+from dataclasses_json.cfg import config, global_config, Exclude
 from dataclasses_json.undefined import CatchAll, Undefined
diff --git a/dataclasses_json/cfg.py b/dataclasses_json/cfg.py
index 8f1ff8d..7a6413d 100644
--- a/dataclasses_json/cfg.py
+++ b/dataclasses_json/cfg.py
@@ -1,10 +1,24 @@
 import functools
-from typing import (Callable, Dict, Optional, Union)
+from typing import Callable, Dict, NamedTuple, Optional, TypeVar, Union
 
 from marshmallow.fields import Field as MarshmallowField
 
 from dataclasses_json.undefined import Undefined, UndefinedParameterError
 
+T = TypeVar("T")
+
+
+class _Exclude(NamedTuple):
+    """
+    Whether or not the field should be excluded when encoded
+    """
+
+    ALWAYS: Callable[[T], bool] = lambda _: True
+    NEVER: Callable[[T], bool] = lambda _: False
+
+
+Exclude = _Exclude()
+
 
 # TODO: add warnings?
 class _GlobalConfig:
@@ -35,7 +49,9 @@ def config(metadata: dict = None, *,
            mm_field: MarshmallowField = None,
            letter_case: Callable[[str], str] = None,
            undefined: Optional[Union[str, Undefined]] = None,
-           field_name: str = None) -> Dict[str, dict]:
+           field_name: str = None,
+           exclude: Optional[Callable[[str, T], bool]] = None,
+           ) -> Dict[str, dict]:
     if metadata is None:
         metadata = {}
 
@@ -75,4 +91,7 @@ def config(metadata: dict = None, *,
 
         lib_metadata['undefined'] = undefined
 
+    if exclude is not None:
+        lib_metadata['exclude'] = exclude
+
     return metadata
diff --git a/dataclasses_json/core.py b/dataclasses_json/core.py
index 527287f..ddb3238 100644
--- a/dataclasses_json/core.py
+++ b/dataclasses_json/core.py
@@ -48,7 +48,7 @@ class _ExtendedEncoder(json.JSONEncoder):
 
 
 def _user_overrides_or_exts(cls):
-    confs = ['encoder', 'decoder', 'mm_field', 'letter_case']
+    confs = ['encoder', 'decoder', 'mm_field', 'letter_case', 'exclude']
     FieldOverride = namedtuple('FieldOverride', confs)
 
     global_metadata = defaultdict(dict)
@@ -94,6 +94,11 @@ def _encode_overrides(kvs, overrides, encode_json=False):
     override_kvs = {}
     for k, v in kvs.items():
         if k in overrides:
+            exclude = overrides[k].exclude
+            # If the exclude predicate returns true, the key should be
+            #  excluded from encoding, so skip the rest of the loop
+            if exclude and exclude(v):
+                continue
             letter_case = overrides[k].letter_case
             original_key = k
             k = letter_case(k) if letter_case is not None else k


Test patch:
diff --git a/tests/test_exclude.py b/tests/test_exclude.py
new file mode 100644
index 0000000..cf5eeb1
--- /dev/null
+++ b/tests/test_exclude.py
@@ -0,0 +1,51 @@
+from dataclasses import dataclass, field
+
+from dataclasses_json.api import DataClassJsonMixin, config
+from dataclasses_json.cfg import Exclude
+
+
+@dataclass
+class EncodeExclude(DataClassJsonMixin):
+    public_field: str
+    private_field: str = field(metadata=config(exclude=Exclude.ALWAYS))
+
+
+@dataclass
+class EncodeInclude(DataClassJsonMixin):
+    public_field: str
+    private_field: str = field(metadata=config(exclude=Exclude.NEVER))
+
+
+@dataclass
+class EncodeCustom(DataClassJsonMixin):
+    public_field: str
+    sensitive_field: str = field(
+        metadata=config(exclude=lambda v: v.startswith("secret"))
+    )
+
+
+def test_exclude():
+    dclass = EncodeExclude(public_field="public", private_field="private")
+    encoded = dclass.to_dict()
+    assert "public_field" in encoded
+    assert "private_field" not in encoded
+
+
+def test_include():
+    dclass = EncodeInclude(public_field="public", private_field="private")
+    encoded = dclass.to_dict()
+    assert "public_field" in encoded
+    assert "private_field" in encoded
+    assert encoded["private_field"] == "private"
+
+
+def test_custom_action_included():
+    dclass = EncodeCustom(public_field="public", sensitive_field="notsecret")
+    encoded = dclass.to_dict()
+    assert "sensitive_field" in encoded
+
+
+def test_custom_action_excluded():
+    dclass = EncodeCustom(public_field="public", sensitive_field="secret")
+    encoded = dclass.to_dict()
+    assert "sensitive_field" not in encoded