Jersey(1.19.1) - JSON Support

时间:2024-03-26 13:07:14

Jersey JSON support comes as a set of JAX-RS MessageBodyReader<T> and MessageBodyWriter<T> providers distributed with jersey-json module. These providers enable using three basic approaches when working with JSON format:

  • POJO support
  • JAXB based JSON support
  • Low-level, JSONObject/JSONArray based JSON support

The first method is pretty generic and allows you to map any Java Object to JSON and vice versa. The other two approaches limit you in Java types your resource methods could produce and/or consume. JAXB based approach could be taken if you want to utilize certain JAXB features. The last, low-level, approach gives you the best fine-grained control over the outcoming JSON data format.

POJO support

POJO suppport represents the easiest way to convert your Java Objects to JSON and back. It is based on the Jackson library.

To use this approach, you will need to turn the JSONConfiguration.FEATURE_POJO_MAPPING feature on. This could be done in web.xml using the following servlet init parameter:

<init-param>
<param-name>com.sun.jersey.api.json.POJOMappingFeature</param-name>
<param-value>true</param-value>
</init-param>

The following snippet shows how to use the POJO mapping feature on the client side:

ClientConfig clientConfig = new DefaultClientConfig();
clientConfig.getFeatures().put(JSONConfiguration.FEATURE_POJO_MAPPING, Boolean.TRUE);
Client client = Client.create(clientConfig);

Jackson JSON processor could be futher controlled via providing custom Jackson ObjectMapper instance. This could be handy if you need to redefine the default Jackson behaviour and to fine-tune how your JSON data structures look like. Detailed description of all Jackson features is out of scope of this guide. The example bellow gives you a hint on how to wire your ObjectMapper instance into your Jersey application.

Download jacksonjsonprovider-1.19.1-project.zip to get a complete example using POJO based JSON support.

JAXB based JSON support

Taking this approach will save you a lot of time, if you want to easily produce/consume both JSON and XML data format. Because even then you will still be able to use a unified Java model. Another advantage is simplicity of working with such a model, as JAXB leverages annotated POJOs and these could be handled as simple Java beans.

A disadvantage of JAXB based approach could be if you need to work with a very specific JSON format. Then it could be difficult to find a proper way to get such a format produced and consumed. This is a reason why a lot of configuration options are provided, so that you can control how things get serialized out and deserialized back.

Following is a very simple example of how a JAXB bean could look like.

@XmlRootElement
public class MyJaxbBean {
public String name;
public int age; public MyJaxbBean() {} // JAXB needs this public MyJaxbBean(String name, int age) {
this.name = name;
this.age = age;
}
}

Using the above JAXB bean for producing JSON data format from you resource method, is then as simple as:

@GET @Produces("application/json")
public MyJaxbBean getMyBean() {
return new MyJaxbBean("Agamemnon", 32);
}

Notice, that JSON specific mime type is specified in @Produces annotation, and the method returns an instance of MyJaxbBean, which JAXB is able to process. Resulting JSON in this case would look like:

{"name":"Agamemnon", "age":"32"}

Configuration Options

JAXB itself enables you to control output JSON format to certain extent. Specifically renaming and ommiting items is easy to do directly using JAXB annotations. E.g. the following example depicts changes in the above mentioned MyJaxbBean that will result in {"king":"Agamemnon"} JSON output.

@XmlRootElement
public class MyJaxbBean { @XmlElement(name="king")
public String name; @XmlTransient
public int age; // several lines removed
}

To achieve more important JSON format changes, you will need to configure Jersey JSON procesor itself. Various configuration options could be set on an JSONConfiguration instance. The instance could be then further used to create a JSONConfigurated JSONJAXBContext, which serves as a main configuration point in this area. To pass your specialized JSONJAXBContext to Jersey, you will finally need to implement a JAXBContext ContextResolver<T>:

@Provider
public class JAXBContextResolver implements ContextResolver<JAXBContext> { private JAXBContext context;
private Class[] types = {MyJaxbBean.class}; public JAXBContextResolver() throws Exception {
this.context =
new JSONJAXBContext(                  // Creation of our specialized JAXBContext
JSONConfiguration.natural().build(), types); // Final JSON format is given by this JSONConfiguration instance
} public JAXBContext getContext(Class<?> objectType) {
for (Class type : types) {
if (type == objectType) {
return context;
}
}
return null;
}
}

JSON Notations

JSONConfiguration allows you to use four various JSON notations. Each of these notations serializes JSON in a different way. Following is a list of supported notations:

  • MAPPED (default notation)
  • NATURAL
  • JETTISON_MAPPED
  • BADGERFISH

Individual notations and their further configuration options are described bellow. Rather then explaining rules for mapping XML constructs into JSON, the notations will be described using a simple example. Following are JAXB beans, which will be used.

@XmlRootElement
public class Address {
public String street;
public String town; public Address() {
} public Address(String street, String town) {
this.street = street;
this.town = town;
}
} @XmlRootElement
public class Contact { public int id;
public String name;
public List<Address> addresses; public Contact() {
}; public Contact(int id, String name, List<Address> addresses) {
this.name = name;
this.id = id;
this.addresses = (addresses != null) ? new LinkedList<Address>(addresses) : null;
}
}

Following text will be mainly working with a contact bean initialized with:

final Address[] addresses = {new Address("Long Street 1", "Short Village")};
Contact contact = new Contact(2, "Bob", Arrays.asList(addresses));

I.e. contact bean with id=2name="Bob" containing a single address (street="Long Street 1"town="Short Village").

All bellow described configuration options are documented also in apidocs at JSONConfiguration Java Doc.

Mapped notation

JSONConfiguration based on mapped notation could be build with

JSONConfiguration.mapped().build()

for usage in a JAXBContext resolver above. Then a contact bean initialized that will be serialized as

{
"id": "2",
"name": "Bob",
"addresses": {"street": "Long Street 1", "town": "Short Village"}
}

The JSON representation seems fine, and will be working flawlessly with Java based Jersey client API.

However, at least one issue might appear once you start using it with a JavaScript based client. The information, that addresses item represents an array, is being lost for every single element array. If you added another address bean to the contact,

contact.addresses.add(new Address("Short Street 1000", "Long Village"));

, you would get

{
"id": "2",
"name": "Bob",
"addresses": [
{"street": "Long Street 1", "town": "Short Village"},
{"street": "Short Street 1000", "town": "Long Village"}
]
}

Both representations are correct, but you will not be able to consume them using a single JavaScript client, because to access "Short Village" value, you will write addresses.town in one case and addresses[0].town in the other. To fix this issue, you need to instruct the JSON processor, what items need to be treated as arrays by setting an optional property, arrays, on your JSONConfiguration object. For our case, you would do it with

JSONConfiguration.mapped().arrays("addresses").build()

You can use multiple string values in the arrays method call, in case you are dealing with more than one array item in your beans. Similar mechanism (one or more argument values) applies also for all below desribed options.

Another issue might be, that number value, 2, for id item gets written as a string, "2". To avoid this, you can use another optional property on JSONConfiguration called nonStrings.

JSONConfiguration.mapped().arrays("addresses").nonStrings("id").build()

It might happen you use XML attributes in your JAXB beans. In mapped JSON notation, these attribute names are prefixed with @ character. If id was an attribute, it's definition would look like:

...
@XmlAttribute
public int id;
...

and then you would get

{"@id":"2" ...

at the JSON output. In case, you want to get rid of the @ prefix, you can take advantage of another configuration option of JSONConfiguration, called attributeAsElement. Usage is similar to previous options.

JSONConfiguration.mapped().attributeAsElement("id").build()

Mapped JSON notation was designed to produce the simplest possible JSON expression out of JAXB beans. While in XML, you must always have a root tag to start a XML document with, there is no such a constraint in JSON. If you wanted to be strict, you might have wanted to keep a XML root tag equivalent generated in your JSON. If that is the case, another configuration option is available for you, which is called rootUnwrapping. You can use it as follows:

JSONConfiguration.mapped().rootUnwrapping(false).build()

and get the following JSON for our Contact bean:

{
"contact": {
"id": "2",
"name": "Bob",
"addresses": {"street": "Long Street 1", "town": "Short Village"}
}
}

rootUnwrapping option is set to true by default. You should switch it to false if you use inheritance at your JAXB beans. Then JAXB might try to encode type information into root element names, and by stripping these elements off, you could break unmarshalling.

In version 1.1.1-ea, XML namespace support was added to the MAPPED JSON notation. There is of course no such thing as XML namespaces in JSON, but when working from JAXB, XML infoset is used as an intermediary format. And then when various XML namespaces are used, ceratin information related to the concrete namespaces is needed even in JSON data, so that the JSON procesor could correctly unmarshal JSON to XML and JAXB. To make it short, the XML namespace support means, you should be able to use the very same JAXB beans for XML and JSON even if XML namespaces are involved.

Map<String, String> ns2json = new HashMap<String, String>();
ns2json.put("http://example.com", "example");
context = new JSONJAXBContext(JSONConfiguration.mapped().xml2JsonNs(ns2json).build(), types);

Dot character (.) will be used by default as a namespace separator in the JSON identifiers. E.g. for the above mentioned example namespace and tag T"example.T" JSON identifier will be generated. To change this default behaviour, you can use the nsSeparator method on the mapped JSONConfiguration builder: JSONConfiguration.mapped().xml2JsonNs(ns2json).nsSeparator(':').build(). Then you will get "example:T" instead of "example.T" generated. This option should be used carefully, as the Jersey framework does not even try to check conflicts between the user selected separator character and the tag and/or namespace names.

Natural notation

After using mapped JSON notation for a while, it was apparent, that a need to configure all the various things manually could be a bit problematic. To avoid the manual work, a new, natural, JSON notation was introduced in Jersey version 1.0.2. With natural notation, Jersey will automatically figure out how individual items need to be processed, so that you do not need to do any kind of manual configuration. Java arrays and lists are mapped into JSON arrays, even for single-element cases. Java numbers and booleans are correctly mapped into JSON numbers and booleans, and you do not need to bother with XML attributes, as in JSON, they keep the original names. So without any additional configuration, just using

JSONConfiguration.natural().build()

for configuring your JAXBContext, you will get the following JSON for the bean initialized above:

{
"id": 2,
"name": "Bob",
"addresses": [
{"street": "Long Street 1", "town": "Short Village"}
]
}

You might notice, that the single element array addresses remains an array, and also the non-string id value is not limited with double quotes, as natural notation automatically detects these things.

To support cases, when you use inheritance for your JAXB beans, an option was introduced to the natural JSON configuration builder to forbid XML root element stripping. The option looks pretty same as at the default mapped notation case

JSONConfiguration.natural().rootUnwrapping(false).build()

Jettison mapped notation

Next two notations are based on project Jettison. You might want to use one of these notations, when working with more complex XML documents. Namely when you deal with multiple XML namespaces in your JAXB beans.

Jettison based mapped notation could be configured using:

JSONConfiguration.mappedJettison().build()

If nothing else is configured, you will get similar JSON output as for the default, mapped, notation:

{
"contact": {
"id": 2,
"name": "Bob",
"addresses": {"street": "Long Street 1", "town": "Short Village"}
}
}

The only difference is, your numbers and booleans will not be converted into strings, but you have no option for forcing arrays remain arrays in single-element case. Also the JSON object, representing XML root tag is being produced.

If you need to deal with various XML namespaces, however, you will find Jettison mapped notation pretty useful. Lets define a particular namespace for id item:

...
@XmlElement(namespace="http://example.com")
public int id;
...

Then you simply confgure a mapping from XML namespace into JSON prefix as follows:

Map<String, String> ns2json = new HashMap<String, String>();
ns2json.put("http://example.com", "example");
context = new JSONJAXBContext(JSONConfiguration.mappedJettison().xml2JsonNs(ns2json).build(), types);

Resulting JSON will look like in the example bellow.

{
"contact": {
"example.id": 2,
"name": "Bob",
"addresses": {"street": "LongStreet1", "town": "ShortVillage"}
}
}

Please note, that id item became example.id based on the XML namespace mapping. If you have more XML namespaces in your XML, you will need to configure appropriate mapping for all of them.

Badgerfish notation

Badgerfish notation is the other notation based on Jettison. From JSON and JavaScript perspective, this notation is definitely the worst readable one. You will probably not want to use it, unless you need to make sure your JAXB beans could be flawlessly written and read back to and from JSON, without bothering with any formatting configuration, namespaces, etc.

JSONConfiguration instance using badgerfish notation could be built with

JSONConfiguration.badgerFish().build()

and the output JSON will be as follows.

{
"contact": {
"id": {"$": "2"},
"name": {"$": "Bob"},
"addresses": {
"street": {"$": "Long Street 1"},
"town": {"$": "Short Village"}
}
}
}

Examples

Download json-from-jaxb-1.19.1-project.zip to get a more complex example using JAXB based JSON support.

Low-level, JSONObject/JSONArray based JSON support

Using this approach means you will be using JSONObject and/or JSONArray classes for your data representations. These classes are actually taken from Jettison project, but conform to the description provided at http://www.json.org/java/index.html.

The biggest advantage here is, that you will gain full control over the JSON format produced and consumed. On the other hand, dealing with your data model objects will probably be a bit more complex, than when taking the JAXB based approach. Differencies are depicted at the following code snipets.

MyJaxbBean myBean = new MyJaxbBean("Agamemnon", 32);

Above you construct a simple JAXB bean, which could be written in JSON as {"name":"Agamemnon", "age":32}

Now to build an equivalent JSONObject (in terms of resulting JSON expression), you would need several more lines of code.

JSONObject myObject = new JSONObject();
myObject.JSONObject myObject = new JSONObject();
try {
myObject.put("name", "Agamemnon");
myObject.put("age", 32);
} catch (JSONException ex) {
LOGGER.log(Level.SEVERE, "Error ...", ex);
}

Examples

Download bookmark-1.19.1-project.zip to get a more complex example using low-level JSON support.