Converting Class Object to XML/Dict/JSON string
Type conversion in programming languages is one of the crucial things that we need from time to time. One such case happened to me. I was looking for a way to convert my class objects to an XML string with the same level of hierarchy. The goal was to simply export class objects with their members and subtypes to XML. Following is the use case for which the python code is provided.
Let's assume we have a few classes that inherit each other in the following fashion.
class Address:
street = "street"
post_code = "12345"
city = "city"
country = "country"
class Company:
name = "company name"
address = None # type: Address
class Person:
first_name = "John"
last_name = "Doe"
address = None # type: Address
work = None # type: Company
In the above example, we want to export the class Person to an XML string. The following XML string is expected.
<Person first_name="John" last_name="Doe"
<Address street="street" post-code="12345" city="city" country="country">
</Address>
<company name="company name">
<Address street="street" post-code="12345" city="city" country="country">
</Address>
</company>
</Person>>
Or in JSON
{
"Person": {
"first_name": "John",
"last_name": "Doe",
"Address": {
"street": "street",
"post-code": "12345",
"city": "city",
"country": "country"
},
"company": {
"name": "company name"
"Address": {
"street": "street",
"post-code": "12345",
"city": "city",
"country": "country"
}
}
}
}
I have chosen recursion to achieve this. The following is the code snippet to convert a class object into the desired output format.
import loggin
import json
from typing import Any, List, Dict
from xml.etree import ElementTree as et
LOG = logging.getLogger(__name__)
""" All classes must be drived from the following base class.
"""
class XmlElement:
"""Base Class"""
_NAME = None # Name of the Class that you want to appear in the XML.
_TEXT = None # Text description of the class if available.
def get_members(obj: object) -> List[str]:
""" Helper function
Get class members other than functions and private members.
"""
members = []
for attr in dir(obj):
if not callable(getattr(obj, attr)) and \
not attr.startswith("_") and \
not attr.startswith("__"):
members.append(attr)
return members
def to_xml(obj: Any) -> Any:
""" Convert a class object and its members to XML.
Each class member is treated as a tag to the current XML-element.
Each member object is treated as a new sub-element.
Each 'list' member is treated as a new list tag.
"""
if isinstance(obj, dict):
raise Exception("Dictionary type is not supported.")
root = None
tags = {}
subelements = {} # type: Dict[Any, Any]
for member in get_members(obj):
item = getattr(obj, member)
member = member.replace('_', '-')
# if object is None, add empty tag
if item is None:
subelements[member] = et.Element(member)
else:
if not isinstance(item, (str, XmlElement, list, set, tuple)):
raise Exception("Attributes must be an expected type, but was: {}".format(type(item)))
# Add list sub-elements
if isinstance(item, (list, set, tuple)):
subelements[member] = []
for list_object in item:
subelements[member].append(to_xml(list_object))
# Add sub-element
elif isinstance(item, XmlElement):
subelements[member] = to_xml(item)
# Add element's tag name
else:
tags[member] = item
try:
if obj._NAME:
root = et.Element(obj._NAME, tags)
else:
raise Exception("Name attribute can't be empty.")
except (AttributeError, TypeError) as ex:
print("Attribute value or type is wrong. %s: %s", obj, ex)
raise
# Add sub elements if any
if subelements:
for name, values in subelements.items():
if isinstance(values, list): # if list of elements. Add all sub-elements
sub = et.SubElement(root, name)
for value in values:
sub.append(value)
else: # single sub-child or None
if values is None: # if None, add empty tag with name
sub = et.SubElement(root, name)
else: # else add object
root.append(values)
try:
if obj._TEXT:
root.text = obj._TEXT
except AttributeError as ex:
print("Attribute does not exists. %s: %s", obj, ex)
raise
return root
def object_to_xml(obj: Any) -> Any:
""" Convert the given class object to xml document str.
"""
return et.tostring(element=to_xml(obj), encoding="UTF-8")
g
Explanation: All classes like Person, Address, Company, and if others must be derived from XmlElement class. This gives the to_xml function the ability to recognize if there is an XML element to be added.
The function get_members is a helper function to retrieve elements of the class that are to be converted to children or as an attribute. The function of the class is not exported to XML string same is the case with the private members. Note, dictionaries are not yet supported. I will add support to that later.
In order to convert a class object to XML, a function call is made to the object_to_xml.
Finally, the function to_xml is where all the magic is happening. First class's public members are retrieved and '_' is replaced with '-'. Later, based on the member typer either the recursive function is called or the members are added to the XML tag. The list of children is accumulated and it is added against the parent NAME tag. This process is repeated until we go deeper into the final sub-member of the class. Finally, the root element is returned to object_to_xml where it is exported as a string with UTF-8 encoding.
For exporting to JSON string, we first export our object to the dictionary and then this dictionary is converted to JSON string. Again, the rules for dictionary creation are the same as for XML.
Recommended by LinkedIn
def to_dict(obj: Any) -> Any
"""Convert class object and its members to dictionary.
Each class member is treated as an element to the current dictionary field.
Each member object is treated as a sub dictionary.
Each List[Any] is treated as a new list of dictionaries.
"""
if isinstance(obj, dict):
raise Exception("Dictionary type is not supported.")
data = {}
tags = {}
subelements = {} # type: Dict[Any, Any]
for member in get_members(obj):
item = getattr(obj, member)
member = member.replace('_', '-')
# if object is None, add empty tag
if item is None:
subelements[member] = {member:None}
else:
if not isinstance(item, (str, XmlElement, list, set, tuple)):
raise Exception("Attributes must be an expected type, but was: {}.".format(type(item)))
# Add list sub-elements
if isinstance(item, (list, set, tuple)):
subelements[member] = []
for list_object in item:
subelements[member].append(to_dict(list_object))
# Add sub-element
elif isinstance(item, XmlElement):
subelements[member] = to_dict(item)
# Add element's tag name
else:
tags[member] = item
try:
if obj._NAME:
data.update(tags)
else:
raise Exception("Name attribute can't be empty.")
except (AttributeError, TypeError) as ex:
print("Attribute value or type is wrong. %s: %s", obj, ex)
raise
# Add sub elements if any
if subelements:
for name, values in subelements.items():
sub = {} # type: Dict
if isinstance(values, list): # if list of elements. Add all sub-elements
sub[name] = []
for value in values:
sub[name].append(value)
else: # single sub-child or None
if values is None: # if None, add empty tag with name
sub[name] = None
else: # else add object
sub[name] = values
data[name] = sub[name]
try:
if obj._TEXT:
data[obj._NAME] = obj._TEXT
except AttributeError as ex:
print("Attribute does not exists. %s: %s", obj, ex)
raise
return data
def dict_to_json(data: dict) -> str:
return json.dumps(data, sort_keys=True, indent=4):
I am working on a serialization involving dicts and cannot seem to modify it to handle dicts, can any get any ideas on how I can do that.
it's amazing I've used it to solve an exporting issue. Thanks a lot for sharing it.