XML

Overview

As with JSON, it’s most convenient to use Simple IO (SIO) to accept or return XML in your services but if that’s not an option it’s still very easy to use XML directly. In fact, if you choose to use lxml, you won’t even have to manipulate XML as such at all - everything will be generated for you.

First, let’s define a bare-bones service and an HTTP channel that will be used throughout the chapter. Save the following code in an xml_example.py file and hot-deploy it.

1
2
3
4
5
from zato.server.service import Service

class MyService(Service):
    def handle(self):
        pass
../_images/xml-channel1.png

Accessing request elements

  • If a channel the service is exposed over defines the data format to be XML, Zato will parse the incoming request using lxml and the service's self.request.payload object will be set to the root element of the XML using an instance of lxml.objectify.ObjectifiedElement. Hence below we can access cust_info.cust.cust_id directly as though they were regular attributes of an object.

    1
    2
    3
    4
    5
    6
    from zato.server.service import Service
    
    class MyService(Service):
        def handle(self):
            if self.request.payload.cust_info.cust.cust_id == 1:
                self.logger.info('Got it!')
    
    $ curl localhost:17010/xml-example.my-service -d '
      <root><cust_info><cust><cust_id>1</cust_id></cust></cust_info></root>'
    
    INFO - Got it!
  • If an element is optional and doesn't exist in the request, you can't simply reference it, an AttributeError will be raised. Instead, you need to use getattr() to find out if it's been provided.

    1
    2
    3
    4
    5
    6
    7
    from zato.server.service import Service
    
    class MyService(Service):
        def handle(self):
            cust_type = getattr(self.request.payload, 'cust_type', None)
            if cust_type == 'AZE':
                self.logger.info('Got it!')
    
  • You access lists of elements by iterating over their parent element using .getchildren()

    1
    2
    3
    4
    5
    6
    from zato.server.service import Service
    
    class MyService(Service):
        def handle(self):
            for elem in self.request.payload.cust_info.address_list.getchildren():
                self.logger.info(elem)
    
    $ curl localhost:17010/xml-example.my-service -d '<root><cust_info>
          <address_list>
              <address>Nerviërslaan 31 Avenue</address>
              <address>Juan de Aliaga 230</address>
          </address_list>
    </cust_info></root>'
    
    INFO - Nerviërslaan 31 Avenue
    INFO - Juan de Aliaga 230
  • Attributes are accessed using an element's .get method.

    1
    2
    3
    4
    5
    6
    7
    # Zato
    from zato.server.service import Service
    
    class MyService(Service):
        def handle(self):
            cust_id = self.request.payload.cust.get('id')
            self.logger.info('cust_id:[{}]'.format(cust_id))
    
    $ curl localhost:17010/xml-example.my-service -d '<root><cust id="1"/></root>'
    
    INFO - cust_id:[1]
  • You can also reference elements using their namespaces. Note in the example below that whole document is in the 'example.com/1' namespace but the element we want to get, relation_type, uses 'example.com/2', hence we cannot access it using plain dot notation.

    1
    2
    3
    4
    5
    6
    7
    8
    from zato.server.service import Service
    
    class MyService(Service):
        def handle(self):
            relation_type = getattr(
              self.request.payload.cust_info, '{example.com/2}relation_type')
            if relation_type == 'AAA':
                self.logger.info('Got it!')
    
    $ curl localhost:17010/xml-example.my-service -d '
    <root xmlns="example.com/1"><cust_info>
       <relation_type xmlns="example.com/2">AAA</relation_type>
    </cust_info></root>'
    
    INFO - Got it!
  • The raw, unparsed, XML document is available under self.request.raw_request. This comes in handy if you want to parse it using your own tools of choice. Note however that in this case you shouldn't set the channel's data format to XML so that Zato doesn't parse it unnecessarily.

    1
    2
    3
    4
    5
    from zato.server.service import Service
    
    class MyService(Service):
        def handle(self):
            self.logger.info('XML:[{}]'.format(self.request.raw_request))
    
    $ curl localhost:17010/xml-example.my-service -d '<root><cust_id>1</cust_id></root>'
    
    INFO - XML:[<root><cust_id>1</cust_id></root>]

Producing response

  • You just need to assign a unicode string representing an XML document to self.response.payload and it's completely up to you how the string is procured.

    Using lxml is one easy way:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    # lxml
    from lxml import etree
    from lxml import objectify
    
    # Zato
    from zato.server.service import Service
    
    class MyService(Service):
        def handle(self):
            cust_data = etree.Element('cust_data')
            phone_number = objectify.SubElement(cust_data, 'phone_number')
            phone_number.text = '123-456-789'
    
            self.response.payload = etree.tostring(cust_data)
    
    $ curl localhost:17010/xml-example.my-service -d ''
    <cust_data><phone_number>123-456-789</phone_number></cust_data>
    
  • It doesn't matter how the string is obtained, it can even be assigned directly.

    1
    2
    3
    4
    5
    from zato.server.service import Service
    
    class MyService(Service):
        def handle(self):
            self.response.payload = '<customer><id>1</id></customer>'
    
    $ curl localhost:17010/xml-example.my-service -d ''
    <customer><id>1</id></customer>
    

SOAP support

You can receive and create SOAP messages with plain HTTP channels using the data format of XML just fine but Zato goes farther and offers a special way to expose services over SOAP that simplifies your job even more.

You need to create SOAP channels instead of plain HTTP ones.

../_images/xml-soap-channel1.png

Now your service will receive the first child of /soapenv:Envelope/soapenv:Body in self.request.payload and just like with plain XML above, you need to assign a unicode string value to self.response.payload. This time however, the payload will be automatically wrapped in a SOAP envelope for you.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# -*- coding: utf-8 -*-

# lxml
from lxml import etree

# Zato
from zato.server.service import Service

class MyService(Service):
    def handle(self):
        cust_id = getattr(self.request.payload, '{example.com/1}cust_id')
        self.logger.info('cust_id:[{}]'.format(cust_id))
        self.response.payload = '<cust xmlns="example.com/1"><name>楊朱</name></cust>'
$ curl localhost:17010/soap-example.my-service \
    -H 'SOAPAction:soap-example.my-service' -d '
 <soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
     xmlns:ns="example.com/1">
  <soapenv:Body>
   <ns:root>
    <ns:cust_id>1</ns:cust_id>
   </ns:root>
  </soapenv:Body>
 </soapenv:Envelope>'

 <?xml version='1.0' encoding='UTF-8'?>
 <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
     xmlns="https://zato.io/ns/20130518">
  <soap:Body>
   <cust xmlns="example.com/1">
    <name>楊朱</name>
   </cust>
  </soap:Body>
 </soap:Envelope>
INFO - cust_id:[1]

Simple IO (SIO)

Note

Consider using SIO if you're developing a new application or its existing data model allows you to, SIO is even more easier to work with.