Source code for json2xml.dicttoxml

from __future__ import annotations

import datetime
import logging
import numbers
from collections.abc import Callable, Sequence
from decimal import Decimal
from fractions import Fraction
from functools import lru_cache
from io import BytesIO
from random import SystemRandom
from typing import Any, Union, cast

__lazy_modules__ = ["defusedxml.minidom"]

# Create a safe random number generator
_SAFE_RANDOM = SystemRandom()

# Set up logging
LOG = logging.getLogger("dicttoxml")
_XML_ESCAPE_CHARS = frozenset("&\"'<>")


class _XMLWriter:
    """Small UTF-8 byte writer used by the internal streaming serializer."""

    __slots__ = ("_buffer",)

    def __init__(self) -> None:
        self._buffer = BytesIO()

    def write(self, value: str) -> None:
        self._buffer.write(value.encode("utf-8"))

    def to_bytes(self) -> bytes:
        return self._buffer.getvalue()


[docs] def make_id(element: str, start: int = 100000, end: int = 999999) -> str: """ Generate a random ID for a given element. Args: element (str): The element to generate an ID for. start (int, optional): The lower bound for the random number. Defaults to 100000. end (int, optional): The upper bound for the random number. Defaults to 999999. Returns: str: The generated ID. """ return f"{element}_{_SAFE_RANDOM.randint(start, end)}"
[docs] def get_unique_id(element: str) -> str: """ Generate a unique ID for a given element. Args: element (str): The element to generate an ID for. Returns: str: The unique ID. """ ids: list[str] = [] # initialize list of unique ids this_id = make_id(element) dup = True while dup: if this_id not in ids: dup = False ids.append(this_id) else: # pragma: no cover this_id = make_id(element) return ids[-1]
ELEMENT = Union[ str, int, float, bool, complex, Decimal, Fraction, numbers.Number, Sequence[Any], datetime.datetime, datetime.date, None, dict[str, Any], ]
[docs] def get_xml_type(val: Any) -> str: """ Get the XML type of a given value. Args: val (ELEMENT): The value to get the type of. Returns: str: The XML type. """ if val is None: return "null" val_type = type(val) if val_type is str: return "str" if val_type is int: return "int" if val_type is float: return "float" if val_type is bool: return "bool" if isinstance(val, numbers.Number): return "number" if isinstance(val, dict): return "dict" if isinstance(val, Sequence): return "list" return type(val).__name__
[docs] def escape_xml(s: str | int | float | numbers.Number | None) -> str: """ Escape a string for use in XML. Args: s (str | numbers.Number): The string to escape. Returns: str: The escaped string. """ if isinstance(s, str): if not _XML_ESCAPE_CHARS.intersection(s): return s s = s.replace("&", "&amp;") s = s.replace('"', "&quot;") s = s.replace("'", "&apos;") s = s.replace("<", "&lt;") s = s.replace(">", "&gt;") return str(s)
[docs] def make_attrstring(attr: dict[str, Any]) -> str: """ Create a string of XML attributes from a dictionary. Args: attr (dict[str, Any]): The dictionary of attributes. Returns: str: The string of XML attributes. """ if not attr: return "" validate_xml_attr_names(attr) if len(attr) == 1: key, val = next(iter(attr.items())) if key == "type": return f' type="{val}"' return f' {key}="{escape_xml(val)}"' attrstring = " ".join(f'{k}="{escape_xml(v)}"' for k, v in attr.items()) return f" {attrstring}"
[docs] def make_typed_attrstring(attr: dict[str, Any], xml_type: str) -> str: """Create XML attributes with a type value without mutating caller attrs.""" if not attr: return f' type="{xml_type}"' typed_attr = dict(attr) typed_attr["type"] = xml_type return make_attrstring(typed_attr)
def _is_fast_valid_xml_name(key: str) -> bool: """Return True for ASCII XML names known to be accepted by the legacy parser.""" if not key or not key.isascii() or ":" in key: return False first = key[0] if not (first.isalpha() or first == "_"): return False return all(char.isalnum() or char in {"-", "_", "."} for char in key[1:])
[docs] @lru_cache(maxsize=4096) def key_is_valid_xml(key: str) -> bool: """ Check if a key is a valid XML name. Args: key (str): The key to check. Returns: bool: True if the key is a valid XML name, False otherwise. """ key = str(key) if _is_fast_valid_xml_name(key): return True if not key or key.isdigit() or ":" in key: return False from defusedxml.minidom import parseString test_xml = f'<?xml version="1.0" encoding="UTF-8" ?><{key}>foo</{key}>' try: parseString(test_xml) return True except Exception: # minidom does not implement exceptions well return False
[docs] @lru_cache(maxsize=4096) def key_is_valid_xml_attr(key: str) -> bool: """Return True when key can be emitted directly as an XML attribute name.""" key = str(key) if not key: return False from defusedxml.minidom import parseString test_xml = f'<?xml version="1.0" encoding="UTF-8" ?><root {key}="value"></root>' try: parseString(test_xml) return True except Exception: # minidom does not implement exceptions well return False
[docs] def validate_xml_attr_names(attr: dict[str, Any]) -> None: """Reject attributes that would make the generated XML malformed.""" for key in attr: if not key_is_valid_xml_attr(key): raise ValueError(f"Invalid XML attribute name: {key}")
[docs] def make_valid_xml_name(key: str, attr: dict[str, Any]) -> tuple[str, dict[str, Any]]: """Return a valid XML element name and carry the original key as metadata when needed.""" key = str(key) if key_is_valid_xml(key): return key, attr if key.isdigit(): return f"n{key}", attr key_with_underscores = key.replace(" ", "_") if key_is_valid_xml(key_with_underscores): return key_with_underscores, attr if ":" in key and key_is_valid_xml(key.replace(":", "")): return key, attr attr["name"] = key return "key", attr
[docs] def wrap_cdata(s: str | int | float | numbers.Number) -> str: """Wraps a string into CDATA sections""" s = str(s).replace("]]>", "]]]]><![CDATA[>") return "<![CDATA[" + s + "]]>"
[docs] def default_item_func(parent: str) -> str: return "item"
# XPath 3.1 json-to-xml conversion # Spec: https://www.w3.org/TR/xpath-functions-31/#json-to-xml-mapping XPATH_FUNCTIONS_NS = "http://www.w3.org/2005/xpath-functions"
[docs] def get_xpath31_tag_name(val: Any) -> str: """ Determine XPath 3.1 tag name by Python type. See: https://www.w3.org/TR/xpath-functions-31/#func-json-to-xml Args: val: The value to get the tag name for. Returns: str: The XPath 3.1 tag name (map, array, string, number, boolean, null). """ if val is None: return "null" if isinstance(val, bool): return "boolean" if isinstance(val, dict): return "map" if isinstance(val, (int, float, numbers.Number)): return "number" if isinstance(val, str): return "string" if isinstance(val, (bytes, bytearray)): return "string" if isinstance(val, Sequence): return "array" return "string"
# @lat: [[behavior#XPath 3.1 format]]
[docs] def convert_to_xpath31(obj: Any, parent_key: str | None = None) -> str: """ Convert a Python object to XPath 3.1 json-to-xml format. See: https://www.w3.org/TR/xpath-functions-31/#json-to-xml-mapping Args: obj: The object to convert. parent_key: The key from the parent dict (used for key attribute). Returns: str: XML string in XPath 3.1 format. """ output = _XMLWriter() _append_xpath31(output, obj, parent_key) return output.to_bytes().decode("utf-8")
def _append_xpath31( output: _XMLWriter, obj: Any, parent_key: str | None = None, namespace: bool = False, ) -> None: """Append XPath 3.1 json-to-xml output without building child strings.""" key_attr = f' key="{escape_xml(parent_key)}"' if parent_key is not None else "" namespace_attr = f' xmlns="{XPATH_FUNCTIONS_NS}"' if namespace else "" tag_name = get_xpath31_tag_name(obj) if tag_name == "null": output.write(f"<null{namespace_attr}{key_attr}/>") elif tag_name == "boolean": output.write(f"<boolean{namespace_attr}{key_attr}>{str(obj).lower()}</boolean>") elif tag_name == "number": output.write(f"<number{namespace_attr}{key_attr}>{obj}</number>") elif tag_name == "string": output.write(f"<string{namespace_attr}{key_attr}>{escape_xml(str(obj))}</string>") elif tag_name == "map": output.write(f"<map{namespace_attr}{key_attr}>") for key, val in obj.items(): _append_xpath31(output, val, key) output.write("</map>") elif tag_name == "array": output.write(f"<array{namespace_attr}{key_attr}>") for item in obj: _append_xpath31(output, item) output.write("</array>") else: output.write(f"<string{namespace_attr}{key_attr}>{escape_xml(str(obj))}</string>")
[docs] def convert( obj: Any, ids: Any, attr_type: bool, item_func: Callable[[str], str], cdata: bool, item_wrap: bool, parent: str = "root", list_headers: bool = False, ) -> str: """Routes the elements of an object to the right function to convert them based on their data type""" output = _XMLWriter() _append_convert( output, obj, ids, attr_type, item_func, cdata, item_wrap, parent, list_headers=list_headers, ) return output.to_bytes().decode("utf-8")
[docs] def is_primitive_type(val: Any) -> bool: return val is None or isinstance(val, (str, bool, numbers.Number))
[docs] def dict2xml_str( attr_type: bool, attr: dict[str, Any], item: dict[str, Any], item_func: Callable[[str], str], cdata: bool, item_name: str, item_wrap: bool, parentIsList: bool, parent: str = "", list_headers: bool = False, ) -> str: """ parse dict2xml """ output = _XMLWriter() _append_dict2xml_str( output, attr_type, attr, item, item_func, cdata, item_name, item_wrap, parentIsList, parent, list_headers=list_headers, ) return output.to_bytes().decode("utf-8")
[docs] def list2xml_str( attr_type: bool, attr: dict[str, Any], item: Sequence[Any], item_func: Callable[[str], str], cdata: bool, item_name: str, item_wrap: bool, list_headers: bool = False, ) -> str: output = _XMLWriter() _append_list2xml_str( output, attr_type, attr, item, item_func, cdata, item_name, item_wrap, list_headers=list_headers, ) return output.to_bytes().decode("utf-8")
[docs] def convert_dict( obj: dict[str, Any], ids: list[str], parent: str, attr_type: bool, item_func: Callable[[str], str], cdata: bool, item_wrap: bool, list_headers: bool = False ) -> str: """Converts a dict into an XML string.""" output = _XMLWriter() _append_convert_dict( output, obj, ids, parent, attr_type, item_func, cdata, item_wrap, list_headers=list_headers, ) return output.to_bytes().decode("utf-8")
[docs] def convert_list( items: Sequence[Any], ids: list[str] | None, parent: str, attr_type: bool, item_func: Callable[[str], str], cdata: bool, item_wrap: bool, list_headers: bool = False, ) -> str: """Converts a list into an XML string.""" output = _XMLWriter() _append_convert_list( output, items, ids, parent, attr_type, item_func, cdata, item_wrap, list_headers=list_headers, ) return output.to_bytes().decode("utf-8")
def _append_convert( output: _XMLWriter, obj: Any, ids: Any, attr_type: bool, item_func: Callable[[str], str], cdata: bool, item_wrap: bool, parent: str = "root", list_headers: bool = False, ) -> None: """Append converted XML directly into output without building subtree strings.""" item_name = item_func(parent) if isinstance(obj, bool): output.write(convert_bool(key=item_name, val=obj, attr_type=attr_type, cdata=cdata)) elif isinstance(obj, numbers.Number): output.write(convert_kv(key=item_name, val=obj, attr_type=attr_type, attr={}, cdata=cdata)) elif isinstance(obj, str): output.write(convert_kv(key=item_name, val=obj, attr_type=attr_type, attr={}, cdata=cdata)) elif hasattr(obj, "isoformat") and isinstance(obj, (datetime.datetime, datetime.date)): output.write( convert_kv( key=item_name, val=obj.isoformat(), attr_type=attr_type, attr={}, cdata=cdata, ) ) elif obj is None: output.write(convert_none(key=item_name, attr_type=attr_type, cdata=cdata)) elif isinstance(obj, dict): _append_convert_dict( output, cast("dict[str, Any]", obj), ids, parent, attr_type, item_func, cdata, item_wrap, list_headers=list_headers, ) elif isinstance(obj, Sequence): _append_convert_list( output, obj, ids, parent, attr_type, item_func, cdata, item_wrap, list_headers=list_headers, ) else: raise TypeError(f"Unsupported data type: {obj} ({type(obj).__name__})") def _append_dict2xml_str( output: _XMLWriter, attr_type: bool, attr: dict[str, Any], item: dict[str, Any], item_func: Callable[[str], str], cdata: bool, item_name: str, item_wrap: bool, parentIsList: bool, parent: str = "", list_headers: bool = False, ) -> None: """Append a dict element using the same shape as dict2xml_str.""" ids: list[str] = [] attr = dict(attr) if attr_type: attr["type"] = get_xml_type(item) has_custom_attrs = "@attrs" in item if has_custom_attrs: raw_attrs = item["@attrs"] val_attr = raw_attrs if isinstance(raw_attrs, dict) else dict(raw_attrs) rawitem = item["@val"] if "@val" in item else { key: value for key, value in item.items() if key != "@attrs" } else: val_attr = attr rawitem = item.get("@val", item) if parentIsList and list_headers: if len(val_attr) > 0 and not item_wrap: output.write(f"<{parent}{make_attrstring(val_attr)}>") else: output.write(f"<{parent}>") _append_rawitem( output, rawitem, ids, attr_type, item_func, cdata, item_wrap, item_name, list_headers, ) output.write(f"</{parent}>") elif item.get("@flat", False) or (parentIsList and not item_wrap): _append_rawitem( output, rawitem, ids, attr_type, item_func, cdata, item_wrap, item_name, list_headers, ) else: output.write(f"<{item_name}{make_attrstring(val_attr)}>") _append_rawitem( output, rawitem, ids, attr_type, item_func, cdata, item_wrap, item_name, list_headers, ) output.write(f"</{item_name}>") def _append_rawitem( output: _XMLWriter, rawitem: Any, ids: list[str], attr_type: bool, item_func: Callable[[str], str], cdata: bool, item_wrap: bool, item_name: str, list_headers: bool, ) -> None: if rawitem is None: return if isinstance(rawitem, bool): output.write("true" if rawitem else "false") elif isinstance(rawitem, (str, numbers.Number)): output.write(escape_xml(str(rawitem))) else: _append_convert( output, rawitem, ids, attr_type, item_func, cdata, item_wrap, item_name, list_headers=list_headers, ) def _append_list2xml_str( output: _XMLWriter, attr_type: bool, attr: dict[str, Any], item: Sequence[Any], item_func: Callable[[str], str], cdata: bool, item_name: str, item_wrap: bool, list_headers: bool = False, ) -> None: ids: list[str] = [] attr = dict(attr) if attr_type: attr["type"] = get_xml_type(item) flat = False if item_name.endswith("@flat"): item_name = item_name[0:-5] flat = True if flat or (len(item) > 0 and is_primitive_type(item[0]) and not item_wrap) or list_headers: _append_convert_list( output, item, ids, item_name, attr_type, item_func, cdata, item_wrap, list_headers=list_headers, ) return output.write(f"<{item_name}{make_attrstring(attr)}>") _append_convert_list( output, item, ids, item_name, attr_type, item_func, cdata, item_wrap, list_headers=list_headers, ) output.write(f"</{item_name}>") def _append_convert_dict( output: _XMLWriter, obj: dict[str, Any], ids: list[str], parent: str, attr_type: bool, item_func: Callable[[str], str], cdata: bool, item_wrap: bool, list_headers: bool = False, ) -> None: """Append a dict as XML without allocating a joined child subtree.""" for key, val in obj.items(): attr = {} if not ids else {"id": f"{get_unique_id(parent)}"} key_is_flat = isinstance(key, str) and key.endswith("@flat") xml_key = key[:-5] if key_is_flat else key key, attr = make_valid_xml_name(xml_key, attr) if isinstance(val, bool): output.write(convert_bool_valid_name(key, val, attr_type, attr)) elif isinstance(val, (numbers.Number, str)): output.write( convert_kv_valid_name( key=key, val=val, attr_type=attr_type, attr=attr, cdata=cdata ) ) elif hasattr(val, "isoformat"): output.write( convert_kv_valid_name( key=key, val=val.isoformat(), attr_type=attr_type, attr=attr, cdata=cdata, ) ) elif isinstance(val, dict): _append_dict2xml_str( output, attr_type, attr, val, item_func, cdata, key, item_wrap, False, list_headers=list_headers, ) elif isinstance(val, Sequence): _append_list2xml_str( output, attr_type=attr_type, attr=attr, item=val, item_func=item_func, cdata=cdata, item_name=f"{key}@flat" if key_is_flat else key, item_wrap=item_wrap, list_headers=list_headers, ) elif not val: output.write(convert_none_valid_name(key, attr_type, attr)) else: raise TypeError(f"Unsupported data type: {val} ({type(val).__name__})") def _append_convert_list( output: _XMLWriter, items: Sequence[Any], ids: list[str] | None, parent: str, attr_type: bool, item_func: Callable[[str], str], cdata: bool, item_wrap: bool, list_headers: bool = False, ) -> None: """Append a list as XML without allocating a joined child subtree.""" item_name = item_func(parent) if item_name.endswith("@flat"): item_name = item_name[:-5] item_name, item_name_attr = make_valid_xml_name(item_name, {}) scalar_key = item_name if item_wrap else parent scalar_key, scalar_key_attr = make_valid_xml_name(scalar_key, {}) this_id = get_unique_id(parent) if ids else None for i, item in enumerate(items): base_attr: dict[str, Any] | None = None if ids: base_attr = {"id": f"{this_id}_{i + 1}"} if isinstance(item, bool): attr = dict(base_attr) if base_attr else {} if item_name_attr: attr.update(item_name_attr) output.write(convert_bool_valid_name(item_name, item, attr_type, attr)) elif isinstance(item, (numbers.Number, str)): attr = dict(base_attr) if base_attr else {} if scalar_key_attr: attr.update(scalar_key_attr) output.write( convert_kv_valid_name( key=scalar_key, val=item, attr_type=attr_type, attr=attr, cdata=cdata, ) ) elif hasattr(item, "isoformat"): attr = dict(base_attr) if base_attr else {} if item_name_attr: attr.update(item_name_attr) output.write( convert_kv_valid_name( key=item_name, val=item.isoformat(), attr_type=attr_type, attr=attr, cdata=cdata, ) ) elif isinstance(item, dict): attr = dict(base_attr) if base_attr else {} _append_dict2xml_str( output, attr_type=attr_type, attr=attr, item=item, item_func=item_func, cdata=cdata, item_name=item_name, item_wrap=item_wrap, parentIsList=True, parent=parent, list_headers=list_headers, ) elif isinstance(item, Sequence): attr = dict(base_attr) if base_attr else {} _append_list2xml_str( output, attr_type=attr_type, attr=attr, item=item, item_func=item_func, cdata=cdata, item_name=item_name, item_wrap=item_wrap, list_headers=list_headers, ) elif item is None: attr = dict(base_attr) if base_attr else {} if item_name_attr: attr.update(item_name_attr) output.write(convert_none_valid_name(item_name, attr_type, attr)) else: raise TypeError(f"Unsupported data type: {item} ({type(item).__name__})")
[docs] def convert_kv( key: str, val: str | int | float | numbers.Number | datetime.datetime | datetime.date, attr_type: bool, attr: dict[str, Any] | None = None, cdata: bool = False, ) -> str: """Converts a number, string, or datetime into an XML element""" if attr is None: attr = {} key, attr = make_valid_xml_name(key, attr) # Convert datetime to isoformat string if hasattr(val, "isoformat") and isinstance(val, (datetime.datetime, datetime.date)): val = val.isoformat() if attr_type: attr["type"] = get_xml_type(val) attr_string = make_attrstring(attr) return f"<{key}{attr_string}>{wrap_cdata(val) if cdata else escape_xml(val)}</{key}>"
[docs] def convert_kv_valid_name( key: str, val: str | int | float | numbers.Number | datetime.datetime | datetime.date, attr_type: bool, attr: dict[str, Any], cdata: bool = False, ) -> str: """Converts a scalar into an XML element when the caller already validated the key.""" if hasattr(val, "isoformat") and isinstance(val, (datetime.datetime, datetime.date)): val = val.isoformat() attr_string = make_typed_attrstring(attr, get_xml_type(val)) if attr_type else make_attrstring(attr) return f"<{key}{attr_string}>{wrap_cdata(val) if cdata else escape_xml(val)}</{key}>"
[docs] def convert_bool( key: str, val: bool, attr_type: bool, attr: dict[str, Any] | None = None, cdata: bool = False ) -> str: """Converts a boolean into an XML element""" if attr is None: attr = {} key, attr = make_valid_xml_name(key, attr) if attr_type: attr["type"] = get_xml_type(val) attr_string = make_attrstring(attr) return f"<{key}{attr_string}>{str(val).lower()}</{key}>"
[docs] def convert_bool_valid_name( key: str, val: bool, attr_type: bool, attr: dict[str, Any], ) -> str: """Converts a boolean when the caller already validated the key.""" attr_string = make_typed_attrstring(attr, "bool") if attr_type else make_attrstring(attr) return f"<{key}{attr_string}>{'true' if val else 'false'}</{key}>"
[docs] def convert_none( key: str, attr_type: bool, attr: dict[str, Any] | None = None, cdata: bool = False ) -> str: """Converts a null value into an XML element""" if attr is None: attr = {} key, attr = make_valid_xml_name(key, attr) if attr_type: attr["type"] = get_xml_type(None) attr_string = make_attrstring(attr) return f"<{key}{attr_string}></{key}>"
[docs] def convert_none_valid_name( key: str, attr_type: bool, attr: dict[str, Any] ) -> str: """Converts a null value when the caller already validated the key.""" attr_string = make_typed_attrstring(attr, "null") if attr_type else make_attrstring(attr) return f"<{key}{attr_string}></{key}>"
# @lat: [[architecture#Conversion engine]]
[docs] def dicttoxml( obj: ELEMENT, root: bool = True, custom_root: str = "root", ids: list[int] | None = None, attr_type: bool = True, item_wrap: bool = True, item_func: Callable[[str], str] = default_item_func, cdata: bool = False, xml_namespaces: dict[str, Any] | None = None, list_headers: bool = False, xpath_format: bool = False, ) -> bytes: """ Converts a python object into XML. :param dict obj: dictionary :param bool root: Default is True specifies wheter the output is wrapped in an XML root element :param custom_root: Default is 'root' allows you to specify a custom root element. :param bool ids: Default is False specifies whether elements get unique ids. :param bool attr_type: Default is True specifies whether elements get a data type attribute. :param bool item_wrap: Default is True specifies whether to nest items in array in <item/> Example if True ..code-block:: python data = {'bike': ['blue', 'green']} .. code-block:: xml <bike> <item>blue</item> <item>green</item> </bike> Example if False ..code-block:: python data = {'bike': ['blue', 'green']} ..code-block:: xml <bike>blue</bike> <bike>green</bike>' :param item_func: items in a list. Default is 'item' specifies what function should generate the element name for :param bool cdata: Default is False specifies whether string values should be wrapped in CDATA sections. :param xml_namespaces: is a dictionary where key is xmlns prefix and value the urn, Default is {}. Example: .. code-block:: python { 'flex': 'http://www.w3.org/flex/flexBase', 'xsl': "http://www.w3.org/1999/XSL/Transform"} results in .. code-block:: xml <root xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:flex="http://www.w3.org/flex/flexBase"> :param bool list_headers: Default is False Repeats the header for every element in a list. Example if True: .. code-block:: python "Bike": [ {'frame_color': 'red'}, {'frame_color': 'green'} ]} results in .. code-block:: xml <Bike><frame_color>red</frame_color></Bike> <Bike><frame_color>green</frame_color></Bike> :param bool xpath_format: Default is False When True, produces XPath 3.1 json-to-xml compliant output as specified by W3C (https://www.w3.org/TR/xpath-functions-31/#func-json-to-xml). Uses type-based element names (map, array, string, number, boolean, null) with key attributes and the http://www.w3.org/2005/xpath-functions namespace. Example: .. code-block:: python {"name": "John", "age": 30} results in .. code-block:: xml <map xmlns="http://www.w3.org/2005/xpath-functions"> <string key="name">John</string> <number key="age">30</number> </map> Dictionaries-keys with special char '@' has special meaning: @attrs: This allows custom xml attributes: .. code-block:: python {'@attr':{'a':'b'}, 'x':'y'} results in .. code-block:: xml <root a="b"><x>y</x></root> @flat: If a key ends with @flat (or dict contains key '@flat'), encapsulating node is omitted. Similar to item_wrap. @val: @attrs requires complex dict type. If primitive type should be used, then @val is used as key. To add custom xml-attributes on a list {'list': [4, 5, 6]}, you do this: .. code-block:: python {'list': {'@attrs': {'a':'b','c':'d'}, '@val': [4, 5, 6]} which results in .. code-block:: xml <list a="b" c="d"><item>4</item><item>5</item><item>6</item></list> """ if xpath_format: output = _XMLWriter() output.write('<?xml version="1.0" encoding="UTF-8" ?>') tag_name = get_xpath31_tag_name(obj) if tag_name in {"map", "array"}: _append_xpath31(output, obj, namespace=True) else: output.write(f'<map xmlns="{XPATH_FUNCTIONS_NS}">') _append_xpath31(output, obj) output.write("</map>") return output.to_bytes() namespace_parts: list[str] = [] if xml_namespaces is None: xml_namespaces = {} for prefix in xml_namespaces: if prefix == 'xsi': for schema_att in xml_namespaces[prefix]: if schema_att == 'schemaInstance': ns = xml_namespaces[prefix]['schemaInstance'] namespace_parts.append(f' xmlns:{prefix}="{ns}"') elif schema_att == 'schemaLocation': ns = xml_namespaces[prefix][schema_att] namespace_parts.append(f' xsi:{schema_att}="{ns}"') elif prefix == 'xmlns': # xmns needs no prefix ns = xml_namespaces[prefix] namespace_parts.append(f' xmlns="{ns}"') else: ns = xml_namespaces[prefix] namespace_parts.append(f' xmlns:{prefix}="{ns}"') namespace_str = "".join(namespace_parts) if root: custom_root, root_attr = make_valid_xml_name(custom_root, {}) output = _XMLWriter() output.write('<?xml version="1.0" encoding="UTF-8" ?>') output.write(f"<{custom_root}{make_attrstring(root_attr)}{namespace_str}>") _append_convert( output, obj, ids, attr_type, item_func, cdata, item_wrap, parent=custom_root, list_headers=list_headers, ) output.write(f"</{custom_root}>") return output.to_bytes() output = _XMLWriter() _append_convert( output, obj, ids, attr_type, item_func, cdata, item_wrap, parent="", list_headers=list_headers ) return output.to_bytes()