Skip to content
PYD ENTERPRISES

SOAP anti-pattern

SOAP and WSDL schema coupling

Service contracts fixed in WSDL and XSD, with generated stubs on both sides, where nillable, minOccurs, xsd:choice and SOAP faults carry meaning that a quick JSON port flattens.

FAMILY
SOAP
CUTOVER RISK
Medium
Adds migration effort and contract risk, but behavior is visible in the code once you look.
MODERN MAPPING
OpenAPI 3.1 with JSON Schema 2020-12 and problem details
GENERATED ARTIFACT
openapi.paths.yaml

Before and after

legacy source → modern mapping

Legacy: exhibits the pattern

CustomerService.wsdl (types excerpt)XML
1<xsd:complexType name="UpdateContactRequest">
2 <xsd:sequence>
3 <xsd:element name="customerId" type="xsd:string"/>
4 <xsd:element name="email" type="xsd:string"
5 minOccurs="0" nillable="true"/>
6 <xsd:choice>
7 <xsd:element name="mobile" type="tns:Phone"/>
8 <xsd:element name="landline" type="tns:Phone"/>
9 </xsd:choice>
10 </xsd:sequence>
11</xsd:complexType>
12
13<wsdl:operation name="UpdateContact">
14 <wsdl:input message="tns:UpdateContactRequest"/>
15 <wsdl:output message="tns:UpdateContactResponse"/>
16 <wsdl:fault name="CustomerNotFound"
17 message="tns:CustomerNotFoundFault"/>
18</wsdl:operation>

Modern: OpenAPI 3.1 with JSON Schema 2020-12 and problem details

openapi.yamlYAML
1UpdateContactRequest:
2 type: object
3 required: [customerId]
4 properties:
5 customerId: { type: string }
6 email:
7 type: [string, 'null']
8 description: >
9 Absent: leave unchanged. null: clear the
10 address (xsi:nil in the legacy contract).
11 oneOf:
12 - required: [mobile]
13 properties: { mobile: { $ref: '#/components/schemas/Phone' } }
14 - required: [landline]
15 properties: { landline: { $ref: '#/components/schemas/Phone' } }

Why it is dangerous in modernization

SOAP services built with JAX-RPC, Axis or JAX-WS are defined by WSDL documents and XSD types, and clients are compiled from them. That makes the XSD the real contract, including details nobody thinks of as business logic: an element with minOccurs="0" may be absent, an element with nillable="true" may be present but explicitly empty, and the service may treat the two differently (absent means 'do not change', nil means 'clear the value'). xsd:choice expresses mutually exclusive alternatives. Business errors travel as typed SOAP faults declared in the WSDL. Older services use RPC/encoded style, which most modern tooling no longer supports. Teams that convert by sampling a few XML messages and writing JSON by hand lose the optionality rules, merge the alternatives into one object, and turn typed faults into a generic 500.

How PYD ENTERPRISES detects it

PYD ENTERPRISES parses WSDL 1.1 (messages, portTypes, bindings, style and use) and the XSDs it imports, and links each operation to the Java or .NET implementation through the generated service endpoint interface or annotations. Within the implementation, dataflow analysis checks how each optional element is read: whether code distinguishes null from absent (for example JAXBElement with isNil, or the specified flags generated by .NET), and which faults are thrown on which paths. The result is a contract model where each element carries its observed semantics, not only its declared type.

Maps to: OpenAPI 3.1 with JSON Schema 2020-12 and problem details

OpenAPI 3.1 aligns with JSON Schema 2020-12, so XSD constructs map cleanly: minOccurs="0" removes the property from required, nillable becomes a type array including null, xsd:choice becomes oneOf, and enumerations become enum. Each declared SOAP fault becomes an error response using the RFC 9457 problem details format with a stable type URI. Where absent and nil mean different things, the generated spec documents both cases explicitly.

Generated specification fragment

openapi.paths.yamlYAML
1/customers/{customerId}/contact:
2 patch:
3 operationId: updateContact
4 responses:
5 '200':
6 description: Contact updated
7 '404':
8 description: SOAP fault CustomerNotFound
9 content:
10 application/problem+json:
11 schema:
12 $ref: '#/components/schemas/Problem'
13 example:
14 type: /errors/customer-not-found
15 title: Customer not found
16 status: 404

Every generated element links back to the source file and line range shown in the legacy pane. Review and sign off before building against it.

Absent versus nil is a business rule

In the example, the implementation checks whether the email element was sent at all, and if it was, whether it carried xsi:nil="true". Absent leaves the stored email alone; nil deletes it. A JSON port that maps both to null either makes it impossible to clear an email or clears emails on every partial update. The engine finds this by tracing the generated binding object (a JAXBElement in Java, a property plus a specified flag in .NET) to the persistence call, and it writes the distinction into the property description and into two equivalence fixtures.

RPC/encoded services

Services using RPC style with SOAP encoding predate the WS-I Basic Profile, which requires literal use. They use multi-reference values and arrays declared with soapenc:Array, and modern stacks such as JAX-WS do not support them. PYD ENTERPRISES parses them anyway, because they are common in insurance and government integrations from the early 2000s, and maps soapenc arrays to JSON arrays with their item types resolved from the arrayType attribute.

Keeping consumers working during the move

Most programs run SOAP and REST side by side for a while. Because the OpenAPI document is generated from the same contract model as the WSDL analysis, a facade can translate between them field by field, and the generated Postman collection exercises both with identical fixtures. Consumers move when ready, and the equivalence suite shows that both interfaces still return the same business result for the same input.

Find every SOAP and WSDL schema coupling in your codebase.

Run PYD ENTERPRISES in your private VPC and get each occurrence with file, line range, modern mapping and equivalence fixtures, ready for your architects to review.