diff --git a/.chronus/changes/copilot-add-xml-deserialization-test-2026-03-04-05-46-13.md b/.chronus/changes/copilot-add-xml-deserialization-test-2026-03-04-05-46-13.md
new file mode 100644
index 00000000000..e006e732936
--- /dev/null
+++ b/.chronus/changes/copilot-add-xml-deserialization-test-2026-03-04-05-46-13.md
@@ -0,0 +1,6 @@
+---
+changeKind: fix
+packages:
+ - "@typespec/http-client-python"
+---
+Return empty list instead of None for non-optional unwrapped XML list fields during deserialization
diff --git a/.chronus/changes/http-client-python-xml-enumeration-results-test-2026-2-23-22-33-2.md b/.chronus/changes/http-client-python-xml-enumeration-results-test-2026-2-23-22-33-2.md
new file mode 100644
index 00000000000..c4a7ac65651
--- /dev/null
+++ b/.chronus/changes/http-client-python-xml-enumeration-results-test-2026-2-23-22-33-2.md
@@ -0,0 +1,7 @@
+---
+changeKind: internal
+packages:
+ - "@typespec/http-client-python"
+---
+
+Add unit test for deserializing Azure Blob Storage EnumerationResults XML payload with attributes, empty list element, and empty string element.
diff --git a/packages/http-client-python/generator/pygen/codegen/templates/model_base.py.jinja2 b/packages/http-client-python/generator/pygen/codegen/templates/model_base.py.jinja2
index c4c58fe89e5..253b0c1c1d8 100644
--- a/packages/http-client-python/generator/pygen/codegen/templates/model_base.py.jinja2
+++ b/packages/http-client-python/generator/pygen/codegen/templates/model_base.py.jinja2
@@ -652,6 +652,9 @@ class Model(_MyMutableMapping):
if len(items) > 0:
existed_attr_keys.append(xml_name)
dict_to_pass[rf._rest_name] = _deserialize(rf._type, items)
+ elif not rf._is_optional:
+ existed_attr_keys.append(xml_name)
+ dict_to_pass[rf._rest_name] = []
continue
# text element is primitive type
@@ -930,6 +933,8 @@ def _get_deserialize_callable_from_annotation( # pylint: disable=too-many-retur
# is it optional?
try:
if any(a is _NONE_TYPE for a in annotation.__args__): # pyright: ignore
+ if rf:
+ rf._is_optional = True
if len(annotation.__args__) <= 2: # pyright: ignore
if_obj_deserializer = _get_deserialize_callable_from_annotation(
next(a for a in annotation.__args__ if a is not _NONE_TYPE), module, rf # pyright: ignore
@@ -1090,9 +1095,7 @@ def _failsafe_deserialize_xml(
return None
-{% if code_model.has_padded_model_property %}
# pylint: disable=too-many-instance-attributes
-{% endif %}
class _RestField:
def __init__(
self,
@@ -1115,6 +1118,7 @@ class _RestField:
self._is_discriminator = is_discriminator
self._visibility = visibility
self._is_model = False
+ self._is_optional = False
self._default = default
self._format = format
self._is_multipart_file_input = is_multipart_file_input
diff --git a/packages/http-client-python/generator/test/unittests/test_model_base_xml_serialization.py b/packages/http-client-python/generator/test/unittests/test_model_base_xml_serialization.py
index 27529cc5263..f026fa6afd5 100644
--- a/packages/http-client-python/generator/test/unittests/test_model_base_xml_serialization.py
+++ b/packages/http-client-python/generator/test/unittests/test_model_base_xml_serialization.py
@@ -4,7 +4,7 @@
# ------------------------------------
import xml.etree.ElementTree as ET
-from typing import Literal
+from typing import Literal, Optional
from specialwords._utils.model_base import (
_get_element,
@@ -165,7 +165,7 @@ def __init__(self, *args, **kwargs):
_xml = {"name": "Data"}
result = _deserialize_xml(XmlModel, basic_xml)
- assert result.age is None
+ assert result.age == []
def test_list_wrapped_items_name_basic_types(self):
"""Test XML list and wrap, items is basic type and there is itemsName."""
@@ -515,6 +515,248 @@ def __init__(self, *args, **kwargs):
assert isinstance(result.filter, CorrelationFilter)
assert result.filter.correlation_id == 12
+ def test_enumeration_results(self):
+ """Test deserializing an Azure Blob Storage EnumerationResults XML payload."""
+ xml_payload = '/'
+
+ class EnumerationResults(Model):
+ service_endpoint: str = rest_field(
+ name="ServiceEndpoint", xml={"name": "ServiceEndpoint", "attribute": True}
+ )
+ container_name: str = rest_field(name="ContainerName", xml={"name": "ContainerName", "attribute": True})
+ delimiter: str = rest_field(name="Delimiter", xml={"name": "Delimiter"})
+ blobs: list[str] = rest_field(name="Blobs", xml={"name": "Blobs"})
+ next_marker: str = rest_field(name="NextMarker", xml={"name": "NextMarker"})
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ _xml = {"name": "EnumerationResults"}
+
+ result = _deserialize_xml(EnumerationResults, xml_payload)
+
+ assert result.service_endpoint == "https://service.blob.core.windows.net/"
+ assert result.container_name == "my-container-108f32e8"
+ assert result.delimiter == "/"
+ assert result.blobs == []
+ assert result.next_marker == ""
+
+ def test_enumeration_results_nested_empty_list(self):
+ """Test deserializing XML where a container element holds a nested empty list (e.g. Blobs/BlobPrefixes)."""
+ xml_payload = '/'
+
+ class BlobPrefix(Model):
+ name: str = rest_field(name="Name", xml={"name": "Name"})
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ _xml = {"name": "BlobPrefix"}
+
+ class BlobsSegment(Model):
+ blob_prefixes: list[BlobPrefix] = rest_field(
+ name="BlobPrefixes", xml={"name": "BlobPrefixes", "itemsName": "BlobPrefix"}
+ )
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ _xml = {"name": "Blobs"}
+
+ class EnumerationResults(Model):
+ service_endpoint: str = rest_field(
+ name="ServiceEndpoint", xml={"name": "ServiceEndpoint", "attribute": True}
+ )
+ container_name: str = rest_field(name="ContainerName", xml={"name": "ContainerName", "attribute": True})
+ delimiter: str = rest_field(name="Delimiter", xml={"name": "Delimiter"})
+ blobs: BlobsSegment = rest_field(name="Blobs", xml={"name": "Blobs"})
+ next_marker: str = rest_field(name="NextMarker", xml={"name": "NextMarker"})
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ _xml = {"name": "EnumerationResults"}
+
+ result = _deserialize_xml(EnumerationResults, xml_payload)
+
+ assert result.service_endpoint == "https://service.blob.core.windows.net/"
+ assert result.container_name == "my-container"
+ assert result.delimiter == "/"
+ assert result.blobs.blob_prefixes == []
+ assert result.next_marker == ""
+
+ def test_enumeration_results_azure_sdk_pattern(self):
+ """Test the real Azure SDK model pattern where BlobsSegment has two unwrapped list fields."""
+ # Both blob_prefixes and blob_items are unwrapped lists (items appear directly in ).
+ # With , no matching children are found so both are None.
+ xml_payload = '/'
+
+ class BlobPrefix(Model):
+ name: str = rest_field(name="Name", xml={"name": "Name"})
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ _xml = {"name": "BlobPrefix"}
+
+ class BlobItem(Model):
+ name: str = rest_field(name="Name", xml={"name": "Name"})
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ _xml = {"name": "Blob"}
+
+ class BlobsSegment(Model):
+ blob_prefixes: list[BlobPrefix] = rest_field(
+ name="blob_prefixes", xml={"name": "BlobPrefix", "unwrapped": True}
+ )
+ blob_items: list[BlobItem] = rest_field(name="blob_items", xml={"name": "Blob", "unwrapped": True})
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ _xml = {"name": "Blobs"}
+
+ class EnumerationResults(Model):
+ service_endpoint: str = rest_field(
+ name="ServiceEndpoint", xml={"name": "ServiceEndpoint", "attribute": True}
+ )
+ container_name: str = rest_field(name="ContainerName", xml={"name": "ContainerName", "attribute": True})
+ delimiter: str = rest_field(name="Delimiter", xml={"name": "Delimiter"})
+ blobs: BlobsSegment = rest_field(name="Blobs", xml={"name": "Blobs"})
+ next_marker: str = rest_field(name="NextMarker", xml={"name": "NextMarker"})
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ _xml = {"name": "EnumerationResults"}
+
+ result = _deserialize_xml(EnumerationResults, xml_payload)
+
+ assert result.service_endpoint == "https://service.blob.core.windows.net/"
+ assert result.container_name == "my-container"
+ assert result.delimiter == "/"
+ assert isinstance(result.blobs, BlobsSegment)
+ # With , no or children exist → unwrapped non-optional lists default to []
+ assert result.blobs.blob_prefixes == []
+ assert result.blobs.blob_items == []
+ assert result.next_marker == ""
+
+ def test_enumeration_results_azure_sdk_pattern_optional(self):
+ """Test the Azure SDK pattern where unwrapped list fields are Optional[list[X]].
+
+ When the type is Optional[list[X]], empty unwrapped lists should stay None
+ (the element is absent, and None is a valid value for the optional type).
+ """
+ xml_payload = '/'
+
+ class BlobPrefix(Model):
+ name: str = rest_field(name="Name", xml={"name": "Name"})
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ _xml = {"name": "BlobPrefix"}
+
+ class BlobItem(Model):
+ name: str = rest_field(name="Name", xml={"name": "Name"})
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ _xml = {"name": "Blob"}
+
+ class BlobsSegment(Model):
+ blob_prefixes: Optional[list[BlobPrefix]] = rest_field(
+ name="blob_prefixes", xml={"name": "BlobPrefix", "unwrapped": True}
+ )
+ blob_items: Optional[list[BlobItem]] = rest_field(
+ name="blob_items", xml={"name": "Blob", "unwrapped": True}
+ )
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ _xml = {"name": "Blobs"}
+
+ class EnumerationResults(Model):
+ service_endpoint: str = rest_field(
+ name="ServiceEndpoint", xml={"name": "ServiceEndpoint", "attribute": True}
+ )
+ container_name: str = rest_field(name="ContainerName", xml={"name": "ContainerName", "attribute": True})
+ delimiter: str = rest_field(name="Delimiter", xml={"name": "Delimiter"})
+ blobs: BlobsSegment = rest_field(name="Blobs", xml={"name": "Blobs"})
+ next_marker: str = rest_field(name="NextMarker", xml={"name": "NextMarker"})
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ _xml = {"name": "EnumerationResults"}
+
+ result = _deserialize_xml(EnumerationResults, xml_payload)
+
+ assert result.service_endpoint == "https://service.blob.core.windows.net/"
+ assert result.container_name == "my-container"
+ assert result.delimiter == "/"
+ assert isinstance(result.blobs, BlobsSegment)
+ # With , no or children exist → Optional lists stay None
+ assert result.blobs.blob_prefixes is None
+ assert result.blobs.blob_items is None
+ assert result.next_marker == ""
+
+ def test_enumeration_results_blobs_unwrapped(self):
+ """Test what happens when the blobs field itself is declared with unwrapped=True."""
+ # When a non-list model field uses unwrapped=True, the matching XML elements are collected
+ # as a list and stored as-is (the field receives a list of ET.Element objects).
+ xml_payload = '/'
+
+ class BlobPrefix(Model):
+ name: str = rest_field(name="Name", xml={"name": "Name"})
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ _xml = {"name": "BlobPrefix"}
+
+ class BlobsSegment(Model):
+ blob_prefixes: list[BlobPrefix] = rest_field(
+ name="BlobPrefixes", xml={"name": "BlobPrefixes", "itemsName": "BlobPrefix"}
+ )
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ _xml = {"name": "Blobs"}
+
+ class EnumerationResults(Model):
+ service_endpoint: str = rest_field(
+ name="ServiceEndpoint", xml={"name": "ServiceEndpoint", "attribute": True}
+ )
+ container_name: str = rest_field(name="ContainerName", xml={"name": "ContainerName", "attribute": True})
+ delimiter: str = rest_field(name="Delimiter", xml={"name": "Delimiter"})
+ # unwrapped=True on a model-typed field: the deserialization collects matching XML
+ # elements as a list (rather than deserializing them into the model).
+ blobs: BlobsSegment = rest_field(name="Blobs", xml={"name": "Blobs", "unwrapped": True})
+ next_marker: str = rest_field(name="NextMarker", xml={"name": "NextMarker"})
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ _xml = {"name": "EnumerationResults"}
+
+ result = _deserialize_xml(EnumerationResults, xml_payload)
+
+ assert result.service_endpoint == "https://service.blob.core.windows.net/"
+ assert result.container_name == "my-container"
+ assert result.delimiter == "/"
+ # unwrapped=True on a model field collects matching elements; is found so it
+ # returns a list containing the raw ET.Element instead of a deserialized BlobsSegment.
+ assert isinstance(result.blobs, list)
+ assert len(result.blobs) == 1
+ assert isinstance(result.blobs[0], ET.Element)
+ assert result.next_marker == ""
+
class TestXmlSerialization:
def test_basic(self):