Repository: tefra/xsdata
Pull request #575 (2021-08-02 19:10:59)

Add serializer option to ignore optional default attributes

sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!&nbsp; &nbsp; ![Quality Gate passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')

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

[![No Coverage information](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/NoCoverageInfo-16px.png 'No Coverage information')](https://sonarcloud.io/component_measures?id=tefra_xsdata&pullRequest=575) No Coverage information  
[![0.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3-16px.png '0.0%')](https://sonarcloud.io/component_measures?id=tefra_xsdata&pullRequest=575&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=tefra_xsdata&pullRequest=575&metric=new_duplicated_lines_density&view=list)


codecov[bot]: # [Codecov](https://codecov.io/gh/tefra/xsdata/pull/575?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou) Report
> Merging [#575](https://codecov.io/gh/tefra/xsdata/pull/575?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou) (fa934b2) into [master](https://codecov.io/gh/tefra/xsdata/commit/7234cf3bdbce22f06094cc54e45b0ed0aeb86641?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou) (7234cf3) will **decrease** coverage by `0.03%`.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/tefra/xsdata/pull/575/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/575?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     #575      +/-   ##
===========================================
- Coverage   100.00%   99.96%   -0.04%     
===========================================
  Files           93       93              
  Lines         8043     8059      +16     
  Branches      1390     1392       +2     
===========================================
+ Hits          8043     8056      +13     
- Misses           0        2       +2     
- Partials         0        1       +1     
```


| [Impacted Files](https://codecov.io/gh/tefra/xsdata/pull/575?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/parsers/tree.py](https://codecov.io/gh/tefra/xsdata/pull/575/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou#diff-eHNkYXRhL2Zvcm1hdHMvZGF0YWNsYXNzL3BhcnNlcnMvdHJlZS5weQ==) | `100.00% <ø> (ø)` | |
| [xsdata/utils/testing.py](https://codecov.io/gh/tefra/xsdata/pull/575/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou#diff-eHNkYXRhL3V0aWxzL3Rlc3RpbmcucHk=) | `100.00% <ø> (ø)` | |
| [xsdata/formats/dataclass/models/builders.py](https://codecov.io/gh/tefra/xsdata/pull/575/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/575/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/formats/dataclass/parsers/config.py](https://codecov.io/gh/tefra/xsdata/pull/575/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou#diff-eHNkYXRhL2Zvcm1hdHMvZGF0YWNsYXNzL3BhcnNlcnMvY29uZmlnLnB5) | `100.00% <100.00%> (ø)` | |
| [xsdata/formats/dataclass/parsers/nodes/element.py](https://codecov.io/gh/tefra/xsdata/pull/575/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou#diff-eHNkYXRhL2Zvcm1hdHMvZGF0YWNsYXNzL3BhcnNlcnMvbm9kZXMvZWxlbWVudC5weQ==) | `100.00% <100.00%> (ø)` | |
| [xsdata/formats/dataclass/serializers/config.py](https://codecov.io/gh/tefra/xsdata/pull/575/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou#diff-eHNkYXRhL2Zvcm1hdHMvZGF0YWNsYXNzL3NlcmlhbGl6ZXJzL2NvbmZpZy5weQ==) | `100.00% <100.00%> (ø)` | |
| [xsdata/formats/dataclass/serializers/json.py](https://codecov.io/gh/tefra/xsdata/pull/575/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou#diff-eHNkYXRhL2Zvcm1hdHMvZGF0YWNsYXNzL3NlcmlhbGl6ZXJzL2pzb24ucHk=) | `100.00% <100.00%> (ø)` | |
| [xsdata/formats/dataclass/serializers/xml.py](https://codecov.io/gh/tefra/xsdata/pull/575/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou#diff-eHNkYXRhL2Zvcm1hdHMvZGF0YWNsYXNzL3NlcmlhbGl6ZXJzL3htbC5weQ==) | `100.00% <100.00%> (ø)` | |
| [xsdata/formats/dataclass/typing.py](https://codecov.io/gh/tefra/xsdata/pull/575/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou#diff-eHNkYXRhL2Zvcm1hdHMvZGF0YWNsYXNzL3R5cGluZy5weQ==) | `97.19% <0.00%> (-2.81%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/tefra/xsdata/pull/575?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/575?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou). Last update [7234cf3...fa934b2](https://codecov.io/gh/tefra/xsdata/pull/575?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/docs/xml.rst b/docs/xml.rst
index 87f0f336..4de14444 100644
--- a/docs/xml.rst
+++ b/docs/xml.rst
@@ -392,12 +392,13 @@ Serializer Config
     ...     encoding="UTF-8",
     ...     xml_version="1.1",
     ...     xml_declaration=False,
+    ...     ignore_default_attributes=True,
     ...     schema_location="urn books.xsd",
     ...     no_namespace_schema_location=None,
     ... ))
     >>> print(serializer.render(books))
     <ns0:books xmlns:ns0="urn:books" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="urn books.xsd">
-      <book id="bk001" lang="en">
+      <book id="bk001">
         <author>Hightower, Kim</author>
         <title>The First Book</title>
         <genre>Fiction</genre>
diff --git a/xsdata/formats/dataclass/models/builders.py b/xsdata/formats/dataclass/models/builders.py
index d6de970c..5dbad68a 100644
--- a/xsdata/formats/dataclass/models/builders.py
+++ b/xsdata/formats/dataclass/models/builders.py
@@ -245,6 +245,7 @@ class XmlVarBuilder:
         namespace = metadata.get("namespace")
         choices = metadata.get("choices", EMPTY_SEQUENCE)
         mixed = metadata.get("mixed", False)
+        required = metadata.get("required", False)
         nillable = metadata.get("nillable", False)
         format_str = metadata.get("format", None)
         sequential = metadata.get("sequential", False)
@@ -288,6 +289,7 @@ class XmlVarBuilder:
             format=format_str,
             clazz=clazz,
             any_type=any_type,
+            required=required,
             nillable=nillable,
             sequential=sequential,
             factory=origin,
diff --git a/xsdata/formats/dataclass/models/elements.py b/xsdata/formats/dataclass/models/elements.py
index ec37ef20..0aa06194 100644
--- a/xsdata/formats/dataclass/models/elements.py
+++ b/xsdata/formats/dataclass/models/elements.py
@@ -65,6 +65,7 @@ class XmlVar(MetaMixin):
     :param format: Value format information
     :param derived: Wrap parsed values with a generic type
     :param any_type: Field supports dynamic value types
+    :param required: Field is mandatory
     :param nillable: Field supports nillable content
     :param sequential: Render values in sequential mode
     :param list_element: Field is a list of elements
@@ -88,6 +89,7 @@ class XmlVar(MetaMixin):
         "format",
         "derived",
         "any_type",
+        "required",
         "nillable",
         "sequential",
         "default",
@@ -122,6 +124,7 @@ class XmlVar(MetaMixin):
         format: Optional[str],
         derived: bool,
         any_type: bool,
+        required: bool,
         nillable: bool,
         sequential: bool,
         default: Any,
@@ -142,6 +145,7 @@ class XmlVar(MetaMixin):
         self.format = format
         self.derived = derived
         self.any_type = any_type
+        self.required = required
         self.nillable = nillable
         self.sequential = sequential
         self.list_element = factory in (list, tuple)
@@ -219,6 +223,16 @@ class XmlVar(MetaMixin):
 
         return None
 
+    def is_optional(self, value: Any) -> bool:
+        """Return whether this var instance is not required and the given value
+        matches the default one."""
+        if self.required:
+            return False
+
+        if callable(self.default):
+            return self.default() == value
+        return self.default == value
+
     @classmethod
     def match_type(
         cls, value: Any, tp: Type, types: Sequence[Type], check_subclass: bool
diff --git a/xsdata/formats/dataclass/parsers/config.py b/xsdata/formats/dataclass/parsers/config.py
index 2c0d776c..cca47fb9 100644
--- a/xsdata/formats/dataclass/parsers/config.py
+++ b/xsdata/formats/dataclass/parsers/config.py
@@ -1,18 +1,15 @@
-from dataclasses import dataclass
 from typing import Callable
 from typing import Dict
 from typing import Optional
 from typing import Type
-from typing import TypeVar
 
-T = TypeVar("T")
+from xsdata.formats.bindings import T
 
 
 def default_class_factory(cls: Type[T], params: Dict) -> T:
     return cls(**params)  # type: ignore
 
 
-@dataclass
 class ParserConfig:
     """
     Parsing configuration options.
@@ -27,8 +24,24 @@ class ParserConfig:
         exceptions
     """
 
-    base_url: Optional[str] = None
-    process_xinclude: bool = False
-    class_factory: Callable[[Type[T], Dict], T] = default_class_factory
-    fail_on_unknown_properties: bool = True
-    fail_on_converter_warnings: bool = False
+    __slots__ = (
+        "base_url",
+        "process_xinclude",
+        "class_factory",
+        "fail_on_unknown_properties",
+        "fail_on_converter_warnings",
+    )
+
+    def __init__(
+        self,
+        base_url: Optional[str] = None,
+        process_xinclude: bool = False,
+        class_factory: Callable[[Type[T], Dict], T] = default_class_factory,
+        fail_on_unknown_properties: bool = True,
+        fail_on_converter_warnings: bool = False,
+    ):
+        self.base_url = base_url
+        self.process_xinclude = process_xinclude
+        self.class_factory = class_factory
+        self.fail_on_unknown_properties = fail_on_unknown_properties
+        self.fail_on_converter_warnings = fail_on_converter_warnings
diff --git a/xsdata/formats/dataclass/parsers/nodes/element.py b/xsdata/formats/dataclass/parsers/nodes/element.py
index f9aff71a..001520a5 100644
--- a/xsdata/formats/dataclass/parsers/nodes/element.py
+++ b/xsdata/formats/dataclass/parsers/nodes/element.py
@@ -81,13 +81,12 @@ class ElementNode(XmlNode):
         self, qname: str, text: Optional[str], tail: Optional[str], objects: List
     ) -> bool:
 
+        obj: Any = None
         if not self.xsi_nil or self.meta.nillable:
             params: Dict = {}
             self.bind_attrs(params)
             self.bind_content(params, text, tail, objects)
             obj = self.config.class_factory(self.meta.clazz, params)
-        else:
-            obj = None
 
         if self.derived_factory:
             obj = self.derived_factory(qname=qname, value=obj, type=self.xsi_type)
diff --git a/xsdata/formats/dataclass/parsers/tree.py b/xsdata/formats/dataclass/parsers/tree.py
index 3e4134c8..4a990de3 100644
--- a/xsdata/formats/dataclass/parsers/tree.py
+++ b/xsdata/formats/dataclass/parsers/tree.py
@@ -52,6 +52,7 @@ class TreeParser(NodeParser):
                 format=None,
                 derived=False,
                 any_type=False,
+                required=False,
                 nillable=False,
                 sequential=False,
                 default=None,
diff --git a/xsdata/formats/dataclass/serializers/config.py b/xsdata/formats/dataclass/serializers/config.py
index 603167ec..b140d75c 100644
--- a/xsdata/formats/dataclass/serializers/config.py
+++ b/xsdata/formats/dataclass/serializers/config.py
@@ -1,25 +1,47 @@
-from dataclasses import dataclass
-from dataclasses import field
 from typing import Optional
 
 
-@dataclass
 class SerializerConfig:
     """
-    Serializing configuration options.
+    Serializer configuration options.
+
+    Some options are not applicable for both xml or json documents.
 
     :param encoding: Text encoding
     :param xml_version: XML Version number (1.0|1.1)
     :param xml_declaration: Generate XML declaration
     :param pretty_print: Enable pretty output
-    :param schema_location: Specify the xsi:schemaLocation attribute value
-    :param no_namespace_schema_location: Specify the xsi:noNamespaceSchemaLocation
+    :param ignore_default_attributes: Ignore optional attributes with
+        default values
+    :param schema_location: xsi:schemaLocation attribute value
+    :param no_namespace_schema_location: xsi:noNamespaceSchemaLocation
         attribute value
     """
 
-    encoding: str = field(default="UTF-8")
-    xml_version: str = field(default="1.0")
-    xml_declaration: bool = field(default=True)
-    pretty_print: bool = field(default=False)
-    schema_location: Optional[str] = field(default=None)
-    no_namespace_schema_location: Optional[str] = field(default=None)
+    __slots__ = (
+        "encoding",
+        "xml_version",
+        "xml_declaration",
+        "pretty_print",
+        "ignore_default_attributes",
+        "schema_location",
+        "no_namespace_schema_location",
+    )
+
+    def __init__(
+        self,
+        encoding: str = "UTF-8",
+        xml_version: str = "1.0",
+        xml_declaration: bool = True,
+        pretty_print: bool = False,
+        ignore_default_attributes: bool = False,
+        schema_location: Optional[str] = None,
+        no_namespace_schema_location: Optional[str] = None,
+    ):
+        self.encoding = encoding
+        self.xml_version = xml_version
+        self.xml_declaration = xml_declaration
+        self.pretty_print = pretty_print
+        self.ignore_default_attributes = ignore_default_attributes
+        self.schema_location = schema_location
+        self.no_namespace_schema_location = no_namespace_schema_location
diff --git a/xsdata/formats/dataclass/serializers/json.py b/xsdata/formats/dataclass/serializers/json.py
index e8edc644..f62e24ce 100644
--- a/xsdata/formats/dataclass/serializers/json.py
+++ b/xsdata/formats/dataclass/serializers/json.py
@@ -94,5 +94,11 @@ class JsonSerializer(AbstractSerializer):
         return converter.serialize(obj, format=var.format)
 
     def next_value(self, obj: Any) -> Iterator[Tuple[str, Any]]:
+        ignore_optionals = self.config.ignore_default_attributes
+
         for var in self.context.build(obj.__class__).get_all_vars():
-            yield var.local_name, self.convert(getattr(obj, var.name), var)
+            value = getattr(obj, var.name)
+            if var.is_attribute and ignore_optionals and var.is_optional(value):
+                continue
+
+            yield var.local_name, self.convert(value, var)
diff --git a/xsdata/formats/dataclass/serializers/xml.py b/xsdata/formats/dataclass/serializers/xml.py
index 0deb26c7..b3cda034 100644
--- a/xsdata/formats/dataclass/serializers/xml.py
+++ b/xsdata/formats/dataclass/serializers/xml.py
@@ -6,9 +6,11 @@ from typing import Any
 from typing import Dict
 from typing import Generator
 from typing import Iterable
+from typing import Iterator
 from typing import List
 from typing import Optional
 from typing import TextIO
+from typing import Tuple
 from typing import Type
 from xml.etree.ElementTree import QName
 
@@ -105,12 +107,13 @@ class XmlSerializer(AbstractSerializer):
 
         yield XmlWriterEvent.START, qname
 
-        for key, value in self.next_attribute(obj, meta, nillable, xsi_type):
+        for key, value in self.next_attribute(
+            obj, meta, nillable, xsi_type, self.config.ignore_default_attributes
+        ):
             yield XmlWriterEvent.ATTR, key, value
 
         for var, value in self.next_value(obj, meta):
-            if value is not None or var.nillable:
-                yield from self.write_value(value, var, namespace)
+            yield from self.write_value(value, var, namespace)
 
         yield XmlWriterEvent.END, qname
 
@@ -302,7 +305,7 @@ class XmlSerializer(AbstractSerializer):
         yield XmlWriterEvent.DATA, cls.encode(value, var)
 
     @classmethod
-    def next_value(cls, obj: Any, meta: XmlMeta):
+    def next_value(cls, obj: Any, meta: XmlMeta) -> Iterator[Tuple[XmlVar, Any]]:
         """
         Return the non attribute variables with their object values in the
         correct order according to their definition and the sequential metadata
@@ -318,7 +321,9 @@ class XmlSerializer(AbstractSerializer):
             var = attrs[index]
 
             if not var.sequential:
-                yield var, getattr(obj, var.name)
+                value = getattr(obj, var.name)
+                if value is not None or var.nillable:
+                    yield var, value
                 index += 1
                 continue
 
@@ -335,23 +340,40 @@ class XmlSerializer(AbstractSerializer):
                     values = getattr(obj, var.name)
                     if j < len(values):
                         rolling = True
-                        yield var, values[j]
+                        value = values[j]
+
+                        if value is not None or var.nillable:
+                            yield var, value
                 j += 1
 
     @classmethod
     def next_attribute(
-        cls, obj: Any, meta: XmlMeta, xsi_nil: bool, xsi_type: Optional[str]
-    ) -> Generator:
+        cls,
+        obj: Any,
+        meta: XmlMeta,
+        nillable: bool,
+        xsi_type: Optional[str],
+        ignore_optionals: bool,
+    ) -> Iterator[Tuple[str, Any]]:
         """
-        Return the attribute variables with their object values.
-
-        Ignores None values and empty lists. Optionally include the xsi
-        properties type and nil.
+        Return the attribute variables with their object values if set and not
+        empty iterables.
+
+        :param obj: Input object
+        :param meta: Object metadata
+        :param nillable: Is model nillable
+        :param xsi_type: The true xsi:type of the object
+        :param ignore_optionals: Skip optional attributes with default value
+        :return:
         """
         for var in meta.get_attribute_vars():
             if var.is_attribute:
                 value = getattr(obj, var.name)
-                if value is None or collections.is_array(value) and not value:
+                if (
+                    value is None
+                    or (collections.is_array(value) and not value)
+                    or (ignore_optionals and var.is_optional(value))
+                ):
                     continue
 
                 yield var.qname, cls.encode(value, var)
@@ -361,7 +383,7 @@ class XmlSerializer(AbstractSerializer):
         if xsi_type:
             yield QNames.XSI_TYPE, QName(xsi_type)
 
-        if xsi_nil:
+        if nillable:
             yield QNames.XSI_NIL, "true"
 
     @classmethod


Test patch:
diff --git a/tests/formats/dataclass/models/test_builders.py b/tests/formats/dataclass/models/test_builders.py
index 128e245d..1a569cc7 100644
--- a/tests/formats/dataclass/models/test_builders.py
+++ b/tests/formats/dataclass/models/test_builders.py
@@ -144,6 +144,7 @@ class XmlMetaBuilderTests(FactoryTestCase):
                 name="author",
                 qname="Author",
                 types=(str,),
+                required=True,
             ),
             XmlVarFactory.create(
                 xml_type=XmlType.ELEMENT,
@@ -151,6 +152,7 @@ class XmlMetaBuilderTests(FactoryTestCase):
                 name="title",
                 qname="Title",
                 types=(str,),
+                required=True,
             ),
             XmlVarFactory.create(
                 xml_type=XmlType.ELEMENT,
@@ -158,6 +160,7 @@ class XmlMetaBuilderTests(FactoryTestCase):
                 name="genre",
                 qname="Genre",
                 types=(str,),
+                required=True,
             ),
             XmlVarFactory.create(
                 xml_type=XmlType.ELEMENT,
@@ -165,6 +168,7 @@ class XmlMetaBuilderTests(FactoryTestCase):
                 name="price",
                 qname="Price",
                 types=(float,),
+                required=True,
             ),
             XmlVarFactory.create(
                 xml_type=XmlType.ELEMENT,
@@ -172,6 +176,7 @@ class XmlMetaBuilderTests(FactoryTestCase):
                 name="pub_date",
                 qname="PubDate",
                 types=(XmlDate,),
+                required=True,
             ),
             XmlVarFactory.create(
                 xml_type=XmlType.ELEMENT,
@@ -179,6 +184,7 @@ class XmlMetaBuilderTests(FactoryTestCase):
                 name="review",
                 qname="Review",
                 types=(str,),
+                required=True,
             ),
             XmlVarFactory.create(
                 xml_type=XmlType.ATTRIBUTE, index=7, name="id", qname="ID", types=(str,)
diff --git a/tests/formats/dataclass/serializers/test_json.py b/tests/formats/dataclass/serializers/test_json.py
index ace75ff0..ff805663 100644
--- a/tests/formats/dataclass/serializers/test_json.py
+++ b/tests/formats/dataclass/serializers/test_json.py
@@ -116,3 +116,25 @@ class JsonSerializerTests(TestCase):
             "JsonSerializer indent property is deprecated, use SerializerConfig",
             str(w[-1].message),
         )
+
+    def test_next_value(self):
+        book = self.books.book[0]
+        serializer = JsonSerializer()
+
+        actual = [name for name, value in serializer.next_value(book)]
+        expected = [
+            "author",
+            "title",
+            "genre",
+            "price",
+            "pub_date",
+            "review",
+            "id",
+            "lang",
+        ]
+        self.assertEqual(expected, actual)
+
+        serializer.config.ignore_default_attributes = True
+        expected = expected[:-1]
+        actual = [name for name, value in serializer.next_value(book)]
+        self.assertEqual(expected, actual)
diff --git a/tests/formats/dataclass/serializers/test_xml.py b/tests/formats/dataclass/serializers/test_xml.py
index 18d75d70..56c74c2c 100644
--- a/tests/formats/dataclass/serializers/test_xml.py
+++ b/tests/formats/dataclass/serializers/test_xml.py
@@ -505,7 +505,7 @@ class XmlSerializerTests(TestCase):
         self.assertEqual(expected, list(result))
 
     def test_next_value(self):
-        obj = SequentialType(x0=1, x1=[2, 3, 4], x2=[6, 7], x3=[9])
+        obj = SequentialType(x0=1, x1=[2, 3, 4, None], x2=[6, 7], x3=[9])
         meta = self.serializer.context.build(SequentialType)
         x0 = meta.text
         x1 = next(meta.find_children("x1"))
@@ -530,7 +530,7 @@ class XmlSerializerTests(TestCase):
         obj = SequentialType(a0="foo", a1={"b": "c", "d": "e"})
         meta = self.serializer.context.build(SequentialType)
 
-        actual = self.serializer.next_attribute(obj, meta, False, None)
+        actual = self.serializer.next_attribute(obj, meta, False, None, False)
         expected = [
             ("a0", "foo"),
             ("b", "c"),
@@ -540,7 +540,7 @@ class XmlSerializerTests(TestCase):
         self.assertIsInstance(actual, Generator)
         self.assertEqual(expected, list(actual))
 
-        actual = self.serializer.next_attribute(obj, meta, True, "xs:bool")
+        actual = self.serializer.next_attribute(obj, meta, True, "xs:bool", False)
         expected.extend(
             [
                 (QNames.XSI_TYPE, "xs:bool"),
@@ -549,6 +549,17 @@ class XmlSerializerTests(TestCase):
         )
         self.assertEqual(expected, list(actual))
 
+        meta.attributes["a0"].required = False
+        meta.attributes["a0"].default = "foo"
+        actual = self.serializer.next_attribute(obj, meta, False, None, True)
+        expected = [
+            ("b", "c"),
+            ("d", "e"),
+        ]
+
+        self.assertIsInstance(actual, Generator)
+        self.assertEqual(expected, list(actual))
+
     def test_render_mixed_content(self):
 
         obj = Paragraph()
diff --git a/tests/formats/dataclass/test_elements.py b/tests/formats/dataclass/test_elements.py
index dd74b8b8..bf97c9c9 100644
--- a/tests/formats/dataclass/test_elements.py
+++ b/tests/formats/dataclass/test_elements.py
@@ -95,6 +95,18 @@ class XmlValTests(TestCase):
         self.assertEqual(var.elements["a"], var.find_value_choice(TypeA(1), True))
         self.assertEqual(var.elements["b"], var.find_value_choice(TypeB(1, "b"), True))
 
+    def test_is_optional(self):
+        var = XmlVarFactory.create(xml_type=XmlType.ATTRIBUTE, name="att")
+
+        self.assertTrue(var.is_optional(None))
+        self.assertFalse(var.is_optional("foo"))
+
+        var.default = lambda: [1, 2, 3]
+        self.assertTrue(var.is_optional([1, 2, 3]))
+
+        var.required = True
+        self.assertFalse(var.is_optional([1, 2, 3]))
+
     def test_match_namespace(self):
         var = XmlVarFactory.create(xml_type=XmlType.WILDCARD, name="foo")
         self.assertTrue(var.match_namespace("a"))
diff --git a/xsdata/utils/testing.py b/xsdata/utils/testing.py
index 73e3bf1e..be59be33 100644
--- a/xsdata/utils/testing.py
+++ b/xsdata/utils/testing.py
@@ -356,6 +356,7 @@ class XmlVarFactory(Factory):
         format: Optional[str] = None,
         derived: bool = False,
         any_type: bool = False,
+        required: bool = False,
         nillable: bool = False,
         sequential: bool = False,
         list_element: bool = False,
@@ -395,6 +396,7 @@ class XmlVarFactory(Factory):
             format=format,
             derived=derived,
             any_type=any_type,
+            required=required,
             nillable=nillable,
             sequential=sequential,
             list_element=list_element,