ymajoros ymajoros - 2 months ago 16
reST (reStructuredText) Question

how to use some indirection when unmarshalling json to java bean using Jersey using jaxb annotations

I'm trying to unmarshall some received json (from Jira restful web service).

Problem is: an "issue" has a "summary" property and a list of fields.

Summary is not present as an attribute in the received json, but as a value of the "fields" attribute. I insist on unmarshalling to this structure:

@XmlRootElement
class Issue {
String summary;
List<Field> fields;
// getters/setters and lots of other fields
}


Received JSON:

{
"expand":"html",
"self":"https://example.com/jira/rest/api/latest/issue/XYZ-1234",
"key":"XYZ-1234",
"fields":
{
"summary":
{
"name":"summary",
"type":"java.lang.String",
"value":"test 1234"
},
"customfield_10080":
{
"name":"Testeur",
"type":"com.atlassian.jira.plugin.system.customfieldtypes:userpicker"
},
"status":
{
"name":"status",
"type":"com.atlassian.jira.issue.status.Status",
"value":
{
"self":"https://example.com/jira/rest/api/latest/status/5",
"name":"Resolved"
}
},
...
},
"transitions":"https://example.com/jira/rest/api/latest/issue/XYZ-1234/transitions"
}


I don't want to use Jira's own client (too many dependencies which I don't want in my app).

edit: I asked my question another way to try to make it clear: how to map a bean structure to a different schema with jax-rs

Answer

Your annotated class should be bijective: it should allow to generate the same input from which it was unmarshalled. If you still want to use a non-bijective object graph, you can use @XmlAnyElement the following way:

public class Issue {

    @XmlElement(required = true)
    protected Fields fields;

    public Fields getFields() {
        return fields;
    }
}

In the input you gave, fields is not a list, but a field (JSON uses [] to delimit lists):

public class Fields {

    @XmlElement(required = true)
    protected Summary summary;

    @XmlAnyElement
    private List<Element> fields;

    public List<Element> getFields() {
        return fields;
    }

    public Summary getSummary() {
        return summary;
    }
}

In order to catch Summary, you'll have to define a dedicated class. Remaining fields will be grouped in the fields list of elements.

public class Summary {

    @XmlAttribute
    protected String name;

    public String getName() {
        return name;
    }
}

Below, a unit test using your input shows that everything work:

public class JaxbTest {
    @Test
    public void unmarshal() throws JAXBException, IOException {
        URL xmlUrl = Resources.getResource("json.txt");
        InputStream stream = Resources.newInputStreamSupplier(xmlUrl).getInput();
        Issue issue = parse(stream, Issue.class);

        assertEquals("summary", issue.getFields().getSummary().getName());

        Element element = issue.getFields().getFields().get(0);
        assertEquals("customfield_10080", element.getTagName());
        assertEquals("name", element.getFirstChild().getLocalName());
        assertEquals("Testeur", element.getFirstChild().getFirstChild().getTextContent());
    }

    private <T> T parse(InputStream stream, Class<T> clazz) throws JAXBException {
        JSONUnmarshaller unmarshaller = JsonContextNatural().createJSONUnmarshaller();
        return unmarshaller.unmarshalFromJSON(stream, clazz);
    }

    private JSONJAXBContext JsonContextNatural() throws JAXBException {
        return new JSONJAXBContext(JSONConfiguration.natural().build(), Issue.class);
    }
}

This tests shows that without using dedicated classes, your code will quickly be hard to read.

You will need those maven dependencies to run it:

<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.8.2</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>r08</version>
</dependency>
<dependency>
    <groupId>com.sun.jersey</groupId>
    <artifactId>jersey-json</artifactId>
    <version>1.6</version>
</dependency>