2025, Dec 07 11:00

Handle unknown SOAP response fields not in the WSDL: prevent suds 'Type not found' with a safe MessagePlugin

Learn how to prevent suds SOAP unmarshalling failures from fields not in the WSDL. Use a MessagePlugin to strip unknown elements and keep calls stable.

When a SOAP backend starts returning fields that aren’t defined in the WSDL, suds can choke during unmarshalling. With suds v1.1.1, a response that includes an unexpected element produced a “Type not found: ‘accountNumber’” error even with faults disabled and flags like allowUnknownMessageParts turned on. Below is a reproducible shape of the issue and a pragmatic way to make the client resilient without changing the service.

Minimal setup that still fails

The initialization looks straightforward, yet it still throws on unknown elements in the response payload:

from suds.client import Client

wsdl_uri = "path_or_url_to_wsdl"
soap_client = Client(wsdl_uri, faults=False)
soap_client.set_options(allowUnknownMessageParts=True, extraArgumentErrors=False)

The server replies with a valid SOAP envelope that includes a payment method carrying a field the WSDL doesn’t know about. The error reported by the library is:

Type not found: ‘accountNumber’

And the relevant XML shape looks like this:

<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
  <soap:Body>
    <ns2:getTransactionResponse xmlns:ns2="http://soap.api.controller.web.payjar.com/">
      <return>
        <basket>
          <amountInCents>500</amountInCents>
          <currencyCode>ZAR</currencyCode>
          <description>ABC Retailer</description>
        </basket>
        <displayMessage>Successful</displayMessage>
        <merchantReference>114ff7d7-3a6d-44e1-bff7-dfjkheuiier789</merchantReference>
        <payUReference>dfskjskjdfhjkksk</payUReference>
        <paymentMethodsUsed xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="ns2:capitecPay">
          <accountNumber>12347383643</accountNumber>
          <amountInCents>500</amountInCents>
          <beneficiaryStatementDescription>CAPITEC1748237482597</beneficiaryStatementDescription>
          <transactionId>a8f64064-39f2-11f0-9fde-ejnsduinsiuefnf</transactionId>
        </paymentMethodsUsed>
        <resultCode>00</resultCode>
        <resultMessage>Successful</resultMessage>
        <successful>true</successful>
        <transactionState>SUCCESSFUL</transactionState>
        <transactionType>PAYMENT</transactionType>
      </return>
    </ns2:getTransactionResponse>
  </soap:Body>
</soap:Envelope>

What’s actually going wrong

The response includes a payment method typed as capitecPay that contains an accountNumber element. That field isn’t part of the WSDL schema. During unmarshalling, suds tries to map response nodes to the schema, sees an element it doesn’t know about, and raises the “Type not found” exception. The usual toggles like faults=False and allowUnknownMessageParts=True don’t shield this particular mismatch.

A targeted workaround with a suds MessagePlugin

The most reliable way to keep the client alive is to intercept the unmarshalling step and normalize the reply before suds validates it. The plugin below trims unknown children based on the schema model, so the client only sees nodes it actually understands.

from suds.client import Client
from suds.plugin import MessagePlugin

class StripUnknownPartsHook(MessagePlugin):
    def unmarshalled(self, ctx):
        if hasattr(ctx.reply, 'children'):
            valid = [node for node in ctx.reply.children 
                     if node.name in ctx.reply.__metadata__.sxtype.rawchildren]
            ctx.reply.children = valid

wsdl_uri = "path_or_url_to_wsdl"
soap_client = Client(wsdl_uri, plugins=[StripUnknownPartsHook()], faults=False)

For the specific case with paymentMethodsUsed where the server injects a type and fields that aren’t declared in the WSDL, it helps to ensure these items don’t fail validation. The following plugin preserves the original behavior above and also tolerates unknown items within paymentMethodsUsed by attaching compatible metadata to them if needed.

from suds.client import Client
from suds.plugin import MessagePlugin

class PayUPayloadNormalizerHook(MessagePlugin):
    def unmarshalled(self, ctx):
        if hasattr(ctx.reply, 'paymentMethodsUsed'):
            for pm in ctx.reply.paymentMethodsUsed:
                if not hasattr(pm, '__metadata__'):
                    pm.__metadata__ = ctx.reply.__metadata__
        if hasattr(ctx.reply, 'children'):
            keep = [n for n in ctx.reply.children 
                    if n.name in ctx.reply.__metadata__.sxtype.rawchildren]
            ctx.reply.children = keep
        return ctx.reply

wsdl_uri = "path_or_url_to_wsdl"
soap_client = Client(
    wsdl_uri,
    plugins=[PayUPayloadNormalizerHook()],
    faults=False,
    allowUnknownMessageParts=True
)

Why this matters

Schema drift between a WSDL and production responses can appear at any time. If your client strictly enforces the contract, even a harmless additional field can take down a call path. By sanitizing or normalizing the incoming payload at the unmarshalling boundary, you keep the integration stable while upstream evolves. When the WSDL gets updated to include the new fields, the workaround becomes unnecessary.

Practical notes and takeaways

If you initialize the client early and later call set_options again while adding headers or other settings, you may override previous flags or lose plugin configuration inadvertently. Make sure the final client instance still carries allowUnknownMessageParts and the plugin you rely on.

When a server introduces a new element like accountNumber under a type such as capitecPay that isn’t present in the WSDL, the plugin approach above allows you to ignore the unknown field and proceed with the fields your client expects.

For long-term hygiene, keep an eye on the WSDL and align it with the server payloads. Until that happens, a focused MessagePlugin is an effective safety net.