5

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

Community
  • 1
  • 1
ymajoros
  • 2,454
  • 3
  • 34
  • 60

2 Answers2

3

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>
yves amsellem
  • 7,106
  • 5
  • 44
  • 68
  • Why wouldn't it be bijective? I want fields->summary to go directly in summary and fields->xyz to stay in fields->xyz. – ymajoros May 04 '11 at 07:16
  • It would not be bijective because other fields will be grouped in a single one. See my updated answer for a code sample. If you find a way to reduce this object graph, please share it with us. – yves amsellem May 04 '11 at 16:09
  • Still, fields can be ungrouped on unmarshalling, so I think it is bijective. Someone suggested a way to solve my problem (in http://stackoverflow.com/questions/5881058/how-to-map-a-bean-structure-to-a-different-schema-with-jax-rs), although it requires using Eclipselink Moxy, which I don't want to introduce (additional dependency and jaxb replacement in whole application). – ymajoros May 05 '11 at 07:09
  • So @XmlAnyElement answers your question? – yves amsellem May 05 '11 at 14:52
  • I've used org.w3c.dom.Element maybe using another one may help navigate through the response (if jaxb supports it). – yves amsellem May 09 '11 at 16:02
  • It looks to me that it is bound to the order / presence of elements. If "summary" isn't the first element, or if it's not present, I'm under the impression that it simply won't work. – ymajoros May 11 '11 at 08:44
  • @ymajoros You're under the impression? Test this full working sample and you will know. What is this habit of asking something and don't even verify if the answer is correct? It took me an hour to made this work, and it does — even if Fields is only made of a List of Elements — it's a minimum that you take two minutes to verify if it fits your needs. – yves amsellem May 11 '11 at 09:04
  • @yves I'm really planning to test it. This test case is simplified, it will take some more time to adapt it to the original code. That's why I'm asking, I'd like to know if I made my original point clear and if this is the answer. I don't think I have the habit of not checking / marking correct, it just can take some time. You are free to answer questions, and I'm free to have a life and check them when I can. Thanks for your answer, I'll definitely check it when possible. – ymajoros May 11 '11 at 10:24
  • @ymajoros ;) I'm just trying to help. Saying it won't work without trying made me worry. Sorry. In your sample you don't define the Field class. I've used an Element class because it is navigable, but it make the code hard to read. In which kind of element do you want to gather the sub-nodes? – yves amsellem May 11 '11 at 13:08
  • Ok, I tried it. A field that isn't mentionned in the Fields class seems to trigger this error: Exception in thread "main" com.sun.jersey.api.client.ClientHandlerException: org.codehaus.jackson.map.exc.UnrecognizedPropertyException: Unrecognized field "timetracking" (Class com.atlassian.jira.rest.client.domain.IssueFields), not marked as ignorable (tried with Element as well as a Field class, same result) – ymajoros May 11 '11 at 14:30
  • Can you provide the IssueFields class? – yves amsellem May 11 '11 at 16:01
  • @XmlElement(required = true, name = "summary") protected Field summaryField; @XmlAnyElement(lax = true) private List fields; – ymajoros May 12 '11 at 07:21
  • Summary is not always required, it is? Why using lax on fields? – yves amsellem May 12 '11 at 08:01
  • Summary is always there, but you could consider it isn't if that's easier. Some fields are always there, some are there sometimes and custom fields can be added. I tried with and without lax. I also tried with List instead of List, because that's what I want in the end. – ymajoros May 12 '11 at 08:26
  • Why do you prefere not to use a normal hierarchy of classes? All the sub-nodes of « fields » looks like each other and missing elements are not a problem for JAXB. I've written a [complete article](http://blog.xebia.com/2011/03/jaxb-xml-data-binding/) on that matter. – yves amsellem May 12 '11 at 08:49
  • I didn't choose the schema, it's the one provided by Jira. I need, however, to be able to access (pseudo-notation) issue.summary as well as issue.fields[custom_1234]. Jira provides an example client, but it really doesn't suit my needs (too many dependencies, somewhat strange hierarchy, almost hand-written unmarshalling, ...). I need to have something that is as good as Jira's example client, without the constraints. – ymajoros May 12 '11 at 09:06
  • I don't mind having to access issue.fields.summary.value instead of the shorter issue.summary, so the actual class hierarchy doesn't matter too much... as long as I don't have to access a loosely typed field map for properties that are always there (not all of them are Strings, btw) – ymajoros May 12 '11 at 09:07
  • You can also define JAXB classes and attributes for your needs, ignoring the rest of the JSON tree? Trying to use a tool out (or in marge) of its purpose is always a pain. – yves amsellem May 12 '11 at 13:32
  • You're right... But what's the conclusion, so I can flag this an answered? ;-) I still can't make it work, I'm trying to. – ymajoros May 12 '11 at 15:07
  • Apparently, your are unmarshaling a larger file than the one provided. Copy it in the comments, I'll take a look. – yves amsellem May 12 '11 at 16:26
  • { "expand":"html", "self":"https://xxx/jira/rest/api/latest/issue/EPC-2731", "key":"EPC-2731", "fields":{ "summary":{ "name":"summary", "type":"java.lang.String", "value":"Fwd: commentaires vides dans FicheSousGroupement" }, "timetracking":{ "name":"timetracking", "type":"com.atlassian.jira.issue.fields.TimeTrackingSystemField", "value":{ "timeestimate":0, "timespent":60 } }, – ymajoros May 13 '11 at 12:43
  • "issuetype":{ "name":"issuetype", "type":"com.atlassian.jira.issue.issuetype.IssueType", "value":{ "self":"https://xxx/jira/rest/api/latest/issueType/2", "name":"Nouvelle fonctionnalité", "subtask":false } }, "customfield_10080":{ "name":"Testeur", "type":"com.atlassian.jira.plugin.system.customfieldtypes:userpicker" }, – ymajoros May 13 '11 at 12:43
  • Please, post it at an answer (that we will delete after), in this form is hardly exploitable. Thanks – yves amsellem May 18 '11 at 12:47
0
{
    "expand":"html",
        "self":"xxx/jira/rest/api/latest/issue/EPC-2731";,
        "key":"EPC-2731",
        "fields":{
            "summary":{
                "name":"summary",
                "type":"java.lang.String",
                "value":"Fwd: commentaires vides dans FicheSousGroupement" 
            },
            "timetracking":{
                "name":"timetracking",
                "type":"com.atlassian.jira.issue.fields.TimeTrackingSystemField",
                "value":{
                    "timeestimate":0,
                    "timespent":60 
                } 
            },
            "issuetype":{
                "name":"issuetype",
                "type":"com.atlassian.jira.issue.issuetype.IssueType",
                "value":{
                    "self":"xxx/jira/rest/api/latest/issueType/2";,
                    "name":"Nouvelle fonctionnalité",
                    "subtask":false 
                } 
            },
            "customfield_10080":{
                "name":"Testeur",
                "type":"com.atlassian.jira.plugin.system.customfieldtypes:userpicker" 
            },
ymajoros
  • 2,454
  • 3
  • 34
  • 60
  • Sorry for the late response but... your code works on my sample (if you 1.switch « timetracking » and « customfield_10080 » or 2. issue.getFields().getFields().get(3)). So, hey, grab the code and you're done. – yves amsellem Jun 28 '11 at 13:20
  • ok, I believe you. I could get *some* things working in the meantime... and decided to use the command-line client instead, because I can't do as much with this anyway... – ymajoros Jun 28 '11 at 13:53