RESTful Service
CustomerService
Normally a real service will be backed by JPA to do persistence operations (see: Creating a RESTful Web Service - Part 4/5). But for this post I will use a "Hello World" style service that returns a Customer based on an ID as XML and JSON to illustrate some points about binding.
package org.example.service; import javax.ejb.*; import javax.ws.rs.*; import javax.ws.rs.core.MediaType; import org.example.model.*; @Stateless @LocalBean @Path("/customers") public class CustomerService { @GET @Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON }) @Path("{id}") public Customer read(@PathParam("id") int id) { Customer customer = new Customer(); customer.setId(id); customer.setName("Jane Doe"); PhoneNumber pn = new PhoneNumber(); pn.setType("work"); pn.setValue("5551111"); customer.getPhoneNumbers().add(pn); return customer; } }
CustomerApplication
I have used an Application class to specify the path for the service.
package org.example.service; import javax.ws.rs.ApplicationPath; import javax.ws.rs.core.Application; @ApplicationPath("rest/*") public class CustomerApplication extends Application { }
Java Model
Below is the Java model we will use for this example. The goal is to produce good XML and JSON representations using a single set of metadata.
Customer
- The id property is an int. Since JSON has a different representation for numbers and text we will look at how this value gets represented in the JSON output.
- The phoneNumbers property is of type List<PhoneNumber>. It has been annotated with @XmlElementWrapper to produce good XML output. We will examine the impact of doing this on the JSON output.
package org.example.model; import java.util.*; import javax.xml.bind.annotation.*; @XmlRootElement public class Customer { private int id; private String name; private List<PhoneNumber> phoneNumbers = new ArrayList<PhoneNumber>(); public int getId() { return id; } public void setId(int id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } @XmlElementWrapper @XmlElement(name="phoneNumber") public List<PhoneNumber> getPhoneNumbers() { return phoneNumbers; } }
PhoneNumber
package org.example.model; import javax.xml.bind.annotation.*; public class PhoneNumber { private String type; private String value; @XmlAttribute public String getType() { return type; } public void setType(String type) { this.type = type; } @XmlValue public String getValue() { return value; } public void setValue(String value) { this.value = value; } }
Using the Service
Below is the URL we will use to access our service:
http://localhost:8080/CustomerResource/rest/customers/1
Response (application/xml)
Since we mapped our domain model with JAXB (JSR-222) metadata the XML representation below isn't much of a surprise.
Response (application/json) - GlassFish 3.1.2
Lets take a look at what the JSON representation looked like back in GlassFish 3.1.2 before MOXy was the default JSON-binding provider. There are several things to take note of:
Response (application/json) - GlassFish 4
Since we mapped our domain model with JAXB (JSR-222) metadata the XML representation below isn't much of a surprise.
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <customer id="1"> <name>Jane Doe</name> <phoneNumbers> <phoneNumber type="work">5551111</phoneNumber> </phoneNumbers> </customer>
Response (application/json) - GlassFish 3.1.2
Lets take a look at what the JSON representation looked like back in GlassFish 3.1.2 before MOXy was the default JSON-binding provider. There are several things to take note of:
- The int value for the id property is represented as JSON text.
- The phoneNumbers property of type List<PhoneNumber>
since it had a single item was represented as a JSON object instead of a JSON array. Also even when this key contains a collection it is still called phoneNumber. - The JSON key corresponding to the type property since it was annotated with @XmlAttribute is prefixed with @.
- The JSON key corresponding to the value property since it was annotated with @XmlValue is called $.
{ "id": "1", "name": "Jane Doe", "phoneNumbers": { "phoneNumber": { "@type": "work", "$": "5551111" } } }
Response (application/json) - GlassFish 4
Using the default configuration you will see that upgrading to GlassFish 4 eliminates the two biggest problems:
- Numeric Values - Now the id property is correctly marshalled to JSON as a numeric value. This isn't a trick, MOXy bases the JSON representation on the Java type of the property. The value property of type String on the instance of PhoneNumber contained only digits and it is correctly marshalled to JSON as text.
- Collections of Size 1 - The phoneNumber key is now a JSON array, unfortunately it is still called phoneNumber (I'm demonstrate how to fix this next).
{ "id": 1, "name": "Jane Doe", "phoneNumbers": { "phoneNumber": [ "@type": "work", "$": "5551111" ] } }
Customizing the JSON-Binding
MOXy can now be configured by leveraging the JAX-RS ContextResolver mechanism. You simply need to return an instance of MOXyJsonConfig. We will use it to do the following:
- Specify that we don't want to prefix the JSON keys that correspond to properties mapped with @XmlAttribute.
- Use the JSON key value instead of $ for properties mapped with @XmlValue.
- MOXyJsonConfig can also be used to pass properties down to the Marshaller/Unmarshaller. We will do this to clean up the JSON key for collection properties (see: Binding to JSON & XML - Handling Collections).
package org.example.service; import javax.ws.rs.ext.*; import org.eclipse.persistence.jaxb.JAXBContextProperties; import org.glassfish.jersey.moxy.json.MoxyJsonConfig; @Provider public class MOXyJsonContextResolver implements ContextResolver<MoxyJsonConfig> { private final MoxyJsonConfig config; public MOXyJsonContextResolver() { config = new MoxyJsonConfig() .setAttributePrefix("") .setValueWrapper("value") .property(JAXBContextProperties.JSON_WRAPPER_AS_ARRAY_NAME, true); } @Override public MoxyJsonConfig getContext(Class<?> objectType) { return config; } }
New Response
Finally we have a JSON representation that we can be happy with. It does not contain any XML related artifacts, even though it was produced with the same metadata as the XML representation (which hasn't changed). All it took was a little MOXy.
{ "id": 1, "name": "Jane Doe", "phoneNumbers": [ { "type": "work", "value": "5551111" } ] }
Further Reading
If you enjoyed this post then you may also be interested in:
- Jersey Documentation: Section 8.1.2 - MOXy
- EclipseLink 2.5 Release Available for Download
- MOXy as your JAX-RS JSON Provider - MOXyJsonProvider
- Introducing EclipseLink JPA-RS
- GlassFish 3.1.2 is Full of MOXy (EclipseLink JAXB)
Great info! Thanks for keeping up-to-date examples of the newest specifications!
ReplyDeleteHow do i get following json response (glassfish 4 jre7)
ReplyDelete{"customer":{"id":1,"name":"Jane Doe","phoneNumbers":[{"type":"work","value":"5551111"}]}}
I would expect the extra "customer": part because of the @XmlRootElement
found it just need to add to MOXyJsonContextResolver config
ReplyDelete.setIncludeRoot(true)
Moxy @2.5.1-SNAPSHOT is still not supporting arrays which work without problems with Jackson:
ReplyDelete@GET
@Produces(MediaType.APPLICATION_JSON)
@Path("/user/{id}")
public double[] getEmployee(@PathParam("id") String id) {
return new double[]{ 1.0, 1.1, 1.2 };
}
This gives an internal server error on Grizzly/Jersey-2.2. Is there a workaround?
Thank you for reporting the issue. In EclipseLink 2.5.2 I have modified the isReadable/isWriteable check to return false for this type of array. I have opened the following bug to add the proper behaviour:
Delete- http://bugs.eclipse.org/417725