Resolving cyclic references in jaxb

4.1k views Asked by At

I'm dealing with some cyclic references while implementing my project's web service layer. I'm using jaxb (latest version, 2.2.7) and even I had a look to some tips as here and here I can't get it working. That's a basic SSCCE about my problem:

/*
* The service interface
*/
@WebService
public interface IMyWS {

    @WebMethod
    public List<Class1> cyclicTest();

}

/*
* Interface implementation
*/
@WebService(endpointInterface = "com.mycompany.ws.interfaces.IMyWS")
public class MyWS implements IMyWS {

    @XmlRootElement
    public static class Class1 {

        @XmlTransient
        private Class2 class2;

        public Class1() {

        }

        public Class1(Class2 refClass) {
            class2 = refClass;
        }

        public Class2 getClass2() {
            return class2;
        }

        public void setClass2(Class2 class2) {
            this.class2 = class2;
        }

        @Override
        public String toString() {
            return this.getClass().getSimpleName();
        }

    }

    @XmlRootElement
    public static class Class2 {

        private Class1 class1;

        public Class2() {

        }

        public Class1 getClass1() {
            return class1;
        }

        public void setClass1(Class1 class1) {
            this.class1 = class1;
        }

        @Override
        public String toString() {
            return this.getClass().getSimpleName();
        }
    }

    @Override
    public List<Class1> cyclicTest() {
        //I create an instance of each class, having them a cyclic reference to the other instance
        Class2 class2 = new Class2();
        Class1 class1 = new Class1(class2);
        class2.setClass1(class1);
        return Arrays.asList(class1);
    }

}

And the exception I'm actually dealing with when calling cyclicTest():

Caused by: javax.xml.bind.MarshalException
 - with linked exception:
[com.sun.istack.SAXException2: Se ha detectado un ciclo en el gráfico de objeto. Esto provocará un XML con profundidad infinita: Class1 -> Class2 -> Class1]
    at com.sun.xml.bind.v2.runtime.MarshallerImpl.write(MarshallerImpl.java:326)
    at com.sun.xml.bind.v2.runtime.MarshallerImpl.marshal(MarshallerImpl.java:178)
    at org.apache.cxf.jaxb.JAXBEncoderDecoder.writeObject(JAXBEncoderDecoder.java:537)
    at org.apache.cxf.jaxb.JAXBEncoderDecoder.marshall(JAXBEncoderDecoder.java:233)
    ... 50 more
Caused by: com.sun.istack.SAXException2: Se ha detectado un ciclo en el gráfico de objeto. Esto provocará un XML con profundidad infinita: Class1 -> Class2 -> Class1
    at com.sun.xml.bind.v2.runtime.XMLSerializer.reportError(XMLSerializer.java:249)
    at com.sun.xml.bind.v2.runtime.XMLSerializer.pushObject(XMLSerializer.java:537)
    at com.sun.xml.bind.v2.runtime.XMLSerializer.childAsXsiType(XMLSerializer.java:631)
    at com.sun.xml.bind.v2.runtime.property.SingleElementNodeProperty.serializeBody(SingleElementNodeProperty.java:158)
    at com.sun.xml.bind.v2.runtime.ClassBeanInfoImpl.serializeBody(ClassBeanInfoImpl.java:361)
    at com.sun.xml.bind.v2.runtime.XMLSerializer.childAsXsiType(XMLSerializer.java:696)
    at com.sun.xml.bind.v2.runtime.property.SingleElementNodeProperty.serializeBody(SingleElementNodeProperty.java:158)
    at com.sun.xml.bind.v2.runtime.ClassBeanInfoImpl.serializeBody(ClassBeanInfoImpl.java:361)
    at com.sun.xml.bind.v2.runtime.XMLSerializer.childAsXsiType(XMLSerializer.java:696)
    at com.sun.xml.bind.v2.runtime.property.ArrayElementNodeProperty.serializeItem(ArrayElementNodeProperty.java:69)
    at com.sun.xml.bind.v2.runtime.property.ArrayElementProperty.serializeListBody(ArrayElementProperty.java:172)
    at com.sun.xml.bind.v2.runtime.property.ArrayERProperty.serializeBody(ArrayERProperty.java:159)
    at com.sun.xml.bind.v2.runtime.ClassBeanInfoImpl.serializeBody(ClassBeanInfoImpl.java:361)
    at com.sun.xml.bind.v2.runtime.XMLSerializer.childAsXsiType(XMLSerializer.java:696)
    at com.sun.xml.bind.v2.runtime.ElementBeanInfoImpl$1.serializeBody(ElementBeanInfoImpl.java:156)
    at com.sun.xml.bind.v2.runtime.ElementBeanInfoImpl$1.serializeBody(ElementBeanInfoImpl.java:131)
    at com.sun.xml.bind.v2.runtime.ElementBeanInfoImpl.serializeBody(ElementBeanInfoImpl.java:333)
    at com.sun.xml.bind.v2.runtime.ElementBeanInfoImpl.serializeRoot(ElementBeanInfoImpl.java:340)
    at com.sun.xml.bind.v2.runtime.ElementBeanInfoImpl.serializeRoot(ElementBeanInfoImpl.java:76)
    at com.sun.xml.bind.v2.runtime.XMLSerializer.childAsRoot(XMLSerializer.java:494)
    at com.sun.xml.bind.v2.runtime.MarshallerImpl.write(MarshallerImpl.java:323)
    ... 53 more

I think I have the proper annotations set. What am I actually missing?

3

There are 3 answers

3
willome On BEST ANSWER

As you add @XmlTransient on a private property, you should change your XmlAccessType (default to XmlAccessType.PUBLIC_MEMBER) to XmlAccessType.PROPERTY

PUBLIC_MEMBER is the default access type in JAXB. It means that a JAXB implementation will generate bindings for: public fields, annotated fields, properties

code copied from W A :

@WebService(endpointInterface = "com.mycompany.ws.interfaces.IMyWS")
public class MyWS implements IMyWS {

@XmlRootElement
@XmlAccessorType(XmlAccessType.PROPERTY)
public static class Class1 {

    private Class2 class2;

    public Class1() {

    }

    public Class1(Class2 refClass) {
        class2 = refClass;
    }

    @XmlTransient
    public Class2 getClass2() {
        return class2;
    }

    public void setClass2(Class2 class2) {
        this.class2 = class2;
    }

    @Override
    public String toString() {
        return this.getClass().getSimpleName();
    }

}

@XmlRootElement
public static class Class2 {

    private Class1 class1;

    public Class2() {

    }

    public Class1 getClass1() {
        return class1;
    }

    public void setClass1(Class1 class1) {
        this.class1 = class1;
    }

    @Override
    public String toString() {
        return this.getClass().getSimpleName();
    }
}

public List<Class1> cyclicTest() {
    //I create an instance of each class, having them a cyclic reference to the other instance
    Class2 class2 = new Class2();
    Class1 class1 = new Class1(class2);
    class2.setClass1(class1);
    return Arrays.asList(class1);
}


public static void main(String[] args) throws JAXBException {

    JAXBContext ctx = JAXBContext.newInstance(Class1.class);
    Marshaller m = ctx.createMarshaller();
    m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
    List<Class1> class1s = new MyWs().cyclicTest();

    for (Class1 c1 : class1s){
        m.marshal(c1, System.out);
    }

}
1
W Almir On

Using @XmlID and @XmlIDREF should solve your problem.

Here's what I did:

I added a String field to Class 1 to be used as an id, and annotated it with @XmlID. Then, I annotated the setClass1() method with @XmlIDREF. My test is in the main method below.

/*
 * Interface implementation
 */
@WebService(endpointInterface = "com.mycompany.ws.interfaces.IMyWS")
public class MyWS implements IMyWS {

    @XmlRootElement
    public static class Class1 {

        @XmlID
        private String id;

        private Class2 class2;

        public Class1() {
            this.id = UUID.randomUUID().toString();
        }

        public Class1(Class2 refClass) {
            this();
            class2 = refClass;
        }

        public Class2 getClass2() {
            return class2;
        }

        public void setClass2(Class2 class2) {
            this.class2 = class2;
        }

        @Override
        public String toString() {
            return this.getClass().getSimpleName();
        }

    }

    @XmlRootElement
    public static class Class2 {

        private Class1 class1;

        public Class2() {

        }

        public Class1 getClass1() {
            return class1;
        }

        @XmlIDREF
        public void setClass1(Class1 class1) {
            this.class1 = class1;
        }

        @Override
        public String toString() {
            return this.getClass().getSimpleName();
        }
    }

    @Override
    public List<Class1> cyclicTest() {
        //I create an instance of each class, having them a cyclic reference to the other instance
        Class2 class2 = new Class2();
        Class1 class1 = new Class1(class2);
        class2.setClass1(class1);
        return Arrays.asList(class1);
    }

    public static void main(String[] args) throws JAXBException {

        JAXBContext ctx = JAXBContext.newInstance(Class1.class);
        Marshaller m = ctx.createMarshaller();
        m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
        List<Class1> class1s = new MyWS().cyclicTest();

        for (Class1 c1 : class1s){
            m.marshal(c1, System.out);
        }

    }

}

Output:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<class1>
    <id>e9e53e73-9a96-4c7c-b919-3ed3d7aa4c5e</id>
    <class2>
        <class1>e9e53e73-9a96-4c7c-b919-3ed3d7aa4c5e</class1>
    </class2>
</class1>
0
astraujums On

Adding to the answer by W Almir, to make generation of unique IDs for objects simpler (no need to add code to constructors), annotate a method that returns the id. For example:

@XmlID
public String getId()
{
    return Integer.toString(System.identityHashCode(this));
}

This seems to work not only for converting cyclical graphs of Java objects to XML but also for converting XML to cyclical graphs of Java objects.