Including null elements in JSON output of Jersey RESTful API with JAXB

10,920

For EclipseLink JAXB (MOXy)'s JSON binding, the correct mapping would be the following. You could try it with your provider to see if it would work also:

@XmlRootElement
public class Data {
    @XmlElement(nillable=true)
    public String firstName;

    @XmlElement(nillable=true)
    public String lastName;
}

For More Information


UPDATE 2

EclipseLink 2.4 includes MOXyJsonProvider which is an implementation of MessageBodyReader/MessageBodyWriter that you can use directly to leverage MOXy's JSON binding

UPDATE 1

The following MessageBodyReader/MessageBodyWriter may work better for you:

import java.io.*;
import java.lang.annotation.Annotation;
import java.lang.reflect.*;
import javax.xml.transform.stream.StreamSource;

import javax.ws.rs.*;
import javax.ws.rs.core.*;
import javax.ws.rs.ext.*;
import javax.xml.bind.*;

import org.eclipse.persistence.jaxb.JAXBContextFactory;

@Provider
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class MOXyJSONProvider implements
    MessageBodyReader<Object>, MessageBodyWriter<Object>{

    @Context
    protected Providers providers;

    public boolean isReadable(Class<?> type, Type genericType,
        Annotation[] annotations, MediaType mediaType) {
        return true;
    }

    public Object readFrom(Class<Object> type, Type genericType,
            Annotation[] annotations, MediaType mediaType,
            MultivaluedMap<String, String> httpHeaders, InputStream entityStream)
            throws IOException, WebApplicationException {
            try {
                Class<?> domainClass = getDomainClass(genericType);
                Unmarshaller u = getJAXBContext(domainClass, mediaType).createUnmarshaller();
                u.setProperty("eclipselink.media-type", mediaType.toString());
                u.setProperty("eclipselink.json.include-root", false);
                return u.unmarshal(new StreamSource(entityStream), domainClass).getValue();
            } catch(JAXBException jaxbException) {
                throw new WebApplicationException(jaxbException);
            }
    }

    public boolean isWriteable(Class<?> type, Type genericType,
        Annotation[] annotations, MediaType mediaType) {
        return true;
    }

    public void writeTo(Object object, Class<?> type, Type genericType,
        Annotation[] annotations, MediaType mediaType,
        MultivaluedMap<String, Object> httpHeaders,
        OutputStream entityStream) throws IOException,
        WebApplicationException {
        try {
            Class<?> domainClass = getDomainClass(genericType);
            Marshaller m = getJAXBContext(domainClass, mediaType).createMarshaller();
            m.setProperty("eclipselink.media-type", mediaType.toString());
            m.setProperty("eclipselink.json.include-root", false);
            m.marshal(object, entityStream);
        } catch(JAXBException jaxbException) {
            throw new WebApplicationException(jaxbException);
        }
    }

    public long getSize(Object t, Class<?> type, Type genericType,
        Annotation[] annotations, MediaType mediaType) {
        return -1;
    }

    private JAXBContext getJAXBContext(Class<?> type, MediaType mediaType)
        throws JAXBException {
        ContextResolver<JAXBContext> resolver
            = providers.getContextResolver(JAXBContext.class, mediaType);
        JAXBContext jaxbContext;
        if(null == resolver || null == (jaxbContext = resolver.getContext(type))) {
            return JAXBContextFactory.createContext(new Class[] {type}, null); 
        } else {
            return jaxbContext;
        }
    }

    private Class<?> getDomainClass(Type genericType) {
        if(genericType instanceof Class) {
            return (Class<?>) genericType;
        } else if(genericType instanceof ParameterizedType) {
            return (Class<?>) ((ParameterizedType) genericType).getActualTypeArguments()[0];
        } else {
            return null;
        }
    }

}
Share:
10,920
Oleksi
Author by

Oleksi

Updated on June 16, 2022

Comments

  • Oleksi
    Oleksi almost 2 years

    I have a class that I would like to expose through a Jersey RESTful API. It looks similar to this:

    @XmlRootElement
    public class Data {
        public String firstName;
        public String lastName;
    }
    

    My problem is that these fields may be null, in which case the field is omitted from the JSON output. I would like all fields to be present regardless of their value. For example, if lastName is null, the JSON output will be:

    {
       "firstName" : "Oleksi"
    }
    

    instead of what I want:

    {
       "firstName" : "Oleksi",
       "lastName" : null
    }
    

    I have a JAXBContextResolver (an implementation of ContextResolver) that looks like this:

    @Provider
    public class JAXBContextResolver implements ContextResolver<JAXBContext> {
    
         // internal state
        private final JAXBContext context;    
        private final Set<Class> types; 
        private final Class[] cTypes = { Data.class };
    
    
        public JAXBContextResolver() 
        throws Exception {
    
            types = new HashSet(Arrays.asList(cTypes));
            context = new JSONJAXBContext(JSONConfiguration.natural().humanReadableFormatting(true).build(), cTypes);
        }
    
        @Override
        public JAXBContext getContext(Class<?> objectType) {
    
            return (types.contains(objectType)) ? context : null;
        }
    }
    

    I've been trying to figure out how to get that desired output for a while, but I've had no luck. I'm open to trying other ContextResolvers/Serializers, but I haven't been able to find one that works, so code examples would be nice.

  • Oleksi
    Oleksi about 12 years
    I've tried this, but I can't quite get it to work. Can you please provide more details on any additional code changes that would be required (other than what's listed above). For instance, do I have to change my JAXBContextResolver class?
  • Oleksi
    Oleksi about 12 years
    I've also had difficulties downloading the required dependancies for this project. I tried looking at this document to no avail. wiki.eclipse.org/EclipseLink/Maven
  • bdoughan
    bdoughan about 12 years
  • Oleksi
    Oleksi about 12 years
    I downloaded and tried it with the JAXBProvider and it didn't work. An empty lastName is output lastName : { "nil" : "true" }. I switched to MOXyJSONProvider given in your example and then I get an exception: javax.ws.rs.WebApplicationException: javax.xml.bind.PropertyException: name: eclipselink.media-type value: application/json at MOXyJSONProvider.writeTo(MOXyJSONProvider.java:58)
  • Oleksi
    Oleksi about 12 years
    Does it have to be version 2.4.0-SNAPSHOT? I'd prefer to use the most up-to-date stable release (2.3.2)
  • bdoughan
    bdoughan about 12 years
    @Oleksi - The JSON binding is being added to 2.4.0, instead of a snap shot you can use a milestone 2.4.0-M18 eclipse.org/eclipselink/downloads/milestones.php. We will be releasing as part of the Eclipse Juno release this summer and should be entering the release candidate stage very soon.
  • Oleksi
    Oleksi about 12 years
    I downloaded and tried it with 2.4.0-M18, but I'm still getting the same exception. Any suggestions?
  • Oleksi
    Oleksi about 12 years
    Looks like getJAXBContext() in MOXyJSONProvider might not be picking up a context correctly (and thus creating a JAXB context that doesn't know what "eclipselink.media-type" is). Do we also need to provide a JAXBContext? If so, how do we tell MOXy about it?
  • bdoughan
    bdoughan about 12 years
    @Oleksi - I've updated my answer with a version that may work better for you.
  • Oleksi
    Oleksi about 12 years
    Tried the new version, but it still doesn't work (same exception). I think there's some type confusion in getJAXBContext(). Should it be working with avax.xml.bind.JAXBContext or org.eclipse.persistence.jaxb.JAXBContext? Or both? Also, do I need the jaxb.properties file, and if so, then where should it be?
  • Oleksi
    Oleksi about 12 years
    Fixed! The problem was the we left the old JAXB provider hanging around with it's @Provider annotation. It seems to work now. Thanks a a lot for your help. :)