9

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?

Community
  • 1
  • 1
gv0tch0
  • 391
  • 1
  • 2
  • 14
  • Have a look at [the @JsonValue annotation](http://fasterxml.github.io/jackson-annotations/javadoc/2.3.0/com/fasterxml/jackson/annotation/JsonValue.html) – Alexey Gavrilov Jun 09 '14 at 17:28
  • The response objects that get marshalled are generated off of schema by `xjc` (JAXB2 binding compiler). Are you suggesting somehow annotating those? Even if this somehow gets accomplished, these are still getting wrapped in `JAXBElement`-s, which themselves cannot be @JsonValue-annotated... – gv0tch0 Jun 10 '14 at 15:23
  • OK. Please take a look at my answer. – Alexey Gavrilov Jun 10 '14 at 16:51

2 Answers2

15

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"
}
Alexey Gavrilov
  • 10,593
  • 2
  • 38
  • 48
  • This works..for the most part. Thank you! The problem is that it appears that mix-in annotations do not mix well (no pun) with the jaxb-annotation-introspector. In fact I found that those two are an either-or proposition. Which means that if I use the mixin approach to unwrap the actual response object from the JAXBElement envelope for marshalling, I have to give up on all of the features that the jaxb-annotation-introspector provides - e.g. marshaling of property names, enum values, etc. the same way they are marshalled in the XML-case. Maybe someone knows how to get the two working together. – gv0tch0 Jun 12 '14 at 00:59
  • 1
    Oh, `mixin`-s and the `JaxbAnnotationIntrospector` *can* actually be [paired](http://wiki.fasterxml.com/JacksonJAXBAnnotations). For me making the `JaxbAnnotationIntrospector` the primary of the pair worked best. – gv0tch0 Jun 12 '14 at 03:03
3

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>
gv0tch0
  • 391
  • 1
  • 2
  • 14
  • can this same approach be extended to setters as well. – dweeb Nov 28 '18 at 20:16
  • below is a partial output of an element which is part of another that wouldn't un-marshal correctly, w/ your method, the parent converts to JSON correctly, but the child here does not. Do you have any suggestions that would help me? ` "sale" : { "actualSalesUnitPriceOrAssociateOrDescription" : [ { "name" : "{http://www.url.url/namespace/}ItemID", "declaredType" : "java.lang.String", "scope" : "javax.xml.bind.JAXBElement$GlobalScope", ...` – dweeb Dec 03 '18 at 19:58