@Path("/orders") public class OrderResource { @GET @Produces(MediaType.APPLICATION_JSON) public List<Order> getOrders() { List<Order> orders = new ArrayList<>(); orders.add(new Order(123L, new Item("Foo", 2))); //orders.add(new Order(456L, new Item("Foo", 1), new Item("Bar", 3))); return orders; } public static class Order { private long id; private List<Item> items = new ArrayList<>(); public Order(long id, Item... toAdd) { this.id = id; this.items.addAll(Arrays.asList(toAdd)); } public long getId() { return id; } public List<Item> getItems() { return Collections.unmodifiableList(items); } } public static class Item { private String name; private int quantity; public Item(String name, int quantity) { this.name = name; this.quantity = quantity; } public String getName() { return name; } public int getQuantity() { return quantity; } } }Next step is deploying this on a JEE6 application server. I used GlassFish, which internally uses Jersey as JAX-RS implementation and Jackson as JSON provider. The resource spits out the following JSON:
[ { "id":123, "items":[ { "name":"Foo", "quantity":2 } ] } ]That's pretty much what you would expect: a list of orders which are made up of an id and a list of items. Uncommenting the second order in the Java code above confirms that the structure is consistent:
[ { "id":123, "items":[ { "name":"Foo", "quantity":2 } ] }, { "id":456, "items":[ { "name":"Foo", "quantity":1 }, { "name":"Bar", "quantity":3 } ] } ]So far so good! Let's repeat this exercise and deploy the resource on CXF. The JSON with a single order now looks like this:
{ "order":[ { "id":123, "items":{ "name":"Foo", "quantity":2 } } ] }That's not quite what we expected: we've got a wrapping JSON map with a list of orders inside it labelled "order"! Furthermore, the list of items inside the order no longer appears to be a list! Note that there is are no square brackets surrounding it. Getting back the list of two orders makes things even more surprising:
{ "order":[ { "id":123, "items":{ "name":"Foo", "quantity":2 } }, { "id":456, "items":[ { "name":"Foo", "quantity":1 }, { "name":"Bar", "quantity":3 } ] } ] }Wow, the list of items is back inside the order structure! Apparently you get a list if you have multiple items, and just the single item if you just have one. That's pretty bad since the JSON structure for an order is now no longer consistent, which of course makes parsing it a headache.
The reason for all of this madness is the fact that CXF is at it's core an XML framework (notice the X in CXF). Doing JSON with CXF involves a bit of trickery. Internally, CXF will first use JAXB to marshall your objects into XML. Actually, I was cheating earlier: you can't directly deploy the OrderResource class shown above on CXF. First you'll have to add JAXB annotations, getters and setters, and other such frivolities to appease JAXB:
@XmlRootElement public class Order { private long id; private List<Item> items = new ArrayList<>(); public long getId() { return id; } public void setId(long id) { this.id = id; } public List<Item> getItems() { return items; } public void setItems(List<Item> items) { this.items = items; } }JAXB produces XML but we want JSON! To resolve this conundrum CXF uses a StAX implementation called Jettison which does not actually write XML but instead outputs JSON. Clever! The downside here is that JSON is produced from XML, not from Java objects. Consequently, Java type information is no longer available when JSON is written out. Looking at the XML produced by JAXB for an order with a single item clarifies things:
<orders> <order> <id>123</id> <items> <name>Foo</name> <quantity>2</quantity> </items> </order> </orders>Looking at this XML, you have no way of knowing that you can have multiple <items> elements. Since Java type information is no longer available, Jettison cannot see that items is actually a java.util.List and consequently it omits the list in the JSON structure:
{ "order":[ { "id":123, "items":{ "name":"Foo", "quantity":2 } } ] }If you have an order with multiple items, multiple <items> elements will be present in the XML and as a result the JSON will contain a list.
This type of translation from XML to JSON is called the mapped convention. CXF (or rather Jettison) also supports the BadgerFish convention, but it's much more esoteric.
By default CXF uses Jettison to produce JSON. Although this might be useful if you're using JAXB for other reasons, it might be better to configure CXF to use Jackson if you're just doing JAX-RS with JSON. Luckily this is easy to do.
Nice article!
ReplyDeleteJust want to add that if you use a JAXB annotated model, but you want to create JSON from it using Jersey and Jackson instead, there is a Jackson JAXB plugin which does not depend on intermediate XML.
In that case Jackson (or better the Jackson JAXB plugin) will parse the JAXB annotations and directly create JSON. This way you can avoid those problems where semantics are lost during transformation.
It is however clear, that if you only want JSON, having to use JAXB annotations is starting off on the wrong foot.
As you have explained if one simply needs to transform from Java objects to JSON, use a JSON serializer directly such as Jackson (which transforms in natural mode) as one would use for example XStream for XML.
When there is really need to support both XML and JSON it is probably better to separate the XML and JSON model altogether.
Having one model generated with the JAXB annotations for XML and another POJO model for JSON, possibly with Jackson annotations.
Generating JSON from JAXB is IMHO only advisable where you need a 1 on 1 conversion to JSON including all XML specifics such as namespaces. Using Jettison and the Badgerfish format is then probably a good option.
Koen, I fully agree.
ReplyDeleteIn my case CXF was the corporate standard for "web services", both WS-* and RESTful web services.