June 12, 2013

MOXy is the New Default JSON-Binding Provider in GlassFish 4

GlassFish 4 is now available offering the complete Java EE 7 (JSR-342) platform.  EclipseLink made some major contributions to this release.  The first is providing the JPA 2.1 (JSR-338) implementation.  The second which I'll cover in this post is EclipseLink MOXy is now the default JSON-binding provider for JAX-RS applications.

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

There are a couple of interesting things to note about the Customer class.
  1. 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.
  2. 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 

In XML the PhoneNumber class maps to a complex type with simple content (see: JAXB and Complex Types with Simple Content).  This means it maps to an XML element with attributes and text.  These XML concepts do not correspond directly to JSON concepts so we will examine the JSON representation.

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

Request - GET

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.

<?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:
  1. The int value for the id property is represented as JSON text.
  2. 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.
  3. The JSON key corresponding to the type property since it was annotated with @XmlAttribute is prefixed with @.
  4. 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:  
  1. 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.
  2. 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:
  1. Specify that we don't want to prefix the JSON keys that correspond to properties mapped with @XmlAttribute.
  2. Use the JSON key value instead of $ for properties mapped with @XmlValue.
  3. 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:

5 comments:

  1. Great info! Thanks for keeping up-to-date examples of the newest specifications!

    ReplyDelete
  2. How do i get following json response (glassfish 4 jre7)

    {"customer":{"id":1,"name":"Jane Doe","phoneNumbers":[{"type":"work","value":"5551111"}]}}

    I would expect the extra "customer": part because of the @XmlRootElement

    ReplyDelete
  3. found it just need to add to MOXyJsonContextResolver config
    .setIncludeRoot(true)

    ReplyDelete
  4. Moxy @2.5.1-SNAPSHOT is still not supporting arrays which work without problems with Jackson:

    @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?

    ReplyDelete
    Replies
    1. 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:
      - http://bugs.eclipse.org/417725

      Delete

Note: Only a member of this blog may post a comment.