Thursday, January 6, 2011

Versioning XML and JSON Models in a CXF REST Service

Following up on my previous post around how simple it was to add JSON support to my CXF-based REST service...now I'll add versioning support to my XML-annotated model and see what happens.

Let's assume two model classes in the com.mybiz.model package, supporting a Foo and a list of Foo's:


public class Foo {
private final String name;

// must have a zero-arg ctor for JAXB
public Foo() { this(""); }

public Foo(String theName) { name = theName; }

@XmlAttribute(name = "name")
public String getName() { return name; }
}

@XmlRootElement(name = "foo-info")
@XmlSeeAlso({Foo.class})
public class FooList {
private List<Foo> foos;

// must have a zero-arg ctor for JAXB
public FooList() { }

public FooList(List<Foo> theList) { foos = theList; }

@XmlElement(name = "foo", required = false, type = Foo.class)
public List<Foo> getFoos() { return foos; }

}

Assume a REST service like so in e.g. the com.mybiz.rest package - it returns both XML and JSON, depending on the "accept" header:


@Path("/foos/")
@Produces({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON})
public interface FooListService {
@GET
FooList getFoos();
}

A "GET /foos" request (I'm ignoring deployment, webapp context, etc.) might return XML like so (I say XML, not JSON, since XML is listed first in the @Produces annotation above, and there's no header entry for accepting anything else):


<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<foo-info>
<foo name="some name goes here"/>
</foo-info>

...and the JSON will look like this (assume an "accept=application/json" header has been added):


{"foo-info":
{"foo":[
{"name":"some name goes here"}
]
}
}

These documents, however, will not allow consumers to distinguish just what version of the model they are receiving (since the model will likely change as the application evolves). To do this we add a package-info.java file to the com.mybiz.model package in which our model objects reside:


@javax.xml.bind.annotation.XmlSchema(
namespace = "http://mybiz.com/model/v1.0",
elementFormDefault = javax.xml.bind.annotation.XmlNsForm.QUALIFIED)
package com.mybiz.model;

We annotate with JAXB to imitate what's done by the JAXB compiler - this gives us an xmlns attribute in the XML documents generated by marshalling the model objects, which supports versioning those documents. Now, the XML will look like this:


<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<foo-info xmlns="http://mybiz.com/model/v1.0">
<foo name="some name goes here"/>
</foo-info>

But specifying JSON as the representation will result in this:

WARN [org.apache.cxf.jaxrs.impl.WebApplicationExceptionMapper]
WebApplicationException has been caught :
Invalid JSON namespace: http://mybiz.com/model/v1.0

To fix this, we follow the instructions (!) provided by the CXF website and configure our Spring-powered REST service as follows, effectively mapping our XML namespace to the desired JSON namespace:


<util:map id="jsonNamespaceMap" map-class="java.util.Hashtable">
<entry key="http://com.mybiz/model/v1.0" value="v1.0"/>
</util:map>
<bean id="jsonProvider" class="org.apache.cxf.jaxrs.provider.JSONProvider">
<property name="namespaceMap" ref="jsonNamespaceMap"/>
</bean>
<jaxrs:server id="restSvc" address="/">
<jaxrs:serviceBeans>
<bean class="com.mybiz.rest.FooListService"/>
</jaxrs:serviceBeans>
<jaxrs:providers>
<ref bean="jsonProvider"/>
</jaxrs:providers>
</jaxrs:server>

Now the JSON documents will look like this - the class names have "namespace prefixes" as specified, and the attributes are prefixed with the "@" character:


{"v1.0.foo-info":
{"v1.0.foo":[
{"@name":"some name goes here"}
]
}
}

You can optionally set the mapped JSON namespace to an empty string to forego the namespace prefix altogether ... but this would leave you without versioning support as your models change, which was the whole point.

No comments:

Post a Comment