Versioning
There are 2 main considerations when versioning schemas,
- Backwards compatibility – Allowing code written to work against a new version of the schema to continue to work against older versions.
- Future compatibility – Allowing code written to work against an old version to continue working if messages constructed against a new version of the schema are passed to it.
There are broadly speaking 2 types of changes,
- minor – ones that preserve compatibility with the previous versions. (e.g., new optional elements, attributes, extensions to an enumerated list, etc.). These can allow Backwards & Future compatibility to be preserved
- major – ones that break compatibility with existing versions (these break Backwards & Future compatibility).
Minor Changes
It is only possible to provide any real form of compatibility with minor changes, so we will consider this first.
Minor changes should be optional and should appear after all exiting elements/attributes.
When designing a schema it is important to decide from what perspective you want versioning to be performed.
Case 1
You are implementing a service. The xml messages you are building describe requests/response messages to/from this service. The service will always be implemented using Liquid XML Library generated from the latest version of the messaging schema, however when you upgrade the clients may still be using older versions.
In this case you need backward compatibility. If only ‘minor’ changes have occurred between versions then if a message is received constructed according to an old version of the schema, then there will just be optional attributes/elements missing from it. This combined with the SchemaVersion implemented in the root element allow this missing data to be defaulted appropriately.
Case 2
You are implementing a client. The xml messages you are building describe requests/responses to the service. You want to build the application, but if the service is upgraded you don’t want to have to roll out the clients again.
If only minor changes have taken place to the schema, then the messages being sent from the client to the service will still be compatible, but the responses received may now contain new (unexpected attributes/elements/enumerations). There are a number of ways for the client to deal with these.
The schema can be altered (either formally or on a working copy if you don’t own the schema) in order to facilitate version proofing. If an <any> element is appended to each element. These <any>’s will form catch all’s for any unknown elements.
This can be time consuming on large schemas, and can cause the schema to become con-deterministic.
The XmlSerializationContext can be setup to ignore unknown elements and attributes. This is a simple solution, and is easy to implement especially if you don’t have control of the schema.
Major changes
When making major changes to the schema there is no way to preserve compatibility between versions, so a new version needs to be produced i.e. 1.0 to 2.0.
If you are building a client then this is not so much of an issue, you build your client to work against a specific major version of the schema, your client can cope with minor changes to the standard (see previous section), but there is no clear way to cope with major changes without re-working code or when using Liquid XML re-generating the library.
If you are building a service then you may have to support a number of versions until your clients upgrade (possibly indefinitely). At this point the strategy you have taken for versioning becomes more critical, both for simplifying your own life and your clients upgrade path.
There are a number of approaches to producing major versions.
Create a new schema
This requires a new namespace and filename/url for the schema, in order to make it distinct from the previous one. This does of course mean that if you are implementing this server side you now have 2 distinct libraries providing similar functionality, and the code behind them needs to be replicated. This approach is better suited when the schema is changing significantly, and the differences between the schemas outweigh any re-use you may get from code produced against the previous version.
Add to the existing one
The simplest way to create a new version is to add new functionality to an existing schema, but do it in such a way that it will support old and new messages. SubstitionGroups and extensions are a useful tool in accomplishing this, lets look at an example.
The following is the a first version of a schema, the CustomerDetails element can contain a Person element, which describes a person.
<?xml version="1.0" encoding="UTF-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified" attributeFormDefault="unqualified">
<xs:element name="Person">
<xs:complexType>
<xs:sequence>
<xs:element name="Name" type="xs:string"/>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="CustomerDetails">
<xs:complexType>
<xs:sequence>
<xs:element ref="Person"/>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:schema>
|
And a sample file
<?xml version="1.0" encoding="UTF-8"?>
<CustomerDetails xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="Person.xsd">
<Person>
<Name>Joe Bloggs</Name>
</Person>
</CustomerDetails>
|
Later the Person definition is found to be a little lacking, and it is decided that the forename & surname need breaking out. If we where to just remove the ‘Name’ element and add the Surname and Forename elements then this would break compatibility with the previous version.
An alternative approach is to create a substitution group or choice allowing legacy documents to be readable.
This ‘version 2’ of the schema shows how to use a choice to provide compatibility with the existing schema, while allowing a new Person2 element to be used interchangeably with it.
The choice solution is simple to implement is Person appears in a small number of places, but the substitution group approach is better if Person appears in a large number of locations.
The Choice approach also has the draw back that each instance creates a new class, which needs to be dealt with as a separate entity in the code.
<?xml version="1.0" encoding="UTF-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
elementFormDefault="qualified"
attributeFormDefault="unqualified">
<xs:element name="Person">
<xs:complexType>
<xs:sequence>
<xs:element name="Name" type="xs:string"/>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="CustomerDetails">
<xs:complexType>
<xs:choice>
<xs:element ref="Person"/>
<xs:element name="Person2" type="Person2Type"/>
</xs:choice>
</xs:complexType>
</xs:element>
<xs:complexType name="Person2Type">
<xs:sequence>
<xs:element name="Forename" type="xs:string"/>
<xs:element name="Surname" type="xs:string"/>
</xs:sequence>
</xs:complexType>
</xs:schema>
|
This ‘Version 2’ of the schema uses substitution groups to acompshish the same thing. This approach is more effort to setup, but is easier to change each instance of Person in use (simply swap it for a reference to the PersonGroup element.
The code generated for the Person substation group is also consistant, so a single function can be used to manipulate Person/Person2 elements throughout the code.
<?xml version="1.0" encoding="UTF-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
elementFormDefault="qualified"
attributeFormDefault="unqualified">
<xs:element name="CustomerDetails">
<xs:complexType>
<xs:sequence>
<xs:element ref="PersonGroup"/>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:complexType name="PersonBase" abstract="true"/>
<xs:complexType name="PersonType">
<xs:complexContent>
<xs:extension base="PersonBase">
<xs:sequence>
<xs:element name="Name" type="xs:string"/>
</xs:sequence>
</xs:extension>
</xs:complexContent>
</xs:complexType>
<xs:complexType name="Person2Type">
<xs:complexContent>
<xs:extension base="PersonBase">
<xs:sequence>
<xs:element name="Forename" type="xs:string"/>
<xs:element name="Surname" type="xs:string"/>
</xs:sequence>
</xs:extension>
</xs:complexContent>
</xs:complexType>
<xs:element name="PersonGroup" type="PersonBase" abstract="true"/>
<xs:element name="Person" type="PersonType" substitutionGroup="PersonGroup"/>
<xs:element name="Person2" type="Person2Type" substitutionGroup="PersonGroup"/>
</xs:schema>
|
This new versions of the schema will allow the previous XML sample to be read as well as the new version.
<?xml version="1.0" encoding="UTF-8"?>
<CustomerDetails xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="Person2.xsd">
<Person2>
<Forename>Joe</Forename>
<Surname>Bloggs</Surname>
</Person2>
</CustomerDetails>
|
Note
Even if you achive your goal of making your schema backwardly compatable, then you must remember that if clients come in with requests marked as version 1, they will still expect the response that conforms to Version 1.
Best practices
- Include the version in the schema itself i.e.
<xs:schema xmlns="http://www.exampleSchema"
targetNamespace="http://www.exampleSchema"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
elementFormDefault="qualified" attributeFormDefault="unqualified"
version="1.3">
<xs:element name="Example">
<xs:complexType>
….
<xs:attribute name="schemaVersion" type="xs:string" use="required"/>
</xs:complexType>
</xs:element>
- Use a convention for schema versioning to indicate whether the schema changed significantly (case 1) or was only extended (case 2). For example, for case 1 a version could increment by one (e.g., v1.0 to v2.0) whereas for case 2 a version could increment by less than one (e.g., v1.2 to v1.3).
- Be consistent, create root level complexTypes for each element, use naming conventions for complextypes (ie postfix Type)
- Avoid using the same name for complex types and element definitions.
- Avoid complex structures substitution groups, extensions etc unless you have a clear reason to use them.
This topic is also covered in the Getting Started manual.