@XmlAnyElement does not unmarshal into specific Java type, but stop at JAXBElement

3.4k views Asked by At

To learn how to use @XmlAnyElement, I created the following test service:

@WebService(serviceName = "TestServices")
@Stateless()
public class TestServices {
    @WebMethod(operationName = "testMethod")
    public ServiceResult testMethod() {
        ServiceResult result = new ServiceResult();

        result.addObject(new SimpleObj(1, 2));
        result.addObject(new SimpleObj(3, 4));

        return result;
    }
}

SimpleObj is a simple class with 2 int fields. Below is the code for the ServiceResult class:

@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
@XmlSeeAlso({SimpleObj.class})
public class ServiceResult {
    @XmlAnyElement(lax = true)
    private List<Object> body;

    public void addObject(Object objToAdd) {
        if (this.body == null)
            this.body = new ArrayList();

        this.body.add(objToAdd);
    }

    // Getters and Setters
}

To consume the above service, I created an appclient with the following Main class:

public class Main {
    @WebServiceRef(wsdlLocation = "META-INF/wsdl/localhost_8080/TestServices/TestServices.wsdl")
    private static TestServices_Service service;
    private static TestServices         port;

    /**
     * @param args the command line arguments
     */
    public static void main(String[] args) {
        port = service.getAdminServicesPort();
        ServiceResult result = port.testMethod();

        for (Object o : result.getAny()) {
            System.out.println("TEST: " + o);
        }
    }
}

Based on the documentation, with @XmlAnyElement, the unmarshaller will eagerly unmarshal this element to a JAXB object. However, what I observed is that JAXB only parsed my object into JAXBElement instead of going all the way into SimpleObj.

I'd be extremely grateful if you could show me how I can get SimpleObj out of the ServiceResult.

UPDATE:

Below is the SimpleObj class:

@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
public class SimpleObj {
    private int a;
    private int b;

    public SimpleObj() {}

    public SimpleObj(int a, int b) {
        this.a = a;
        this.b = b;
    }

    // Getters and Setters
}
1

There are 1 answers

2
bdoughan On BEST ANSWER

I am unable to reproduce the issue that you are seeing. Below is some demo code that interacts directly with JAXB.

import java.io.*;
import javax.xml.bind.*;

public class Demo {

    public static void main(String[] args) throws Exception {
        JAXBContext jc = JAXBContext.newInstance(ServiceResult.class);

        Unmarshaller unmarshaller = jc.createUnmarshaller();
        StringReader xml = new StringReader("<serviceResult><simpleObj/><simpleObj/></serviceResult>");
        ServiceResult result = (ServiceResult) unmarshaller.unmarshal(xml);

        for(Object item : result.getBody()) {
            System.out.println(item.getClass());
        }
    }

}

The output from running the demo code shows that it is instances of SimpleObj in the field annotated with @XmlAnyElement(lax=true).

class forum27871349.SimpleObj
class forum27871349.SimpleObj

UPDATE #1

On the side note, I've read your blog articles on @XmlAnyElement and I've never seen you had to include @XmlSeeAlso({SimpleObj.class}) in any of your examples.

I'm not sure why I never leverage @XmlSeeAlso in my examples.

However, in my case, if I don't have this, I would have the error saying Class *** nor any of its super class is known to this context. It'd be great if you could also show me if there is a way to make all of these classes known to the consumer without using @XmlSeeAlso

When you are creating the JAXBContext yourself, you simply need to include anything you would have referenced in an @XmlSeeAlso annotation as part of the classes you used to bootstrap the JAXBContext.

JAXBContext jc = JAXBContext.newInstance(ServiceResult.class, SimpleObj.class);

In a JAX-WS (or JAX-RS) setting where you don't have direct access to the JAXBContext I would recommend using the @XmlSeeAlso annotation like you have done.


UPDATE #2

Regarding the @XmlAnyElement, from the documentation, I thought if the unmarshaller cannot unmarshal elements into JAXB objects or JAXBElement objects, I will at least get a DOM node.

When you have a property mapped with @XmlAnyElement(lax=true) the following will happen:

  1. If the element corresponds to the @XmlRootElement of a class, then you will get an instance of that class.
  2. If the element corresponds to the @XmlElementDecl of a class on the ObjectFactory or another class annotated with @XmlRegistry then you will get an instance of that class wrapped in an instance of JAXBElement.
  3. If JAXB does not have an association between the element and a class, then it will convert it to a DOM Element.

I will demonstrate below with an example.

ObjectFactory

import javax.xml.namespace.QName;
import javax.xml.bind.JAXBElement;
import javax.xml.bind.annotation.*;

@XmlRegistry
public class ObjectFactory {

    @XmlElementDecl(name="simpleObjJAXBElement")
    public JAXBElement<SimpleObj> createSimpleObj(SimpleObj simpleObj) {
        return new JAXBElement<SimpleObj>(new QName("simpleObjJAXBElement"), SimpleObj.class, simpleObj);
    }

}

Demo

import java.io.*;
import javax.xml.bind.*;

public class Demo {

    public static void main(String[] args) throws Exception {
        JAXBContext jc = JAXBContext.newInstance(ServiceResult.class, ObjectFactory.class);

        Unmarshaller unmarshaller = jc.createUnmarshaller();
        StringReader xml = new StringReader("<serviceResult><simpleObj/><unmapped/><simpleObjJAXBElement/></serviceResult>");
        ServiceResult result = (ServiceResult) unmarshaller.unmarshal(xml);

        for(Object item : result.getBody()) {
            System.out.println(item.getClass());
        }
    }

}

Output

class forum27871349.SimpleObj
class com.sun.org.apache.xerces.internal.dom.ElementNSImpl
class javax.xml.bind.JAXBElement