How to serialize Java primitives using Jersey REST

Solution 1:

Have a look at Genson.It helped me a lot with a similar problem.With Genson you could use generics like int,boolean, lists and so on...Here is a quick example.

@GET
@Produces(MediaType.APPLICATION_JSON)
public Response getMagicList() {
    List<Object> objList = new ArrayList<>();
    stringList.add("Random String");
    stringList.add(121); //int
    stringList.add(1.22); //double
    stringList.add(false); //bolean

    return Response.status(Status.OK).entity(objList).build();
}

This will produce a valid JSON witch can be retrieved very simple like this:

    Client client = Client.create();
    WebResource webResource = client.resource("...path to resource...");
    List objList = webResource.accept(MediaType.APPLICATION_JSON).get(ArrayList.class);
    for (Object obj : objList) {
        System.out.println(obj.getClass());
    }

You will see that Genson will help you decode the JSON on the client side also and output the correct class for each.

Solution 2:

Are you writing a service or a client? In the service-end of things, you would simply write a MessageBodyWriter to serialize a stream of data to a Java object for your types. In my use cases, the services I'm writing output to JSON or XML, and in XML's case, I just throw one JAXB annotation on the top of my classes and I'm done.

Have you looked at the Jersey User guide regarding this?

3.6. Adding support for new representations

Solution 3:

Actually your best bet is to write a custom ContextResolver Provider like the following that uses natural building of JSON.

   @Provider
   public class YourContextResolver implements ContextResolver<JAXBContext> {

    private JAXBContext context;
    private Class<?>[] types = { YourSpecialBean.class };

    public YourContextResolver() throws Exception {
        this.context = new JSONJAXBContext(
                JSONConfiguration.natural().build(), types);
    }

    public JAXBContext getContext(Class<?> objectType) {
        for (int i = 0; i < this.types.length; i++)
            if (this.types[i].equals(objectType)) return context;

        return null;
    }
}

The only thing special here to notice is the YourSpecialBean.class in the Class[]. This defines an array of class types that this provider will resolve naturally.

Solution 4:

Tell Jersey generate proper JSON documents (natural json). I use same class for rest app and JAXBContext resolver, found it the most clean encapsulation.

Better programmer could implement helper to iterate .class files and list appropriate classes automatically by identifying @Annotation tags. I don't know how to do it runtime in an own source code.

These two links were helpful studying this extra java jargon. I don't know why there is no Jersey parameter to make all just work out of the box.

  • http://jersey.java.net/nonav/documentation/latest/json.html
  • http://jersey.java.net/nonav/documentation/latest/chapter_deps.html
  • https://maven.java.net/content/repositories/releases/com/sun/jersey/jersey-archive/
  • http://search.maven.org/remotecontent?filepath=asm/asm/3.3.1/asm-3.3.1.jar
  • https://github.com/rmuller/infomas-asl/

WEB-INF/web.xml (snippet):

<servlet>
  <servlet-name>RESTServlet</servlet-name>
  <servlet-class>com.sun.jersey.spi.container.servlet.ServletContainer</servlet-class>
  <init-param>
    <param-name>javax.ws.rs.Application</param-name>
    <param-value>com.myapp.rest.RESTApplication</param-value>
  </init-param>
</servlet>
<servlet-mapping>
  <servlet-name>RESTServlet</servlet-name>
  <url-pattern>/servlet/rest/*</url-pattern>
</servlet-mapping>

com.myapp.rest.RESTApplication.java

package com.myapp.rest;

import java.util.*;
import javax.ws.rs.core.Application;
import javax.ws.rs.ext.ContextResolver;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import com.sun.jersey.api.json.JSONConfiguration;
import com.sun.jersey.api.json.JSONJAXBContext;

public class RESTApplication extends Application implements ContextResolver<JAXBContext> {
    private JAXBContext context;
    private Class<?>[] types;

    public RESTApplication() throws JAXBException {
        // list JAXB bean types to be used for REST serialization
        types = new Class[] {
            com.myapp.rest.MyBean1.class, 
            com.myapp.rest.MyBean2.class, 
        };
        context = new JSONJAXBContext(JSONConfiguration.natural().build(), types);
    }

    @Override
    public Set<Class<?>> getClasses() {
        // list JAXB resource/provider/resolver classes
        Set<Class<?>> classes = new HashSet<Class<?>>();
        //for(Class<?> type : types)
        //    classes.add(type);
        classes.add(MyBeansResource.class);
        classes.add(this.getClass()); // used as a ContextResolver class
        return classes;
    }

    @Override
    public JAXBContext getContext(Class<?> objectType) {
        // this is called each time when rest path was called by remote client
        for (Class<?> type : types) {
            if (type==objectType)
                return context;
        }
        return null;
    }
}

Classes MyBean1,MyBean2 are plain java objects and MyBeansResource class is the one with @Path rest functions. There is nothing special in them expect standard jaxp @Annotations here and there. After this java jargon JSON documents have

  • zero or single-element List arrays are always written as json array ([] field)
  • primitive integers and boolean fields are written as json primitives (without quotations)

I use the following environment

  • Sun Java JDK1.6.x
  • Apache Tomcat 6.x
  • Jersey v1.14 libraries (jersey-archive-1.14.zip)
  • webapps/myapp/WEB-INF/lib folder has asm-3.3.1.jar, jackson-core-asl.jar, jersey-client.jar, jersey-core.jar, jersey-json.jar, jersey-server.jar, jersey-servlet.jar libraries
  • add optional annotation-detector.jar if you use infomas-asl discovery tool

jersey-archive.zip had older asm-3.1.jar file, probably works fine but chapter_deps.html links to a newer file. See link list at the top.

Edit I found an excellent(fast, lightweight just 15KB) annotation discovery tool. See this post about how I autodiscover types at runtime and no longer need to edit RESTApplication each time new java(jaxb) bean is added.

https://github.com/rmuller/infomas-asl/issues/7

Solution 5:

I had the same problem today and didn't give up until i found a really good suitable solution. I can not update the jersey library from 1.1.5 it is a Legacy System. My Rest Service returns a List and they should follow those rules.

  1. Empty Lists are rendered as [] (almost impossible)
  2. One Element Lists are rendered as [] (difficult but only mapping configuration)
  3. Many Element Lists are rendered as [] (easy)

Start from easy to impossible.

3) nothing today normal JSON Mapping

2) Register JAXBContextResolver like the following

@Provider
public class JAXBContextResolver implements ContextResolver<JAXBContext> {
    private final JAXBContext context;
    private final Set<Class<?>> types;
    private Class<?>[] ctypes = { Pojo.class }; //your pojo class
    public JAXBContextResolver() throws Exception {
        this.types = new HashSet<Class<?>>(Arrays.asList(ctypes));
        this.context = new JSONJAXBContext(JSONConfiguration.mapped()
                .rootUnwrapping(true)
                .arrays("propertyName") //that should rendered as JSONArray even if the List only contain one element but doesn't handle the empty Collection case
                .build()
                , ctypes);
    }

    @Override
    public JAXBContext getContext(Class<?> objectType) {
        return (types.contains(objectType)) ? context : null;
    }
}

1) The following approach only works for Collections$EmptyList class. May you find a way to make it general for all Collections they are empty. May code deal with EmptyList so.

@Provider
@Produces(value={MediaType.APPLICATION_JSON})
public class EmptyListWriter implements MessageBodyWriter<AbstractList> {

    private static final String EMPTY_JSON_ARRAY = "[]";

    @Override
    public long getSize(AbstractList list, Class<?> clazz, Type type, Annotation[] annotations, MediaType mediaType) {
        return EMPTY_JSON_ARRAY.length();
    }

    @Override
    public boolean isWriteable(Class<?> clazz, Type type, Annotation[] annotations, MediaType mediaType) {
        return clazz.getName().equals("java.util.Collections$EmptyList");
    }

    @Override
    public void writeTo(AbstractList list, Class<?> clazz, Type type, Annotation[] annotations, MediaType mediaType, 
            MultivaluedMap<String, Object> headers, OutputStream outputStream) throws IOException, WebApplicationException {
        if (list.isEmpty())
            outputStream.write(EMPTY_JSON_ARRAY.getBytes());            
    }
}