javajaxbjerseymoxy

Why can't I map this simple object to text in XML in Java/Jersey?


I have a REST API created with Jersey in Java. For one request I'd like to return in JSON a list of tuples of pair of coordinates. For this, I have a class that's a wrapper for an ArrayList, a Tuple2 class and a Coords class. I use the javax.xml.bind.annotations for automatically generating the XML/JSON of my classes.

But for a reason that I don't understand my Coords class can't be me mapped to XML.

I have tried different types of attributes (Integers instead of int), having the @XmlAttribute at different places (before the attributes and before the getters) and different XmlAccessType (PROPERTY instead of NONE) but the results were the same.

Here is my Coords class :

package model;

import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlAttribute;
import static javax.xml.bind.annotation.XmlAccessType.NONE;

@XmlRootElement
@XmlAccessorType(NONE)
public class Coords {
    @XmlAttribute private int x;
    @XmlAttribute private int y;

    public Coords(final int x, final int y) {
        this.x = x;
        this.y = y;
    }

    public Coords() {
        this.x = 0;
        this.y = 0;
    }

    public int getX() {
        return this.x;
    }

    public int getY() {
        return this.y;
    }
}

And here is how it is present in my Tuple2

package model;

import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlAttribute;
import static javax.xml.bind.annotation.XmlAccessType.NONE;

@XmlRootElement
@XmlAccessorType(NONE)
public class Tuple2 {
    private Coords c1;
    private Coords c2;
// ...
    @XmlAttribute 
    public Coords getFirst() {
        return this.c1;
    }

    @XmlAttribute 
    public Coords getSecond() {
        return this.c2;
    }
// ...
}

Here is the error message:

[EL Warning]: moxy: 2019-10-27 15:01:08.586--javax.xml.bind.JAXBException: 
Exception Description: The @XmlAttribute property first in type model.Tuple2 must reference a type that maps to text in XML. model.Coords cannot be mapped to a text value.
 - with linked exception:
[Exception [EclipseLink-50096] (Eclipse Persistence Services - 2.7.4.v20190115-ad5b7c6b2a): org.eclipse.persistence.exceptions.JAXBException
Exception Description: The @XmlAttribute property first in type model.Tuple2 must reference a type that maps to text in XML.  model.Coords cannot be mapped to a text value.]
oct. 27, 2019 3:01:08 PM org.glassfish.jersey.message.internal.WriterInterceptorExecutor$TerminalWriterInterceptor aroundWriteTo
GRAVE: MessageBodyWriter not found for media type=application/json, type=class model.ActionList, genericType=class model.ActionList.

Thank you for your help.


Solution

  • Your problem is coming from the misuse of the xml annotations. You are defining a Tuple2 to be an xml root element by annotating it with @XmlRootElement, and its fields to be xml attributes by annotating the get methods with @XmlAttribute. Which translates to:

    <tuple2 first="first_attributes_vale" second="second_attributes_value" />
    

    Now, both fields are of type Coords, which is declared to be another xml element by annotating the Coords class with @XmlRootElement, and its fields to be xml attributes. When Coords gets serialised to xml, it will be:

    <coords x="value" y="value" />
    

    The problem occurs when serialising Tuple2. Its fields are supposed to be xml attributes, but Coords is another xml element. Xml attributes cannot contain nested elements, but only values.

    Solution

    Depending on what you want, you can solve this in two different ways. Although, I wouldn't recommend the second approach, because it's odd (even though it works) and will incur extra effort on the client side (see explanations below).

    First Approach

    Annotate the getFirst() and getSecond() methods with the @XmlElement annotation.

    package model;
    
    import static javax.xml.bind.annotation.XmlAccessType.NONE;
    
    import javax.xml.bind.annotation.XmlAccessorType;
    import javax.xml.bind.annotation.XmlElement;
    import javax.xml.bind.annotation.XmlRootElement;
    
    @XmlRootElement
    @XmlAccessorType(NONE)
    public class Tuple2 {
        private Coords c1;
        private Coords c2;
    
        public Tuple2(Coords c1, Coords c2) {
            this.c1 = c1;
            this.c2 = c2;
        }
    
        public Tuple2() {
            c1 = new Coords(0, 0);
            c2 = new Coords(0, 0);
        }
    
        @XmlElement
        public Coords getFirst() {
            return this.c1;
        }
    
        @XmlElement
        public Coords getSecond() {
            return this.c2;
        }
    }
    

    This will produce a result that looks like this:

    <tuple2>
        <first x="2" y="4"/>
        <second x="12" y="12"/>
    </tuple2>
    

    Second Approach

    This is the odd way of solving it. It works, but it incurs extra effort on the client side, because the values of Coords are encoded as string values and will require parsing on the receiving side.

    Change the return type of getFirst() and getSecond() methods to be String and override the toString() method of Coords.

    package model;
    
    import static javax.xml.bind.annotation.XmlAccessType.NONE;
    
    import javax.xml.bind.annotation.XmlAccessorType;
    import javax.xml.bind.annotation.XmlAttribute;
    import javax.xml.bind.annotation.XmlRootElement;
    
    @XmlRootElement
    @XmlAccessorType(NONE)
    public class Tuple2 {
        private Coords c1;
        private Coords c2;
    
        public Tuple2(Coords c1, Coords c2) {
            this.c1 = c1;
            this.c2 = c2;
        }
    
        public Tuple2() {
            c1 = new Coords(0, 0);
            c2 = new Coords(0, 0);
        }
    
        @XmlAttribute
        public String getFirst() {
            return this.c1.toString();
        }
    
        @XmlAttribute
        public String getSecond() {
            return this.c2.toString();
        }
    }
    

    Override the toString() method of Coords:

    package model;
    
    import static javax.xml.bind.annotation.XmlAccessType.NONE;
    
    import javax.xml.bind.annotation.XmlAccessorType;
    import javax.xml.bind.annotation.XmlAttribute;
    import javax.xml.bind.annotation.XmlRootElement;
    
    @XmlRootElement
    @XmlAccessorType(NONE)
    public class Coords {
        @XmlAttribute private int x;
        @XmlAttribute private int y;
    
        public Coords(final int x, final int y) {
            this.x = x;
            this.y = y;
        }
    
        public Coords() {
            this.x = 0;
            this.y = 0;
        }
    
        public int getX() {
            return x;
        }
    
        public int getY() {
            return y;
        }
    
        @Override
        public String toString() {
            StringBuilder builder = new StringBuilder();
            builder.append("Coords [x=");
            builder.append(x);
            builder.append(", y=");
            builder.append(y);
            builder.append("]");
            return builder.toString();
        }
    }
    

    This will produce a result similar to this:

    <tuple2 first="Coords [x=2, y=4]" second="Coords [x=12, y=12]"/>
    

    The values of the attributes first and second will be whatever the toString() method of Coords returns.