Skip to main content

Make minor backward-compatible changes to your Web services

Web services versioning

Russell Butek (butek@us.ibm.com), Web Services Consultant, IBM, Software Group
Russell Butek is an IBM Web Services Consultant. He was one of the developers of the IBM WebSphere Web services engine. He is also a member the JAX-RPC Java Specification Request (JSR) expert group. He was involved in the implementation of Apache's AXIS SOAP engine, driving AXIS 1.0 to comply with JAX-RPC 1.0. Previously, he was a developer of the IBM CORBA ORB and an IBM representative on a number of OMG task forces, including chairing the portable interceptor task force.

Summary:  Web services versioning doesn't really exist. To achieve new versions of a service, you have to create a new set of WSDL/XSD files with new namespaces, essentially creating a new Web service. That's a rather drastic solution. There are some changes you can make to an existing set of WSDL and XSD files that are backward compatible so that you can evolve your service, to a limited degree, without the drastic measure of creating a new one.

Date:  30 Nov 2004
Level:  Intermediate
Activity:  2368 views

The WSDL 1.1 specification (see Resources) does not address the issue of Web services versioning; that is, how to deal with situations when the interface for your service changes with new versions. When people ask me about Web services versioning, my short answer is always, "There isn't any." That's not a very helpful answer, however, and there are things you can do, but you have to be aware that you are on your own when it comes to Web services versioning. You will be developing hand-crafted solutions.

It is not necessarily clear what a version of an API really is. The general solution for versioning a Web service is to create an entirely new Web service with new namespaces in new files. This is a rather drastic solution. For a discussion of this solution, and a discussion of what is meant by versioning, read the article entitled "Best practices for Web services versioning" (see Resources). I don't address those topics here; instead, I focus on the set of changes which you can make to an existing service so that it remains backward compatible with older clients.

There are all sorts of changes you can make to your service. Unfortunately, most of these are major changes that require you to create a whole new Web service. There is a set of minor changes that you can make to your service without taking the major leap of creating a new Web service. To be minor, a change must be backward compatible, meaning that old clients can still talk with your new Web service. There are two types of backward-compatible changes: changes to service input and changes to service output.

Backward-compatible changes to service input

A backward-compatible change to service input is one in which a new Web service can continue to receive data from an old client. These changes include the addition of the following:

  • New operations
  • New data types
  • New optional fields to existing data types
  • Type expansion

New operations

Simply adding a new operation is one rather common reason for updating a Web service. Luckily, this is a minor change. If you add a new operation to a service without removing any of the old operations, and without affecting the existing behavior, clients of the old version of the API will continue to work with the new service.

New data types

You can add new types that are used by new methods. As long as you haven't changed or removed any of the old types, a client of the old version will continue to work with the new service.

You can also add extensions of existing types, but be careful. Adding an extension type is a minor change only if it is recieved as input to the new service. The new service cannot return a new extension type. If an old client were to receive a new extension type, the client deserialization would fail.

New, optional fields in an existing data type

You can add an element to an existing complexType as long as you make it optional (using the minOccurs="0" attribute). But be careful. Adding an optional element is a minor change only if its enclosing complexType is received as input to the new service. The new service cannot return a complexType with new fields. If an old client were to receive the new field, the client deserialization would fail because the client would not know about the new field.

You can also add new attributes to a complexType in the same manner as new elements, as long as the attribute's schema has the attribute use="optional".

Type expansion

You can expand the value space for a data type that your service receives. In other words, if the set of possible values of the new type is a superset of that of the old type, then you have expanded the type. A client sending the old type to a new service will still function.

Note that this will not work with an RPC/encoded service. Since type information is sent in an RPC/encoded message, the type cannot change. It only works with Literal services which only send the value. When the value true is received, for instance, you cannot know, just by looking at it, whether it is a Boolean or a String; the service dispatcher has to get involved to identify the value's exact type.

This also only works with data types received by the new service, not sent from the service. If you tried sending a value of an expanded type that is outside the bounds of the old type's value space to a client which is not aware of that new type, the client will fail to deserialize that type.

Type expansion is a little riskier than the previous backward-compatible choices. The others are all additions; this one is a change. A change of an API often also involves a change of the underlying implementation's behavior. Be wary! When you are doing minor changes, you do not want to change the existing behavior. You must preserve the semantics of the values. As long as the old behavior continues to exist with the old values, then type expansion is safe. But if the behavior of the implementation receiving old values changes, than this is not a backward-compatible change.


Backward-compatible changes to service output

A backward-compatible change to service output is one in which a new Web service can continue to send data to an old client.

I can only think of one backward-compatible change to output: type restriction. Your server can restrict the type of a field sent to an older client. The old client will still expect the expanded set of values for the type, so it won't really care that you are now restricting the values, but be careful: type restriction is a minor change only when you are sending data to the client. Do not restrict a type that the service receives from a client. An old client may send a value that is outside the new, restricted value space.


Example

Start with the basic service in Listing 1. It provides an API for adding to an address book. I will go through some change scenarios which will provide examples for each of the minor version changes listed above.


Listing 1. Add address book service version 1.0
<?xml version="1.0" ?>
<definitions targetNamespace="urn:add.addressBook/1.0"
             xmlns:tns="urn:add.addressBook/1.0"
             xmlns:xsd="http://www.w3.org/2001/XMLSchema"
             xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
             xmlns="http://schemas.xmlsoap.org/wsdl/">
  <types>
    <schema targetNamespace="urn:addressBook/1.0"
            xmlns="http://www.w3.org/2001/XMLSchema"
            xmlns:tns="urn:addressBook/1.0">

      <complexType name="phone">
        <sequence>
            <element name="areaCode" type="int"/>
            <element name="exchange" type="int"/>
            <element name="number" type="int"/>
        </sequence>
      </complexType>

      <complexType name="address">
        <sequence>
            <element name="streetNum" type="int"/>
            <element name="streetName" type="string"/>
            <element name="city" type="string"/>
            <element name="state" type="string"/>
            <element name="zip" type="int"/>
            <element name="phoneNumber" type="tns:phone"/>
        </sequence>
      </complexType>
    </schema>
    <schema targetNamespace="urn:add.addressBook/1.0"
            xmlns="http://www.w3.org/2001/XMLSchema"
            xmlns:tns="urn:add.addressBook/1.0"
            xmlns:addressbook="urn:addressBook/1.0">
      <element name="addAddress" type="tns:addAddress"/>
      <complexType name="addAddress">
        <sequence>
          <element name="name" type="string"/>
          <element name="address" type="addressbook:address"/>
        </sequence>
      </complexType>
      <element name="addAddressResponse" type="tns:addAddressResponse"/>
      <complexType name="addAddressResponse">
        <sequence>
          <element name="returnCode" type="string"/>
        </sequence>
      </complexType>
    </schema>
  </types>

  <message name="AddAddressRequest">
    <part name="parameters" element="tns:addAddress"/>
  </message>
  <message name="AddAddressResponse">
    <part name="parameters" element="tns:addAddressResponse"/>
  </message>

  <portType name="AddressBook">
    <operation name="addAddress">
      <input message="tns:AddAddressRequest"/>
      <output message="tns:AddAddressResponse"/>
    </operation>
  </portType>

  <!-- binding declns -->
  <binding name="AddressBookSOAPBinding" type="tns:AddressBook">
    <soap:binding style="document"
                  transport="http://schemas.xmlsoap.org/soap/http"/>
    <operation name="addEntry">
      <soap:operation soapAction=""/>
      <input>
        <soap:body use="literal"/>
      </input>
      <output>
        <soap:body use="literal"/>
      </output>
    </operation>
    <operation name="addAddress">
      <soap:operation soapAction=""/>
      <input>
        <soap:body use="literal"/>
      </input>
      <output>
        <soap:body use="literal"/>
      </output>
    </operation>
  </binding>

  <service name="AddressBookService">
    <port name="AddressBook" binding="tns:AddressBookSOAPBinding">
      <soap:address location="http://localhost:9080/AddressBook/services/Add"/>
    </port>
  </service>
</definitions>

New operations

Let's assume a number of your clients want to know how many address are stored in your address book. They suggest a count operation. You don't have to change any existing behavior with such a simple operation, so you agree to add it as a minor change to your API. See Listing 2.


Listing 2. New operation: count
  <types>
    <schema ...>
      <element name="count" type="tns:count"/>
      <complexType name="count">
        <sequence/>
      </complexType>
      <element name="countResponse" type="tns:countResponse"/>
      <complexType name="countResponse">
        <sequence>
          <element name="count" type="int"/>
        </sequence>
      </complexType>
    </schema>
  </types>

  ...

  <message name="CountRequest">
    <part name="parameters" element="tns:count"/>
  </message>
  <message name="CountResponse">
    <part name="parameters" element="tns:countResponse"/>
  </message>

  <portType name="AddressBook">
    ...
    <operation name="count">
      <input message="tns:CountRequest"/>
      <output message="tns:CountResponse"/>
    </operation>
  </portType>

  ...

New data types

You realize that your phone data type is inadequate. You have a number of business clients and many of them have phone numbers with extensions. So you extend the phone type with a businessPhone type (see Listing 3). This new data type is only input to this service, never output, so old clients will continue to work with the old phone, and new clients, which send the new businessPhone, will work because your new service will accept those new types.


Listing 3. New data type: businessPhone
      <complexType name="businessPhone">
        <complexContent>
          <extension base="tns:phone">
            <sequence>
              <element name="extension" type="string"/>
            </sequence>
          </extension>
        </complexContent>
      </complexType>

New, optional fields in an existing data type

You realize that your address data type is inadequate because it doesn't account for apartment addresses. So you add a new, optional, apptNum element to phone (see Listing 4). You made this element optional for backward compatibility, but you'll notice that the optionality of this field is a good idea anyway, because not all addresses have apartment numbers.

You might have noticed that you could either add apptNum as an optional field to address, or you could extend address with an apptAddress subtype and add the new apptNum field there. Both approaches accomplish essentially the same thing: adding an optional field. So why choose one over the other?

  • Optional fields produce smaller SOAP messages. When you send an extension type, the type information is sent in the SOAP message. When you send an optional field, you simply send that optional field (or you don't).
  • Your design can sometimes dictate which choice you make; for example, extending base types with subtypes is clearly a more object-oriented approach.

Listing 4. New, optional field in an existing data type: apptNum
      <complexType name="address">
        <sequence>
          <element name="apptNum" type="int" minOccurs="0"/>
          <element name="streetNum" type="int"/>
          <element name="streetName" type="string"/>
          <element name="city" type="string"/>
          <element name="state" type="string"/>
          <element name="zip" type="int"/>
          <element name="phoneNumber" type="tns:phone"/>
        </sequence>
      </complexType>

Type expansion

A number of your clients, for marketing reasons, use letters in their phone numbers rather than simply numbers. It's an easy matter to expand the phone types from ints to strings, although it will be a bit more work for your service to validate the incoming strings. But it's a minor API change, so you agree to do it. When old clients send ints, the integer characters are still characters, and they can be sent as strings as well as ints. See Listing 5.


Listing 5. Type expansion: phone numbers
      <complexType name="phone">
        <sequence>
          <element name="areaCode" type="string"/>
          <element name="exchange" type="string"/>
          <element name="number" type="string"/>
        </sequence>
      </complexType>

Type restriction

You decide that a string for a resultCode is a bit too open. Since the string can be just about anything, your clients aren't really sure what to do with it. You've told them, verbally, that OK would be returned on a successful call, but you're starting to get clients off the internet that you have never given verbal instructions to, so how do they even know when the result is okay? So you think hard and long about all the possible resultCodes you can have, and you create a string enumeration for the resultCode (see Listing 6). (OK, so this is a rather trivial example. You would really probably have sent a fault back when the operation failed, but I wanted to keep this example simple.)


Listing 6. Type restriction: resultCode
      <simpleType name="returnCode">
        <restriction base="string">
          <enumeration value="OK"/>
          <enumeration value="We already have an entry for that name."/>
          <enumeration value="Address Book is full."/>
          <enumeration value="Some other failure."/>
        </restriction>
      </simpleType>

      ...

      <complexType name="addAddressResponse">
        <sequence>
          <element name="returnCode" type="tns:returnCode"/>
        </sequence>
      </complexType>

All of these backward-compatible changes have been lumped together into the WSDL in Listing 7. This WSDL is used to build the new version of your service. It doesn't matter whether your clients use this new WSDL version or the WSDL version from Listing 1; they will all work with your new service because you were careful to make only backward-compatible changes.


Listing 7. Complete add address book service, version 1.1
<?xml version="1.0" ?>
<definitions targetNamespace="urn:Add.AddressBook/1.0"
             xmlns:tns="urn:Add.AddressBook/1.0"
             xmlns:xsd="http://www.w3.org/2001/XMLSchema"
             xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
             xmlns="http://schemas.xmlsoap.org/wsdl/">
  <types>
    <schema targetNamespace="urn:AddressBook/1.0"
            xmlns="http://www.w3.org/2001/XMLSchema"
            xmlns:tns="urn:AddressBook/1.0">

      <complexType name="phone">
        <sequence>
          <element name="areaCode" type="string"/>
          <element name="exchange" type="string"/>
          <element name="number" type="string"/>
        </sequence>
      </complexType>

      <complexType name="businessPhone">
        <complexContent>
          <extension base="tns:phone">
            <sequence>
              <element name="extension" type="string"/>
            </sequence>
          </extension>
        </complexContent>
      </complexType>

      <complexType name="address">
        <sequence>
          <element name="apptNum" type="int" minOccurs="0"/>
          <element name="streetNum" type="int"/>
          <element name="streetName" type="string"/>
          <element name="city" type="string"/>
          <element name="state" type="string"/>
          <element name="zip" type="int"/>
          <element name="phoneNumber" type="tns:phone"/>
        </sequence>
      </complexType>
    </schema>
    <schema targetNamespace="urn:Add.AddressBook/1.0"
            xmlns="http://www.w3.org/2001/XMLSchema"
            xmlns:tns="urn:Add.AddressBook/1.0"
            xmlns:addressbook="urn:AddressBook/1.0">

      <simpleType name="returnCode">
        <restriction base="string">
          <enumeration value="OK"/>
          <enumeration value="We already have an entry for that name."/>
          <enumeration value="Address Book is full."/>
          <enumeration value="Some other failure."/>
        </restriction>
      </simpleType>

      <element name="addAddress" type="tns:addAddress"/>
      <complexType name="addAddress">
        <sequence>
          <element name="name" type="string"/>
          <element name="address" type="addressbook:address"/>
        </sequence>
      </complexType>
      <element name="addAddressResponse" type="tns:addAddressResponse"/>
      <complexType name="addAddressResponse">
        <sequence>
          <element name="returnCode" type="tns:returnCode"/>
        </sequence>
      </complexType>
      <element name="count" type="tns:count"/>
      <complexType name="count">
        <sequence/>
      </complexType>
      <element name="countResponse" type="tns:countResponse"/>
      <complexType name="countResponse">
        <sequence>
          <element name="count" type="int"/>
        </sequence>
      </complexType>
    </schema>
  </types>

  <message name="AddAddressRequest">
    <part name="parameters" element="tns:addAddress"/>
  </message>
  <message name="AddAddressResponse">
    <part name="parameters" element="tns:addAddressResponse"/>
  </message>
  <message name="CountRequest">
    <part name="parameters" element="tns:count"/>
  </message>
  <message name="CountResponse">
    <part name="parameters" element="tns:countResponse"/>
  </message>

  <portType name="AddressBook">
    <operation name="addAddress">
      <input message="tns:AddAddressRequest"/>
      <output message="tns:AddAddressResponse"/>
    </operation>
    <operation name="count">
      <input message="tns:CountRequest"/>
      <output message="tns:CountResponse"/>
    </operation>
  </portType>

  <binding name="AddressBookSOAPBinding" type="tns:AddressBook">
    <soap:binding style="document"
                  transport="http://schemas.xmlsoap.org/soap/http"/>
    <operation name="addAddress">
      <soap:operation soapAction=""/>
      <input>
        <soap:body use="literal"/>
      </input>
      <output>
        <soap:body use="literal"/>
      </output>
    </operation>
    <operation name="count">
      <soap:operation soapAction=""/>
      <input>
        <soap:body use="literal"/>
      </input>
      <output>
        <soap:body use="literal"/>
      </output>
    </operation>
  </binding>

  <service name="AddressBookService">
    <port name="AddressBook" binding="tns:AddressBookSOAPBinding">
      <soap:address location="http://localhost:9080/AddressBook/services/Add"/>
    </port>
  </service>
</definitions>


New client, old service?

Up to this point, I have only discussed a new Web service and an old client. Though far less likely, it is possible that you might want new clients talking to old Web services. Will the list of backward-compatible changes apply? In a way, yes. The possible backward-compatible changes are now the input and output to the client, not to the server, which can be a little strange to think about. Let's consider each of them. Keep in mind that your new client doesn't know whether it's talking to a new or an old service.

  • New operations? No. A client cannot receive operations, only call them, so you can't add new operations. An old service will not be able to receive them.
  • New data types? In a way. The new types would have to be input to the client. Since you cannot have new operations, the only new types that can be inputs to the client are extensions to existing types which are only returned to the client. A new service could return the new extension. An old service would only send the old types.
  • New optional fields? Yes. Remember that this will only work for complexTypes returned to the client. Old services would never return the new fields, though new services might.
  • Type expansion? Yes. An old service returning a type to a client would restrict its value space to the old type, which will continue to work within the value space of the new type.
  • Type restriction? Yes. Remember that this only works as output from the client. So the only types you can restrict are types the client sends to the service.

Designing for versioning

You might want to consider versioning from the very beginning of your service design. If so, there are a number of options you have.

Dividing input from output

If you think about the service example in this article, it might seem rather silly. What's the point of an address book if all you can do is add to it? But there is a reason I created such a simple-minded service (other than the obvious reason of brevity). If I had included both add and query functions in one service, then all of the changes we made to the data types would no longer be minor changes. They would affect not only input to the service, but output from the service. None of the minor changes I presented can be changes to both input and output. So there is a good reason that this service is only an add service. Presumably there is another service that does the query into this address book. Note that versioning might not be the only reason for dividing input from output when designing your services. For example, a query service is often simpler and requires less quality of service (for example, security or transaction reliability) than an update service.

Dividing a service into input and output services can be tricky. Presumably both the input and the output services use the same data types. If you want to change a data type without a major change, however, you can only change the input service. If you change a data type in the output service, it is a major change. So to avoid a major change, the output service implementation will have to convert from the new data types to the old data types before returning them.

One option to ease this issue for the output service might be to have an explicit version parameter on the operations or an explicit version field on the complexTypes. A client of the output service would tell the service which minor version of the data type they understand, so that the service can send them the data types appropriate to them. But now you are really in the create-your-own area of version control.

Filler fields

You might be able to avoid minor changes altogether. If you think your future changes will only be additions, you could simple add filler fields to your design right from the beginning. These fields will have no semantics, but they will take up space (even if they're optional, they still logically exist) so your clients will know about them from day one. xsd:string is a good catch-all type for these extra fields. Even if you decide in some future version that the type should have been an integer or a boolean, for instance, you can always tell your new clients what to expect and leave it to them to do the proper conversion. (It's much more difficult, say, to start with an xsd:int filler field and decide later that it should have been a string.)

Even though strings are an adequate filler type, they're not great. They cannot handle complex types very well. So you might decide that your filler fields should be xsd:anyType or xsd:any. Be very careful with any types, however. They're not as useful as they may seem. The obvious shortcoming of any types is that they typically map to barely usable types in various language mappings. In JAX-RPC, for instance, xsd:any maps to javax.xml.soap.SOAPElement; and xsd:anyType is undefined, though often it maps to java.lang.Object. But even worse, just because the API says you can send anything does not mean you can really send anything. The other side must be able to receive what you're sending to it. Frequently that means that the other side must have type code mappings and serializers and deserializers for the type, especially for instances sent through xsd:anyType. If you're sending a new type in your xsd:anyType field to an old receiver, that receiver likely won't be able to deserialize that type into a language-specific object. xsd:any, at least in the Java mapping of JAX-RPC, is a little more flexible than xsd:anyType since it maps to SOAPElement, which means that the type remains in a "native" XML format and is not deserialized into a language-specific object. But SOAPElement is still not the friendliest of APIs.

One final comment about filler fields. How do you know how much is enough? You don't. So an added safety net, if using filler fields, is to make them optional (using minOccurs="0", for instance) and to make them open-ended (using maxOccurs="unbounded").


Summary

Web services versioning barely exists today. Each version is essentially a new Web service with new namespaces. But if you make minor, backward-compatible changes to your service as described in this paper, being careful not to change behavior, then old clients can still talk with your new service. You might even want to design your service from the very beginning to make any future, minor changes, easier.


Resources

About the author

Russell Butek is an IBM Web Services Consultant. He was one of the developers of the IBM WebSphere Web services engine. He is also a member the JAX-RPC Java Specification Request (JSR) expert group. He was involved in the implementation of Apache's AXIS SOAP engine, driving AXIS 1.0 to comply with JAX-RPC 1.0. Previously, he was a developer of the IBM CORBA ORB and an IBM representative on a number of OMG task forces, including chairing the portable interceptor task force.

Comments (Undergoing maintenance)



Trademarks  |  My developerWorks terms and conditions

Help: Update or add to My dW interests

What's this?

This little timesaver lets you update your My developerWorks profile with just one click! The general subject of this content (AIX and UNIX, Information Management, Lotus, Rational, Tivoli, WebSphere, Java, Linux, Open source, SOA and Web services, Web development, or XML) will be added to the interests section of your profile, if it's not there already. You only need to be logged in to My developerWorks.

And what's the point of adding your interests to your profile? That's how you find other users with the same interests as yours, and see what they're reading and contributing to the community. Your interests also help us recommend relevant developerWorks content to you.

View your My developerWorks profile

Return from help

Help: Remove from My dW interests

What's this?

Removing this interest does not alter your profile, but rather removes this piece of content from a list of all content for which you've indicated interest. In a future enhancement to My developerWorks, you'll be able to see a record of that content.

View your My developerWorks profile

Return from help

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=1
Zone=SOA and Web services, XML
ArticleID=31937
ArticleTitle=Make minor backward-compatible changes to your Web services
publish-date=11302004
author1-email=butek@us.ibm.com
author1-email-cc=

My developerWorks community

Tags

Help
Use the search field to find all types of content in My developerWorks with that tag.

Use the slider bar to see more or fewer tags.

Popular tags shows the top tags for this particular content zone (for example, Java technology, Linux, WebSphere).

My tags shows your tags for this particular content zone (for example, Java technology, Linux, WebSphere).

Use the search field to find all types of content in My developerWorks with that tag. Popular tags shows the top tags for this particular content zone (for example, Java technology, Linux, WebSphere). My tags shows your tags for this particular content zone (for example, Java technology, Linux, WebSphere).

Special offers