How to JSON-marshal JAXBElement-wrapped responses without the JAXBElement wrapper?

11k views Asked by At

I have an http service that is using Spring (v4.0.5). Its http endpoints are configured using Spring Web MVC. The responses are JAXB2-anotated classes that are generated off of a schema. The responses are wrapped in JAXBElement as the generated JAXB classes do not sport @XmlRootElement annotations (and the schema cannot be modified to doctor this). I had to fight a bit with getting XML marshalling ti work; in any case, it is working.

Now I am setting up JSON marshalling. What I am running into is getting JSON-documents that feature the JAXBElement "envelope".

{
  "declaredType": "io.github.gv0tch0.sotaro.SayWhat",
  "globalScope": true,
  "name": "{urn:io:github:gv0tch0:sotaro}say",
  "nil": false,
  "scope": "javax.xml.bind.JAXBElement$GlobalScope",
  "typeSubstituted": false,
  "value": {
    "what": "what",
    "when": "2014-06-09T15:56:46Z"
  }
}

What I would like to get marshalled instead is just the value-object:

{
  "what": "what",
  "when": "2014-06-09T15:56:46Z"
}

Here is my JSON marshalling config (part of the spring context configuration):

<bean id="jsonConverter" class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter">
  <property name="objectMapper" ref="jacksonMapper" />
  <property name="supportedMediaTypes" value="application/json" />
</bean>

<bean id="jacksonMapper" class="com.fasterxml.jackson.databind.ObjectMapper">
  <property name="dateFormat">
    <bean class="java.text.SimpleDateFormat">
      <constructor-arg type="java.lang.String" value="yyyy-MM-dd'T'HH:mm:ss'Z'" />
      <property name="timeZone">
        <bean class="java.util.TimeZone" factory-method="getTimeZone">
          <constructor-arg type="java.lang.String" value="UTC" />
        </bean>
      </property>
    </bean>
  </property>
</bean>

I am hoping that this can be accomplished by configuring the ObjectMapper. I guess alternatively rolling out my own serializer may work. Thoughts? Suggestions?

2

There are 2 answers

2
Alexey Gavrilov On BEST ANSWER

You can register a mixin annotation for the JAXBElement class which would put the @JsonValue annotation on the JAXBElement.getValue() method making its return value to be the JSON representation. Here is an example:

An example .xsd chema file that are given to xjc.

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>

<xs:schema version="1.0" xmlns:xs="http://www.w3.org/2001/XMLSchema">
    <xs:element name="item" type="Thing"/>

    <xs:complexType name="Thing">
        <xs:sequence>
            <xs:element name="number" type="xs:long"/>
            <xs:element name="string" type="xs:string" minOccurs="0"/>
        </xs:sequence>
    </xs:complexType>

</xs:schema>

A Java main class:

public class JacksonJAXBElement {
    // a mixin annotation that overrides the handling for the JAXBElement
    public static interface JAXBElementMixin {
        @JsonValue
        Object getValue();
    }

    public static void main(String[] args) throws JAXBException, JsonProcessingException {
        ObjectFactory factory = new ObjectFactory();
        Thing thing = factory.createThing();
        thing.setString("value");
        thing.setNumber(123);
        JAXBElement<Thing> orderJAXBElement = factory.createItem(thing);

        System.out.println("XML:");
        JAXBContext jc = JAXBContext.newInstance(Thing.class);
        Marshaller marshaller = jc.createMarshaller();
        marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);
        marshaller.marshal(orderJAXBElement, System.out);
        System.out.println("JSON:");

        ObjectMapper mapper = new ObjectMapper();
        mapper.addMixInAnnotations(JAXBElement.class, JAXBElementMixin.class);
        System.out.println(mapper.writerWithDefaultPrettyPrinter()
                .writeValueAsString(orderJAXBElement));
    }
}

Output:

XML:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<item>
    <number>123</number>
    <string>value</string>
</item>
JSON:
{
  "number" : 123,
  "string" : "value"
}
2
gv0tch0 On

For an example of a fully configured project that:

  • Uses solely Spring (v4.0.5) for content negotiation and marshaling.
  • Uses JAXB to generate the object representation of the responses.
  • Supports both XML and JSON content negotiation.
  • Honors the JAXB annotations when marshaling JSON responses.
  • Avoids leaking the JAXBElement-wrapper around the response object.

head over here.

The essential pieces are:

  • A Jackson mixin, which allows Jackson to unwrap the response object from the JAXBElement-wrapper prior to marshaling.
  • Spring context configuration which configures the JSON object mapper to use Jackson, and configures said mapper to take advantage of the mixin and use an AnnotationIntrospectorPair introspector (note that the page is a little out of date) which configures the JAXB annotation introspector as the primary introspector (makes sure the responses conform to what the XSD prescribes) and the Jackson one as the secondary (ensures the JAXBElement unwrapping mixin is in play).

The Mixin:

/**
 * Ensures, when the Jackson {@link ObjectMapper} is configured with it, that
 * {@link JAXBElement}-wrapped response objects when serialized as JSON documents
 * do not feature the JAXBElement properties; but instead the JSON-document that
 * results in marshalling the member returned by the {@link JAXBElement#getValue()}
 * call.
 * <p>
 * More on the usage and configuration options is available
 * <a href="http://wiki.fasterxml.com/JacksonMixInAnnotations">here</a>.
 */
public interface JaxbElementMixin {
  @JsonValue
  Object getValue();
}

The Spring context config:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
  xmlns:aop="http://www.springframework.org/schema/aop"
  xmlns:context="http://www.springframework.org/schema/context" 
  xmlns:tx="http://www.springframework.org/schema/tx"
  xmlns:util="http://www.springframework.org/schema/util" 
  xmlns:mvc="http://www.springframework.org/schema/mvc"
  xmlns:oxm="http://www.springframework.org/schema/oxm"
  xsi:schemaLocation="
  http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
  http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.0.xsd
  http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.0.xsd
  http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd
  http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-4.0.xsd
  http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.0.xsd
  http://www.springframework.org/schema/oxm http://www.springframework.org/schema/oxm/spring-oxm-4.0.xsd">

  <context:component-scan base-package="io.github.gv0tch0.sotaro"/>

  <bean id="contentNegotiationManager" class="org.springframework.web.accept.ContentNegotiationManagerFactoryBean">
    <property name="favorPathExtension" value="false" />
    <property name="ignoreAcceptHeader" value="false" />
    <property name="useJaf" value="false" />
    <property name="defaultContentType" value="application/xml" />
    <property name="mediaTypes">
      <map>
        <entry key="xml" value="application/xml" />
        <entry key="json" value="application/json" />
      </map>
    </property>
  </bean>

  <bean id="typeFactory" class="com.fasterxml.jackson.databind.type.TypeFactory" factory-method="defaultInstance" />

  <bean id="jaxbIntrospector" class="com.fasterxml.jackson.module.jaxb.JaxbAnnotationIntrospector">
    <constructor-arg ref="typeFactory" />
  </bean>

  <bean id="jacksonIntrospector" class="com.fasterxml.jackson.databind.introspect.JacksonAnnotationIntrospector" />

  <bean id="annotationIntrospector" class="com.fasterxml.jackson.databind.introspect.AnnotationIntrospectorPair">
    <!-- Note that in order to get the best of both the JAXB annotation instrospector and the Mixin configuration
         the JAXB introspector needs to be the primary introspector, hence it needs to stay at position 0. -->
    <constructor-arg index="0" ref="jaxbIntrospector" />
    <constructor-arg index="1" ref="jacksonIntrospector" />
  </bean>

  <bean id="jacksonMapper" class="com.fasterxml.jackson.databind.ObjectMapper">
    <property name="annotationIntrospector" ref="annotationIntrospector" />
    <!-- The mixin ensures that when JAXBElement wrapped responses are marshalled as JSON the
         JAXBElement "envelope" gets discarded (which makes our JSON responses conform to spec). -->
    <property name="mixInAnnotations">
      <map key-type="java.lang.Class" value-type="java.lang.Class">
        <entry key="javax.xml.bind.JAXBElement" value="io.github.gv0tch0.sotaro.JaxbElementMixin" />
      </map>
    </property>
    <property name="dateFormat">
      <bean class="java.text.SimpleDateFormat">
        <constructor-arg type="java.lang.String" value="yyyy-MM-dd'T'HH:mm:ss'Z'" />
        <property name="timeZone">
          <bean class="java.util.TimeZone" factory-method="getTimeZone">
            <constructor-arg type="java.lang.String" value="UTC" />
          </bean>
        </property>
      </bean>
    </property>
  </bean>

  <bean id="jsonConverter" class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter">
    <property name="objectMapper" ref="jacksonMapper" />
    <property name="supportedMediaTypes" value="application/json" />
  </bean>

  <bean id="jaxbMarshaller" class="org.springframework.oxm.jaxb.Jaxb2Marshaller">
    <property name="supportJaxbElementClass" value="true" />
    <property name="contextPath" value="io.github.gv0tch0.sotaro" />
  </bean>

  <bean id="xmlConverter" class="org.springframework.http.converter.xml.MarshallingHttpMessageConverter">
    <constructor-arg ref="jaxbMarshaller" />
    <property name="supportedMediaTypes" value="application/xml" />
  </bean>

  <mvc:annotation-driven content-negotiation-manager="contentNegotiationManager">
    <mvc:message-converters register-defaults="false">
      <ref bean="xmlConverter" />
      <ref bean="jsonConverter" />
    </mvc:message-converters>
  </mvc:annotation-driven>

</beans>