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

2

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

Avifauna answered 28/4, 2011 at 14:15 Comment(1)
Is the question clear? I wonder why nobody has an answer.Avifauna
J
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>
Jocosity answered 3/5, 2011 at 14:31 Comment(23)
Why wouldn't it be bijective? I want fields->summary to go directly in summary and fields->xyz to stay in fields->xyz.Avifauna
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.Jocosity
Still, fields can be ungrouped on unmarshalling, so I think it is bijective. Someone suggested a way to solve my problem (in #5881558), although it requires using Eclipselink Moxy, which I don't want to introduce (additional dependency and jaxb replacement in whole application).Avifauna
So @XmlAnyElement answers your question?Jocosity
I've used org.w3c.dom.Element maybe using another one may help navigate through the response (if jaxb supports it).Jocosity
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.Avifauna
@Avifauna 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.Jocosity
@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.Avifauna
@Avifauna ;) 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?Jocosity
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)Avifauna
Can you provide the IssueFields class?Jocosity
@XmlElement(required = true, name = "summary") protected Field summaryField; @XmlAnyElement(lax = true) private List<Element> fields;Avifauna
Summary is not always required, it is? Why using lax on fields?Jocosity
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<Fields> instead of List<Element>, because that's what I want in the end.Avifauna
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 on that matter.Jocosity
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.Avifauna
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)Avifauna
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.Jocosity
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.Avifauna
Apparently, your are unmarshaling a larger file than the one provided. Copy it in the comments, I'll take a look.Jocosity
{ "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 } },Avifauna
"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" },Avifauna
Please, post it at an answer (that we will delete after), in this form is hardly exploitable. ThanksJocosity
A
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" 
            },
Avifauna answered 20/5, 2011 at 11:46 Comment(2)
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.Jocosity
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...Avifauna

© 2022 - 2024 — McMap. All rights reserved.