tefra / xsdata

Naive XML & JSON Bindings for python
https://xsdata.readthedocs.io
MIT License
334 stars 61 forks source link

SOAP call response parsing failing with pydantic models #1093

Open mmakaay opened 3 hours ago

mmakaay commented 3 hours ago

Context

I am currently investigating if I can use xsdata for consuming a SOAP service that I have to integrate with. So far, I see a lot of good stuff. I'm using xsdata-pydantic, and am very happy with the generated schema classes. I was quickly able to build a request to send using these.

Issue

Parsing the response using the built-in Client fails. When using client.send(...), validation fails on the output message that is returned from the service:

1 validation error for Body
fault
  Field required [type=missing, input_value={'can_response': CanRespo...cel.'), request_uid='')}, input_type=dict]
    For further information visit https://errors.pydantic.dev/2.9/v/missing

It tells me that the fault field is required. The service that I am talking to however, does not define any faults for any of the operations. Here's the operation that results in the above validation error.

<wsdl:operation name="can">
    <wsdl:input message="odfs:canRequest"/>
    <wsdl:output message="odfs:canResponse"/>
</wsdl:operation>

The xsdata code generation adds the fault attribute to the expected schema.

Possible issue

When I disable the two lines of code at

https://github.com/tefra/xsdata/blob/main/xsdata/codegen/mappers/definitions.py#L219-L220

and generate fresh code, I end up with a working client that can process the response data.

Maybe, the build_envelope_fault(...) method needs something like this at its beginning, to not generate a wsdl:Fault when no faults are defined?

        if not port_type_operation.faults:
            return

When the Fault needs to be added, then possibly the issue arises because the Fault is defined as a required Field?

My current work-around

It's a bit of a kludge, but right now I am using xsdata-generated classes for building and parsing messages, while using Zeep's Client for handling the actual HTTP request. This looks somewhat like:

    # Build request data using xsdata-generated classes
    header = Header(...)   

    # Send request using Zeep.
    result = zeep_service.can(
        header=xsdata_to_dict(header),  # uses xsdata's JsonSerializer to build a dict, so Zeep can consume it
        externalOrderUid="MY-TEST-ORDER-1235",
        code="SOMETHING",
    )

    # Use Zeep's serializer to create a dict out of the response
    serialized = serialize_object(result, target_cls=dict)  

    # Using xsdata's JSON parser to map the dict to xsdata-generated classes.
    response = json_parser.decode(serialized, CanResponse)

This works (also showing that Zeep is able to handle the deserialization of the response), but it is far from ideal.

mmakaay commented 2 hours ago

Here's a way to reproduce the issue:

Create an empty directory. Inside that directory, generate classes for a simple public SOAP service:

$ mkdir test-xsdata-client
$ cd test-xsdata-client
$ xsdata --output pydantic 'https://www.dataaccess.com/webservicesserver/NumberConversion.wso?WSDL'

In the same directory, add the following testscript:

#!/usr/bin/env python3

from xsdata.formats.dataclass.client import Client, Config
from xsdata.formats.dataclass.context import XmlContext
from xsdata.formats.dataclass.parsers import XmlParser
from xsdata.formats.dataclass.serializers import XmlSerializer

from generated import *

context = XmlContext(class_type="pydantic")
serializer = XmlSerializer(context=context)
parser = XmlParser(context=context)

client = Client(
    config=Config.from_service(NumberConversionSoapTypeNumberToWords),
    parser=parser,
    serializer=serializer
)

response = client.send(
    NumberConversionSoapTypeNumberToWordsInput(
        body=NumberConversionSoapTypeNumberToWordsInput.Body(
            number_to_words=NumberToWords(ubi_num=1234)
        )
    )
)

print(response)

Actual output

When running this script, send() fails with a "fault field required" error.

Expected output

body=Body(number_to_words_response=NumberToWordsResponse(number_to_words_result='one thousand two hundred and thirty four '))
mmakaay commented 2 hours ago

After generating dataclasses instead of pydantic classes, and after switching to the default XML serializer and parser, the test code worked just fine. Thus, the issue only shows up with Pydantic.

this might suggest that the bug report should have been filed under the xsdata-pydantic repository. Leaving it in here though, since the code that adds the wsdl:Fault is part of the xsdata repository code.