Repository: tefra/xsdata

Issue:
A way to not output tags with nillable="true" and minOccurs="0" ?
Hi, 

Is there a way to control the behavior of the serializer (XML serializer in my case) when handling elements declared with both `nillable="true"` and `minOccurs="0"` ?

The doc says for a `nillable` field :

> Specify if the field has to be present in the serialized result even when it doesn't have any meaningful content.

However if the field is also declared `minOccurs="0"`, it's perfectly valid to simply ignore the element in the final XML output.

For example, given this schema :  

```xml
<?xml version="1.0" encoding="utf-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
    <xs:element name="elements">
      <xs:complexType>
        <xs:sequence>
          <xs:element minOccurs="0" name="foobar" nillable="true" type="xs:string"/>
        </xs:sequence>
      </xs:complexType>
    </xs:element>
</xs:schema>
```

I can generate two XML snippets like so :  

```py
from xsdata.formats.dataclass.serializers import XmlSerializer
from xsdata.formats.dataclass.serializers.config import SerializerConfig

from nillabletest import *  # my package generated from the .xsd

config = SerializerConfig(encoding="utf-8", ignore_default_attributes=True, indent="  ")
serializer = XmlSerializer(config=config)
tree = Elements(foobar="hello, world!")
print(serializer.render(tree))

print("--------------")

tree = Elements()
print(serializer.render(tree))
```

Which gives : 

```xml
<?xml version="1.0" encoding="utf-8"?>
<elements>
  <foobar>hello, world!</foobar>
</elements>
```

```xml
<?xml version="1.0" encoding="utf-8"?>
<elements>
  <foobar xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:nil="true"/>
</elements>
```

In the second scenario (no value given to `foobar`), I would prefer the resulting XML not to contain the `<foobar>` tag at all :  

```xml
<?xml version="1.0" encoding="utf-8"?>
<elements>
</elements>
```

Which is still valid against the original .xsd, but I see no way to instruct the XMLSerializer to give this output.
Am I missing something?

Thanks for your help, and for this massively helpful project!

Cheers,

Discussion:
sonarcloud[bot]: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=tefra_xsdata&pullRequest=1067) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=1067&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=1067&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=tefra_xsdata&pullRequest=1067&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=tefra_xsdata&pullRequest=1067&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=tefra_xsdata&pullRequest=1067&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=tefra_xsdata&pullRequest=1067)


sonarcloud[bot]: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=tefra_xsdata&pullRequest=1067) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=1067&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=1067&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=tefra_xsdata&pullRequest=1067&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=tefra_xsdata&pullRequest=1067&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=tefra_xsdata&pullRequest=1067&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=tefra_xsdata&pullRequest=1067)


codecov[bot]: ## [Codecov](https://app.codecov.io/gh/tefra/xsdata/pull/1067?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 100.00%. Comparing base [(`6eccffb`)](https://app.codecov.io/gh/tefra/xsdata/commit/6eccffb886ca537c4d1b1fa0f59cac637311e43f?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou) to head [(`00d910f`)](https://app.codecov.io/gh/tefra/xsdata/commit/00d910f2a8133ba937d47eaf39449729db7f5a03?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou).

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff            @@
##              main     #1067   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files          115       115           
  Lines         9259      9259           
  Branches      2183      2183           
=========================================
  Hits          9259      9259           
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/tefra/xsdata/pull/1067?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou).

sonarcloud[bot]: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=tefra_xsdata&pullRequest=1067) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=1067&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=1067&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=tefra_xsdata&pullRequest=1067&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=tefra_xsdata&pullRequest=1067&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=tefra_xsdata&pullRequest=1067&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=tefra_xsdata&pullRequest=1067)



Patch:
diff --git a/docs/models/fields.md b/docs/models/fields.md
index 8c151c49..09f1fd5e 100644
--- a/docs/models/fields.md
+++ b/docs/models/fields.md
@@ -63,12 +63,12 @@ The namespace name of the XML element or attribute.
 ### `nillable`
 
 Specify if the field has to be present in the serialized result even when it doesn't
-have any meaningful content.
+have any meaningful content. If the field is not required the serializer will ignore it.
 
 ```python
 >>> @dataclass
 ... class Root:
-...     first: Optional[str] = field(metadata={"nillable": True}, default=None)
+...     first: Optional[str] = field(metadata={"nillable": True, "required": True}, default=None)
 ...     second: Optional[str] = field(default=None)
 ...
 >>> print(serializer.render(Root()))
diff --git a/xsdata/formats/dataclass/serializers/mixins.py b/xsdata/formats/dataclass/serializers/mixins.py
index da3a0f47..3e7c31fc 100644
--- a/xsdata/formats/dataclass/serializers/mixins.py
+++ b/xsdata/formats/dataclass/serializers/mixins.py
@@ -933,7 +933,7 @@ class EventGenerator:
 
             if var.sequence is None:
                 value = getattr(obj, var.name)
-                if value is not None or var.nillable:
+                if value is not None or (var.nillable and var.required):
                     yield var, value
                 index += 1
                 continue
@@ -955,11 +955,11 @@ class EventGenerator:
                         if j < len(values):
                             rolling = True
                             value = values[j]
-                            if value is not None or var.nillable:
+                            if value is not None or (var.nillable and var.required):
                                 yield var, value
                     elif j == 0:
                         rolling = True
-                        if values is not None or var.nillable:
+                        if values is not None or (var.nillable and var.required):
                             yield var, values
 
                 j += 1


Test patch:
diff --git a/tests/fixtures/models.py b/tests/fixtures/models.py
index 6987ccad..b841ca17 100644
--- a/tests/fixtures/models.py
+++ b/tests/fixtures/models.py
@@ -172,6 +172,8 @@ class SequentialType:
     x4: Optional[int] = field(
         default=None, metadata={"type": "Element", "sequence": 2}
     )
+    x5: Optional[str] = field(default=None, metadata={"type": "Element", "nillable": True})
+    x6: Optional[str] = field(default=None, metadata={"type": "Element", "nillable": True, "required": True})
 
 
 @dataclass
diff --git a/tests/formats/dataclass/serializers/test_mixins.py b/tests/formats/dataclass/serializers/test_mixins.py
index 2eb2e828..e79cd1c1 100644
--- a/tests/formats/dataclass/serializers/test_mixins.py
+++ b/tests/formats/dataclass/serializers/test_mixins.py
@@ -693,6 +693,10 @@ class EventGeneratorTests(TestCase):
         expected = [
             ("start", "a"),
             ("attr", "a0", "foo"),
+            ("start", "{xsdata}x6"),
+            ("attr", QNames.XSI_NIL, "true"),
+            ("data", None),
+            ("end", "{xsdata}x6"),
             ("end", "a"),
         ]
 
@@ -814,6 +818,7 @@ class EventGeneratorTests(TestCase):
         x2 = next(meta.find_children("x2"))
         x3 = next(meta.find_children("x3"))
         x4 = next(meta.find_children("x4"))
+        x6 = next(meta.find_children("x6"))
 
         actual = self.generator.next_value(obj, meta)
         expected = [
@@ -825,6 +830,7 @@ class EventGeneratorTests(TestCase):
             (x1, 4),
             (x3, 9),
             (x4, 10),
+            (x6, None),
         ]
 
         self.assertIsInstance(actual, Generator)