Friday, September 5, 2008

WSDL-First and Schemas: Use Global Types or Global Elements?

Often I'm faced with needing to expose a Web Service where the request and response bodies are messages, based on a well established shape that is not just the concern of the specific Web Service. The message received by the Web Service is likely to flow through other parts of the internal system 'behind' the service endpoint. The Schemas for these messages are not specific to just the Web Service WSDL but are also used by other internal components which have to deal with XML. Often these Schemas are pre-defined across an organisation or by third-parties (eg. the ARTS IXRetail schema for the Retail industry).

With these sorts of scenarios, a “WSDL first” approach is usually required for creating or generating Web Service end-points, where the WSDL identifies the shape of the service request and response bodies, but does not directly define this shape. Instead, the WSDL imports the externally held Schemas and references entities from the Schema to declare the format of the top-level nodes for a SOAP message body.

With this in mind, when designing Schemas that will be used by WSDLs and other XML components, I've been trying to work out whether top-level schema entities should be defined as Global Types or as Global Elements.

So I tried a little experiment, using a simple home-made schema to represent a Customer, including Name, Date of Birth, Addresses and Telephone Numbers. I created a SOAP Web Service which enables a Customer's personal information to be retrieved by providing the customer's name as the search criteria in the SOAP request.

First I tried using a Global Type in my Customer schema (Customer.xsd) to represent the shape of the top level request XML (CustomerSearchCrtieria) and response XML (Customer), as follows:
<schema ....>
<complexType name="CustomerSearchCriteria">
<sequence>
<element name="FirstName" type="tns:FirstName"/>
<element name="LastName" type="tns:LastName"/>
</sequence>
</complexType>

<complexType name="Customer">
<sequence>
<element name="FirstName" type="tns:FirstName"/>
<element name="LastName" type="tns:LastName"/>
<element name="DateOfBirth" type="tns:DateOfBirth"/>
<element name="Address" type="tns:Address" minOccurs="0" maxOccurs="unbounded"/>
<element name="Telephone" type="tns:TelephoneNumber" minOccurs="0" maxOccurs="unbounded"/>
</sequence>
</complexType>
....
In the WSDL, I defined the request and response message parts to use the two Global Types from the imported schemas, as follows:
<wsdl:definitions name="CustomerInfoService" ... xmlns:cust="http://www.example.org/Customer">
<wsdl:types>
<xsd:schema>
<xsd:import namespace="http://www.example.org/Customer" schemaLocation="./Customer.xsd"/>
</xsd:schema>
</wsdl:types>

<wsdl:message name="GetCustomerInfoRequest">
<wsdl:part name="MyCustomerSearchCriteria" type="cust:CustomerSearchCriteria"/>
</wsdl:message>

<wsdl:message name="GetCustomerInfoResponse">
<wsdl:part name="MyCustomer" type="cust:Customer"/>
</wsdl:message>
....
Then when I fired a SOAP request into the service using my test harness, the SOAP request contained the following (notice no namespace for top level element in the body):
<soapenv:Envelope ....>
<soap:Header ..../>
<soapenv:Body>
<MyCustomerSearchCriteria xmlns:cus="http://www.example.org/Customer">
<cus:FirstName>Paul</cus:FirstName>
<cus:LastName>Done</cus:LastName>
</MyCustomerSearchCriteria>
</soapenv:Body>
</soapenv:Envelope>
The SOAP response contained the following (notice no namespace for top level element in the body):
<soapenv:Envelope ....>
<soap:Header ..../>
<soapenv:Body>
<MyCustomer xmlns:cus="http://www.example.org/Customer">
<cus:FirstName>Paul</cus:FirstName>
<cus:LastName>Done</cus:LastName>
<cus:DateOfBirth>1973-12-12</cus:DateOfBirth>
<cus:Address>
<cus:HouseNumber>1</cus:HouseNumber>
<cus:Street>High Street</cus:Street>
<cus:Town>Big Town</cus:Town>
<cus:Postcode>BT1 1AZ</cus:Postcode>
</cus:Address>
<cus:Telephone>
<cus:AreaCode>0111</cus:AreaCode>
<cus:LocalNumber>1234561</cus:LocalNumber>
</cus:Telephone>
</MyCustomer>
</soapenv:Body>
</soapenv:Envelope>
As you can see, the resulting top level XML element inside the SOAP body for both the request and the response is NOT part of the same namespace at the rest of the message XML. This is because the top-level element has actually been defined as an element by the WSDL, rather than being defined externally in the schema itself. To me this is less than ideal because I believe most people would want the whole message to have the same namespace.

In addition I believe most people would want the top level nodes to have an element name mandated by the Schema, not by the WSDL (eg. the top level element should be forced to be 'Customer' and not 'MyCustomer').

So I then tried using a Global Element instead, in my Customer schema (Customer.xsd) to represent the shape of the top level request XML (CustomerSearchCrtieria) and response XML (Customer), as follows:
<schema ...>
<element name="CustomerSearchCriteria" type="tns:CustomerSearchCriteria"/>

<element name="Customer" type="tns:Customer"/>
....
In the WSDL, I defined the request and response message parts to use the two Global Elements from the schemas, as follows:
<wsdl:definitions name="CustomerInfoService" .... xmlns:cust="http://www.example.org/Customer">
<wsdl:types>
<xsd:schema>
<xsd:import namespace="http://www.example.org/Customer" schemaLocation="./Customer.xsd"/>
</xsd:schema>
</wsdl:types>

<wsdl:message name="GetCustomerInfoRequest">
<wsdl:part element="cust:CustomerSearchCriteria" name="parameters"/>
</wsdl:message>

<wsdl:message name="GetCustomerInfoResponse">
<wsdl:part element="cust:Customer" name="parameters"/>
</wsdl:message>
....
Then when I fired a SOAP request into the service using my test harness, the SOAP request contained the following:
<soapenv:Envelope ....>
<soap:Header ..../>
<soapenv:Body>
<cus:CustomerSearchCriteria xmlns:cus="http://www.example.org/Customer">
<cus:FirstName>Paul</cus:FirstName>
<cus:LastName>Done</cus:LastName>
</cus:CustomerSearchCriteria>
</soapenv:Body>
</soapenv:Envelope>
The SOAP response contained the following:
<soapenv:Envelope ....>
<soap:Header ..../>
<soapenv:Body>
<cus:Customer xmlns:cus="http://www.example.org/Customer">
<cus:FirstName>Paul</cus:FirstName>
<cus:LastName>Done</cus:LastName>
<cus:DateOfBirth>1973-12-12</cus:DateOfBirth>
<cus:Address>
<cus:HouseNumber>1</cus:HouseNumber>
<cus:Street>High Street</cus:Street&
<cus:Town>Big Town</cus:Town>
<cus:Postcode>BT1 1AZ</cus:Postcode>
</cus:Address>
<cus:Telephone>
<cus:AreaCode>0111</cus:AreaCode>
<cus:LocalNumber>1234561</cus:LocalNumber>
</cus:Telephone>
</cus:Customer>
</soapenv:Body>
</soapenv:Envelope>
As you can now see, all of the elements in the XML message, contained within the request and response SOAP bodies, now have the same namespace and the top level element name is defined by the Schema rather than by the WSDL. This is much more desirable in my view.

In summary, when dealing with Web Services in a “WSDL-first” scenario and using externally defined schemas, I would recommend defining Global Elements in the Schema and then referencing these elements from a WSDL, rather than using Global Types. This enables the complete message to share the same namespace and enables all elements (and their names), in the message, to be completely defined and enforced by the Schema.


Note: I am not necessarily making a recommendation to use Global Elements over Global Types in all circumstances, rather, just in situations where a Schema could possibly be used directly by a SOAP Web Service description file (WSDL).in addition to other parts of a system.


Soundtrack for today: Gouge Away by Pixies