Repository: tefra/xsdata

Issue:
Exclude private attributes when serialize to xml
Hello,

I'm in need to have some "private" attributes in my dataclass which I want to serialize to xml. But those private attributes should not be included in the serialized xml. Is there a way to achieve this or is this a feature request.

Here's a minimal example how it looks like now:

```python
from dataclasses import dataclass
from typing import Optional

from xsdata.formats.dataclass.serializers import XmlSerializer
from xsdata.formats.dataclass.serializers.config import SerializerConfig


@dataclass
class User:
    name: str
    age: int

    _privatekey: Optional[str] = None

    @property
    def privatekey(self) -> Optional[str]:
        return self._privatekey

    @privatekey.setter
    def privatekey(self, value: str) -> None:
        self._privatekey = value


user = User(name="john doe", age=42)
user.privatekey = "testdata"

serializer_config = SerializerConfig(pretty_print=True, xml_declaration=False)
serializer = XmlSerializer(config=serializer_config)
xml = serializer.render(user)

print(xml)
```

**Output:**
```
<User>
  <name>john doe</name>
  <age>42</age>
  <_privatekey>testdata</_privatekey>
</User>
```

**Output in Request:**
```
<User>
  <name>john doe</name>
  <age>42</age>
</User>
```

fin swimmer

Discussion:
sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!

[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug.png' alt='Bug' width='16' height='16' />](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=506&resolved=false&types=BUG) [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A.png' alt='A' width='16' height='16' />](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=506&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=506&resolved=false&types=BUG)  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability.png' alt='Vulnerability' width='16' height='16' />](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=506&resolved=false&types=VULNERABILITY) [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A.png' alt='A' width='16' height='16' />](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=506&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=506&resolved=false&types=VULNERABILITY)  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot.png' alt='Security Hotspot' width='16' height='16' />](https://sonarcloud.io/project/security_hotspots?id=tefra_xsdata&pullRequest=506&resolved=false&types=SECURITY_HOTSPOT) [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A.png' alt='A' width='16' height='16' />](https://sonarcloud.io/project/security_hotspots?id=tefra_xsdata&pullRequest=506&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=tefra_xsdata&pullRequest=506&resolved=false&types=SECURITY_HOTSPOT)  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell.png' alt='Code Smell' width='16' height='16' />](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=506&resolved=false&types=CODE_SMELL) [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A.png' alt='A' width='16' height='16' />](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=506&resolved=false&types=CODE_SMELL) [0 Code Smells](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=506&resolved=false&types=CODE_SMELL)

[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/NoCoverageInfo.png' alt='No Coverage information' width='16' height='16' />](https://sonarcloud.io/component_measures?id=tefra_xsdata&pullRequest=506) No Coverage information  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3.png' alt='0.0%' width='16' height='16' />](https://sonarcloud.io/component_measures?id=tefra_xsdata&pullRequest=506&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=tefra_xsdata&pullRequest=506&metric=new_duplicated_lines_density&view=list)


codecov[bot]: # [Codecov](https://codecov.io/gh/tefra/xsdata/pull/506?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou) Report
> Merging [#506](https://codecov.io/gh/tefra/xsdata/pull/506?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou) (39c0c6b) into [master](https://codecov.io/gh/tefra/xsdata/commit/eed822b83f362f48561a7d116e181a5422ff52dd?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou) (eed822b) will **not change** coverage.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/tefra/xsdata/pull/506/graphs/tree.svg?width=650&height=150&src=pr&token=YzDDLtywvl&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou)](https://codecov.io/gh/tefra/xsdata/pull/506?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou)

```diff
@@            Coverage Diff            @@
##            master      #506   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files           84        84           
  Lines         7407      7412    +5     
  Branches      1297      1299    +2     
=========================================
+ Hits          7407      7412    +5     
```


| [Impacted Files](https://codecov.io/gh/tefra/xsdata/pull/506?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou) | Coverage Δ | |
|---|---|---|
| [xsdata/formats/dataclass/models/builders.py](https://codecov.io/gh/tefra/xsdata/pull/506/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou#diff-eHNkYXRhL2Zvcm1hdHMvZGF0YWNsYXNzL21vZGVscy9idWlsZGVycy5weQ==) | `100.00% <100.00%> (ø)` | |
| [xsdata/formats/dataclass/models/elements.py](https://codecov.io/gh/tefra/xsdata/pull/506/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou#diff-eHNkYXRhL2Zvcm1hdHMvZGF0YWNsYXNzL21vZGVscy9lbGVtZW50cy5weQ==) | `100.00% <100.00%> (ø)` | |
| [xsdata/models/mixins.py](https://codecov.io/gh/tefra/xsdata/pull/506/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou#diff-eHNkYXRhL21vZGVscy9taXhpbnMucHk=) | `100.00% <100.00%> (ø)` | |
| [xsdata/models/wsdl.py](https://codecov.io/gh/tefra/xsdata/pull/506/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou#diff-eHNkYXRhL21vZGVscy93c2RsLnB5) | `100.00% <100.00%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/tefra/xsdata/pull/506?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/tefra/xsdata/pull/506?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou). Last update [eed822b...39c0c6b](https://codecov.io/gh/tefra/xsdata/pull/506?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou).


Patch:
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index bdb756bc..fc7b061a 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -11,7 +11,7 @@ repos:
     hooks:
       - id: reorder-python-imports
   - repo: https://github.com/ambv/black
-    rev: 21.5b1
+    rev: 21.5b2
     hooks:
       - id: black
   - repo: https://gitlab.com/pycqa/flake8
diff --git a/docs/models.rst b/docs/models.rst
index fa66c3db..9f4c91e2 100644
--- a/docs/models.rst
+++ b/docs/models.rst
@@ -143,7 +143,7 @@ binding procedures.
    * - type
      - str
      - The field xml type:
-       ``Text | Element | Elements | Attribute | Wildcard | Attributes``,
+       ``Ignore | Text | Element | Elements | Attribute | Wildcard | Attributes ``,
        default: ``Text`` or ``Element``
    * - nillable
      - bool
@@ -185,6 +185,23 @@ but currently they are only used to troubleshoot the code generator.
     namespace to an empty string ``namespace=""``.
 
 
+Type: Ignore
+~~~~~~~~~~~~
+
+This type will force the internal xml context instance to ignore the field during
+binding. Make sure your field is declared with `init=False` or with a default value
+otherwise data binding will fail.
+
+
+.. code-block:: python
+
+    index: int = field(
+        default_factory=int,
+        init=False,
+        metadata={"type": "Ignore"},
+    )
+
+
 Type: Element
 ~~~~~~~~~~~~~
 
diff --git a/xsdata/formats/dataclass/models/builders.py b/xsdata/formats/dataclass/models/builders.py
index 74616b27..d86e798e 100644
--- a/xsdata/formats/dataclass/models/builders.py
+++ b/xsdata/formats/dataclass/models/builders.py
@@ -115,16 +115,18 @@ class XmlMetaBuilder:
             attribute_name_generator=attribute_name_generator,
         )
 
-        for index, var in enumerate(fields(clazz)):
-            yield builder.build(
+        for index, _field in enumerate(fields(clazz)):
+            var = builder.build(
                 index,
-                var.name,
-                type_hints[var.name],
-                var.metadata,
-                var.init,
-                cls.default_field_value(var),
+                _field.name,
+                type_hints[_field.name],
+                _field.metadata,
+                _field.init,
+                cls.default_field_value(_field),
                 globalns,
             )
+            if var is not None:
+                yield var
 
     @classmethod
     def build_source_qname(cls, clazz: Type, element_name_generator: Callable) -> str:
@@ -202,10 +204,13 @@ class XmlVarBuilder:
         init: bool,
         default_value: Any,
         globalns: Any,
-    ) -> XmlVar:
+    ) -> Optional[XmlVar]:
         """Build the binding metadata for a dataclass field."""
-        tokens = metadata.get("tokens", False)
         xml_type = metadata.get("type", self.default_xml_type)
+        if xml_type == XmlType.IGNORE:
+            return None
+
+        tokens = metadata.get("tokens", False)
         local_name = metadata.get("name")
         namespace = metadata.get("namespace")
         choices = metadata.get("choices", EMPTY_SEQUENCE)
@@ -236,7 +241,7 @@ class XmlVarBuilder:
             else:  # choice.is_wildcard:
                 wildcards.append(choice)
 
-        var = XmlVar(
+        return XmlVar(
             index=index + 1,
             name=name,
             qname=qname,
@@ -257,8 +262,6 @@ class XmlVarBuilder:
             derived=False,
         )
 
-        return var
-
     def build_choices(
         self, name: str, choices: List[Dict], globalns: Any
     ) -> Iterator[XmlVar]:
@@ -280,6 +283,10 @@ class XmlVarBuilder:
             var = self.build(
                 index, name, type_hint, metadata, True, default_value, globalns
             )
+
+            # It's impossible for choice elements to be ignorable, read above!
+            assert var is not None
+
             var.list_element = True
 
             if var.any_type or any(True for tp in var.types if tp in existing_types):
diff --git a/xsdata/formats/dataclass/models/elements.py b/xsdata/formats/dataclass/models/elements.py
index 5057f12e..d3121653 100644
--- a/xsdata/formats/dataclass/models/elements.py
+++ b/xsdata/formats/dataclass/models/elements.py
@@ -31,6 +31,7 @@ class XmlType:
     WILDCARD = sys.intern("Wildcard")
     ATTRIBUTE = sys.intern("Attribute")
     ATTRIBUTES = sys.intern("Attributes")
+    IGNORE = sys.intern("Ignore")
 
 
 class MetaMixin:
diff --git a/xsdata/models/mixins.py b/xsdata/models/mixins.py
index b27996da..62a97e05 100644
--- a/xsdata/models/mixins.py
+++ b/xsdata/models/mixins.py
@@ -29,8 +29,16 @@ class ElementBase:
     :param ns_map: Namespace prefix-URI map
     """
 
-    index: int = field(default_factory=int, init=False)
-    ns_map: Dict[str, str] = field(default_factory=dict, init=False)
+    index: int = field(
+        default_factory=int,
+        init=False,
+        metadata={"type": "Ignore"},
+    )
+    ns_map: Dict[str, str] = field(
+        default_factory=dict,
+        init=False,
+        metadata={"type": "Ignore"},
+    )
 
     @property
     def class_name(self) -> str:
diff --git a/xsdata/models/wsdl.py b/xsdata/models/wsdl.py
index 661f1efb..8e830aad 100644
--- a/xsdata/models/wsdl.py
+++ b/xsdata/models/wsdl.py
@@ -37,7 +37,9 @@ class WsdlElement:
 
     name: str = attribute()
     documentation: Optional[Documentation] = element()
-    ns_map: Dict[str, str] = field(default_factory=dict, init=False)
+    ns_map: Dict[str, str] = field(
+        default_factory=dict, init=False, metadata={"type": "Ignore"}
+    )
 
 
 @dataclass


Test patch:
diff --git a/tests/fixtures/models.py b/tests/fixtures/models.py
index d703d804..5805371a 100644
--- a/tests/fixtures/models.py
+++ b/tests/fixtures/models.py
@@ -16,6 +16,7 @@ class TypeA:
 class TypeB:
     x: int
     y: str
+    skip: Optional[str] = field(default=None, metadata={"type": "Ignore"})
 
 
 @dataclass
diff --git a/tests/formats/dataclass/models/test_builders.py b/tests/formats/dataclass/models/test_builders.py
index 1daebdf4..b496b058 100644
--- a/tests/formats/dataclass/models/test_builders.py
+++ b/tests/formats/dataclass/models/test_builders.py
@@ -15,6 +15,7 @@ from unittest import TestCase
 
 from tests.fixtures.artists import Artist
 from tests.fixtures.books import BookForm
+from tests.fixtures.models import TypeB
 from tests.fixtures.series import Country
 from xsdata.exceptions import XmlContextError
 from xsdata.formats.dataclass.models.builders import XmlMetaBuilder
@@ -160,6 +161,13 @@ class XmlMetaBuilderTests(FactoryTestCase):
         for var in result:
             self.assertIsNone(var.clazz)
 
+    def test_build_vars_with_ignore_types(self):
+        result = XmlMetaBuilder.build_vars(TypeB, None, return_input, return_input)
+        self.assertIsInstance(result, Iterator)
+
+        actual = list(result)
+        self.assertEqual(2, len(actual))
+
     def test_default_xml_type(self):
         cls = make_dataclass("a", [("x", int)])
         self.assertEqual(XmlType.TEXT, XmlMetaBuilder.default_xml_type(cls))