I'm trying to POST the following payload to my Jersey-based web service:
{
"firstname":"Jimmy",
"lastname":"Johns",
"addresses":
[
{
"street":"19 Mayberry Drive",
"city":"Mayberry",
"state":"nc",
"postalcode":"27043",
"country":"us",
"addresstype":1
}
],
"data":
{
"eyes":"blue",
"hair":"brown",
"sandwich":"roast beef"
}
}
My Jersey code:
@POST
public Response create( Person person )
{
createBo( person ); <------- stopped here in debugger
...
Stopped just as Jersey calls me, I see addresses in person flushed out with exactly what I'm looking for (what's in the JSON above). However, my data tuples aren't there. I know Jersey is calling my no-arg constructor for Address es and its setters are getting called, but I'm up in the night as far as what Jersey might or might not be trying to do with these random ("data") tuples in my JSON. (I say "random" because in a different invocation, these might be "cave":"deep, dark", "mountain":"high, wide", etc. This is part and parcel of my interface.)
To flesh out what I'm talking about, consider these POJOs as context for the above:
@XmlAccessorType( XmlAccessType.FIELD )
@XmlRootElement
public class Person implements Serializable
{
@XmlElement
private List< Address > addresses = new ArrayList< Address >();
@XmlElement
private Map< String, String > data = new HashMap< String, String >();
...
@XmlRootElement
public class Address implements Serializable
{
private String street;
private String city;
private String state;
private String country;
private String postalcode;
private Integer addresstype;
...
Note: I can't do the random tuples as I've done Address because I don't actually know beforehand what the keys will be (whereas I limit Address to street, city, etc.).
What I need is some kind of magic serializer for HashMaps in Jersey and I cannot seem to interpret the docs well enough to understand how to write one or work around this problem while still maintaining the flexibility of my interface.
I would appreciate any indication as to how to accomplish this.
Russ Bateman
P.S. Note sadly that Java.util.Map to JSON Object with Jersey / JAXB / Jackson was not helpful, though it showed great promise.
Note: I'm the EclipseLink JAXB (MOXy) lead and a member of the JAXB (JSR-222) expert group.
The following will work if you are using MOXy, and should work with any other JAXB provider. This approach converts the java.util.Map
to an org.w3c.dom.Element
using an XmlAdapter
.
MapAdapter
An XmlAdapter
allows you to marshal an instance of one class as an instance of another class (see: http://blog.bdoughan.com/2010/07/xmladapter-jaxbs-secret-weapon.html).
package forum11353790;
import java.util.*;
import java.util.Map.Entry;
import javax.xml.bind.annotation.adapters.XmlAdapter;
import javax.xml.parsers.*;
import org.w3c.dom.*;
public class MapAdapter extends XmlAdapter<Element, Map<String, String>> {
private DocumentBuilder documentBuilder;
public MapAdapter() throws Exception {
documentBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
}
@Override
public Element marshal(Map<String, String> map) throws Exception {
Document document = documentBuilder.newDocument();
Element rootElement = document.createElement("data");
document.appendChild(rootElement);
for(Entry<String,String> entry : map.entrySet()) {
Element childElement = document.createElement(entry.getKey());
childElement.setTextContent(entry.getValue());
rootElement.appendChild(childElement);
}
return rootElement;
}
@Override
public Map<String, String> unmarshal(Element rootElement) throws Exception {
NodeList nodeList = rootElement.getChildNodes();
Map<String,String> map = new HashMap<String, String>(nodeList.getLength());
for(int x=0; x<nodeList.getLength(); x++) {
Node node = nodeList.item(x);
if(node.getNodeType() == Node.ELEMENT_NODE) {
map.put(node.getNodeName(), node.getTextContent());
}
}
return map;
}
}
Person
You specify that a field/property should leverage the XmlAdapter
via the @XmlJavaTypeAdapter
annotation.
package forum11353790;
import java.io.Serializable;
import java.util.*;
import javax.xml.bind.annotation.*;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
@XmlAccessorType( XmlAccessType.FIELD )
@XmlRootElement
public class Person implements Serializable {
private String firstname;
private String lastname;
private List< Address > addresses = new ArrayList< Address >();
@XmlAnyElement
@XmlJavaTypeAdapter(MapAdapter.class)
private Map< String, String > data = new HashMap< String, String >();
}
Address
package forum11353790;
import java.io.Serializable;
import javax.xml.bind.annotation.*;
@XmlAccessorType(XmlAccessType.FIELD)
public class Address implements Serializable {
private String street;
private String city;
private String state;
private String country;
private String postalcode;
private Integer addresstype;
}
jaxb.properties
To specify MOXy as your JAXB provider you need to include a file called jaxb.properties
in the same package as your domain model with the following entry (see: http://blog.bdoughan.com/2011/05/specifying-eclipselink-moxy-as-your.html).
javax.xml.bind.context.factory=org.eclipse.persistence.jaxb.JAXBContextFactory
Demo
Below is a standalone example you can run to prove everything works.
package forum11353790;
import java.io.FileInputStream;
import java.util.*;
import javax.xml.bind.*;
import javax.xml.transform.stream.StreamSource;
import org.eclipse.persistence.jaxb.JAXBContextProperties;
public class Demo {
public static void main(String[] args) throws Exception {
Map<String, Object> properties = new HashMap<String,Object>(2);
properties.put(JAXBContextProperties.MEDIA_TYPE, "application/json");
properties.put(JAXBContextProperties.JSON_INCLUDE_ROOT, false);
JAXBContext jc = JAXBContext.newInstance(new Class[] {Person.class}, properties);
Unmarshaller unmarshaller = jc.createUnmarshaller();
StreamSource json = new StreamSource(new FileInputStream("src/forum11353790/input.json"));
Person person = unmarshaller.unmarshal(json, Person.class).getValue();
Marshaller marshaller = jc.createMarshaller();
marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
marshaller.marshal(person, System.out);
}
}
input.json/Output
{
"firstname" : "Jimmy",
"lastname" : "Johns",
"addresses" : [ {
"street" : "19 Mayberry Drive",
"city" : "Mayberry",
"state" : "nc",
"country" : "us",
"postalcode" : "27043",
"addresstype" : 1
} ],
"data" : {
"sandwich" : "roast beef",
"hair" : "brown",
"eyes" : "blue"
}
}
MOXy and JAX-RS/Jersey
You can leverage MOXy in a JAX-RS environment by leveraging the MOXyJsonProvider
class:
- http://blog.bdoughan.com/2012/05/moxy-as-your-jax-rs-json-provider.html
Jackson provides the facility for you. You can force it by adding the following to your Application
class. Note this may disable automatic location of your @Path
annotated classes.
@Override
public Set<Object> getSingletons() {
return ImmutableSet
.<Object> builder()
.add(new JacksonJaxbJsonProvider(new ObjectMapper(),
JacksonJaxbJsonProvider.DEFAULT_ANNOTATIONS)).build();
}