@@ -188,6 +188,76 @@ def default_item_func(parent: str) -> str:
188188 return "item"
189189
190190
191+ # XPath 3.1 json-to-xml conversion
192+ # Spec: https://www.w3.org/TR/xpath-functions-31/#json-to-xml-mapping
193+ XPATH_FUNCTIONS_NS = "http://www.w3.org/2005/xpath-functions"
194+
195+
196+ def get_xpath31_tag_name (val : Any ) -> str :
197+ """
198+ Determine XPath 3.1 tag name by Python type.
199+
200+ See: https://www.w3.org/TR/xpath-functions-31/#func-json-to-xml
201+
202+ Args:
203+ val: The value to get the tag name for.
204+
205+ Returns:
206+ str: The XPath 3.1 tag name (map, array, string, number, boolean, null).
207+ """
208+ if val is None :
209+ return "null"
210+ if isinstance (val , bool ):
211+ return "boolean"
212+ if isinstance (val , dict ):
213+ return "map"
214+ if isinstance (val , (int , float , numbers .Number )):
215+ return "number"
216+ if isinstance (val , Sequence ) and not isinstance (val , str ):
217+ return "array"
218+ if isinstance (val , str ):
219+ return "string"
220+ return "string"
221+
222+
223+ def convert_to_xpath31 (obj : Any , parent_key : str | None = None ) -> str :
224+ """
225+ Convert a Python object to XPath 3.1 json-to-xml format.
226+
227+ See: https://www.w3.org/TR/xpath-functions-31/#json-to-xml-mapping
228+
229+ Args:
230+ obj: The object to convert.
231+ parent_key: The key from the parent dict (used for key attribute).
232+
233+ Returns:
234+ str: XML string in XPath 3.1 format.
235+ """
236+ key_attr = f' key="{ escape_xml (parent_key )} "' if parent_key is not None else ""
237+
238+ if obj is None :
239+ return f"<null{ key_attr } />"
240+
241+ if isinstance (obj , bool ):
242+ return f"<boolean{ key_attr } >{ str (obj ).lower ()} </boolean>"
243+
244+ if isinstance (obj , (int , float , numbers .Number )):
245+ return f"<number{ key_attr } >{ obj } </number>"
246+
247+ if isinstance (obj , str ):
248+ return f"<string{ key_attr } >{ escape_xml (obj )} </string>"
249+
250+ if isinstance (obj , dict ):
251+ children = "" .join (convert_to_xpath31 (v , k ) for k , v in obj .items ())
252+ return f"<map{ key_attr } >{ children } </map>"
253+
254+ if isinstance (obj , Sequence ):
255+ children = "" .join (convert_to_xpath31 (item ) for item in obj )
256+ return f"<array{ key_attr } >{ children } </array>"
257+
258+ return f"<string{ key_attr } >{ escape_xml (str (obj ))} </string>"
259+
260+
191261def convert (
192262 obj : ELEMENT ,
193263 ids : Any ,
@@ -563,7 +633,8 @@ def dicttoxml(
563633 item_func : Callable [[str ], str ] = default_item_func ,
564634 cdata : bool = False ,
565635 xml_namespaces : dict [str , Any ] = {},
566- list_headers : bool = False
636+ list_headers : bool = False ,
637+ xpath_format : bool = False ,
567638) -> bytes :
568639 """
569640 Converts a python object into XML.
@@ -652,6 +723,28 @@ def dicttoxml(
652723 <Bike><frame_color>red</frame_color></Bike>
653724 <Bike><frame_color>green</frame_color></Bike>
654725
726+ :param bool xpath_format:
727+ Default is False
728+ When True, produces XPath 3.1 json-to-xml compliant output as specified
729+ by W3C (https://www.w3.org/TR/xpath-functions-31/#func-json-to-xml).
730+ Uses type-based element names (map, array, string, number, boolean, null)
731+ with key attributes and the http://www.w3.org/2005/xpath-functions namespace.
732+
733+ Example:
734+
735+ .. code-block:: python
736+
737+ {"name": "John", "age": 30}
738+
739+ results in
740+
741+ .. code-block:: xml
742+
743+ <map xmlns="http://www.w3.org/2005/xpath-functions">
744+ <string key="name">John</string>
745+ <number key="age">30</number>
746+ </map>
747+
655748 Dictionaries-keys with special char '@' has special meaning:
656749 @attrs: This allows custom xml attributes:
657750
@@ -681,6 +774,18 @@ def dicttoxml(
681774 <list a="b" c="d"><item>4</item><item>5</item><item>6</item></list>
682775
683776 """
777+ if xpath_format :
778+ xml_content = convert_to_xpath31 (obj )
779+ output = [
780+ '<?xml version="1.0" encoding="UTF-8" ?>' ,
781+ xml_content .replace ("<map" , f'<map xmlns="{ XPATH_FUNCTIONS_NS } "' , 1 )
782+ if xml_content .startswith ("<map" )
783+ else xml_content .replace ("<array" , f'<array xmlns="{ XPATH_FUNCTIONS_NS } "' , 1 )
784+ if xml_content .startswith ("<array" )
785+ else f'<map xmlns="{ XPATH_FUNCTIONS_NS } ">{ xml_content } </map>' ,
786+ ]
787+ return "" .join (output ).encode ("utf-8" )
788+
684789 output = []
685790 namespace_str = ""
686791 for prefix in xml_namespaces :
0 commit comments