# SOAP with Python objects

Dot access, attributes, lists and namespaces - SOAP messages as plain Python objects with no schemas or WSDL.

Every SOAP request you send and every response you receive is a `SOAPMessage` - a dynamic Python object where attributes are XML elements. You never declare its structure upfront, you never touch a schema, and no code is ever generated from a WSDL.

```python
from zato.common.soap.message import SOAPMessage

request = SOAPMessage()
request.namespace = 'urn:cdc:iisb:2014'
request.hl7Message = 'MSH|^~\\&|...'
```

That is a complete request body. The rules below are the whole contract.

## The rules {#the-rules}

### 1\. Dot access creates elements {#1-dot-access-creates-elements}

Assigning to an attribute creates an element. Intermediate elements spring into existence as you go deeper - there is nothing to declare first:

```python
request = SOAPMessage()
request.order.customer.name = 'Jane Smith'
request.order.customer.city = 'Geneva'
```

```xml
<order>
    <customer>
        <name>Jane Smith</name>
        <city>Geneva</city>
    </customer>
</order>
```

### 2\. Assignment order is wire order {#2-assignment-order-is-wire-order}

Elements appear on the wire in exactly the order you assigned them. Nothing is reordered, which matters because many SOAP endpoints validate element order strictly - if the documentation of a system says `username` comes before `password`, assign them in that order and that is the order they will have.

### 3\. Brackets are attributes {#3-brackets-are-attributes}

Brackets set and read XML attributes:

```python
request.slot['name'] = 'creationTime'
request.slot.value = '2026-01-15T10:30:00'
```

```xml
<slot name="creationTime">
    <value>2026-01-15T10:30:00</value>
</slot>
```

One bracket key is reserved: `namespace`. Setting `message.namespace` picks the default namespace for the whole message, while setting it on a node overrides the namespace from that node down:

```python
request = SOAPMessage()
request.namespace = 'urn:example:orders'

# This one subtree uses a different namespace
request.metadata['namespace'] = 'urn:example:common'
request.metadata.created_by = 'api'
```

### 4\. Lists are repeated elements {#4-lists-are-repeated-elements}

Assigning a list produces one element per item - both for scalars and for nested `SOAPMessage` nodes:

```python
line1 = SOAPMessage()
line1.product = 'AA-100'
line1.quantity = 3

line2 = SOAPMessage()
line2.product = 'BB-200'
line2.quantity = 1

request.order.lines.line = [line1, line2]
```

```xml
<order>
    <lines>
        <line><product>AA-100</product><quantity>3</quantity></line>
        <line><product>BB-200</product><quantity>1</quantity></line>
    </lines>
</order>
```

### 5\. Python types choose the XML form {#5-python-types-choose-the-xml-form}

The type of the value dictates its lexical form on the wire - you assign natural Python values and the platform writes what the receiving system expects:

| You assign | The wire receives |
| --- | --- |
| `str` | The text as-is |
| `int`, `float`, `Decimal` | The number's canonical form |
| `bool` | `true` or `false` |
| `datetime` | An `xs:dateTime` timestamp |
| `date` | An `xs:date` value |
| `None` | An `xsi:nil` element |
| `bytes` | An MTOM part when the connection has [MTOM](https://zato.io/docs/dev/soap/attachments/index.html) enabled, inline base64 otherwise |

### 6\. Responses are read with dot access {#6-responses-are-read-with-dot-access}

Responses are `SOAPMessage` objects as well, so attributes read as they were assigned - repeated elements are lists, `xsi:nil` reads as `None`, and you never need to know or spell out the namespaces of the reply:

```python
response = conn.invoke('getOrder', request)

order = response.getOrderResponse.order
status = order.status

for line in order.lines.line:
    self.logger.info('%s x %s', line.product, line.quantity)
```

An element that has attributes is always a node - its attributes read through brackets and its text through `str()`:

```python
# <status code="42">Accepted</status>
code = response.status['code']   # '42'
text = str(response.status)      # 'Accepted'
```

### 7\. Responses include their protocol context {#7-responses-include-their-protocol-context}

Two reserved attributes are added to responses when the connection uses the matching blocks:

- `response.addressing` - the reply's [WS-Addressing](https://zato.io/docs/dev/soap/addressing/index.html) headers: `action`, `message_id`, `relates_to`
- `response.attachments` - the reply's [MTOM parts](https://zato.io/docs/dev/soap/attachments/index.html), each with `content_id`, `content_type` and `data` bytes

## Faults are exceptions {#faults-are-exceptions}

A SOAP fault of either version is reported as a single exception type, `SOAPFault`, raised before any part of the response reaches your code:

```python
from zato.common.soap.common import SOAPFault

try:
    response = conn.invoke('submitSingleMessage', request)
except SOAPFault as fault:
    self.logger.warning('Rejected: %s %s', fault.code, fault.reason)
```

`fault.code` and `fault.reason` are strings and `fault.detail` is a dot-accessed `SOAPMessage` holding whatever the endpoint put in the fault's detail section.

## Learn more {#learn-more}

- [SOAP integrations](https://zato.io/docs/dev/soap/) - Envelopes, versions and Python objects instead of XML
- [SOAP security](https://zato.io/docs/dev/soap/security/) - WS-Security, credentials in the body and client certificates
- [MTOM and SwA attachments](https://zato.io/docs/dev/soap/attachments/) - Binary content out of the envelope and into MIME parts
- [SOAP mandates](https://zato.io/docs/dev/soap/mandates/) - What each national and sector profile requires, and how to meet it
- [SOAP tutorial](https://zato.io/tutorials/soap/calling.html) - Call any SOAP service with Python objects, start to finish
