I am trying to unmarshal the XML
and map it to the Java POJO. My XML can have some of the user-defined elements which can be random so I would like to store them. After researching I found that I can use @XmlAnyElement(lax=true)
. I am trying to use the XMLAdapter
along with the @XmlAnyElement
but for some reason, the method unmarshal
within my XMLAdapter
is not being called at all due to which I am unable to map the user-defined
fields.
Can someone please explain to me how to unmarshal
the user-defined
fields to my Map<String,Object>
for the marshalling
everything is working fine and I am using the approach mentioned here. But unmarshalling
is not being called at all which is bit confusing for me.
Following is my XML
which needs to be unmarshalled
. (Please note that the namespace
can be dynamic and user-defined which may change in every xml):
<Customer xmlns:google="https://google.com">
<name>Batman</name>
<google:main>
<google:sub>bye</google:sub>
</google:main>
</Customer>
Following is my Customer
class to which XML
needs to be unmarshalled
;
@XmlRootElement(name = "Customer")
@XmlType(name = "Customer", propOrder = {"name", "others"})
@XmlAccessorType(XmlAccessType.FIELD)
public class Customer {
private String name;
@XmlAnyElement(lax = true)
@XmlJavaTypeAdapter(TestAdapter.class)
private Map<String, Object> others = new HashMap<>();
//Getter Setter and other constructors
}
Following is my XMLAdapter (TestAdapter)
class which will be used for marshalling
and unmarshalling
the user-defined
fields. The unmarshalling
method is not being called at all. However the marshalling
method works as expected based on the code provided here.
class TestAdapter extends XmlAdapter<Wrapper, Map<String,Object>> {
@Override
public Map<String,Object> unmarshal(Wrapper value) throws Exception {
//Method not being called at all the following SYSTEM.OUT is NOT PRINTED
System.out.println("INSIDE UNMARSHALLING METHOD TEST");
System.out.println(value.getElements());
return null;
}
@Override
public Wrapper marshal(Map<String,Object> v) throws Exception {
return null;
}
}
class Wrapper {
@XmlAnyElement
List elements;
}
I have used the package-info.java
file and it has been filled with following contents:
@jakarta.xml.bind.annotation.XmlSchema(namespace = "http://google.com", elementFormDefault = jakarta.xml.bind.annotation.XmlNsForm.QUALIFIED)
package io.model.jaxb;
I researched a lot but could not find any answer which does something similar. Also, tried a lot of things but none worked. Hence, posting the same here and looking for some suggestion or workaround.
I have few doubts with regards to unmarshalling
:
XMLAdapter TestAdapter.class
unmarshal
method is not being called during the unmarshalling?unmarshal
the XML fields which can appear randomly with namespaces?*** FOLLOWING IS EDITED SECTION BASED ON RESPONSE FROM Thomas Fritsch ****
Based on the response I have edited my class but still not working as expected:
@XmlRootElement(name = "Customer")
@XmlType(name = "Customer", propOrder = {"name", "others"})
@XmlAccessorType(XmlAccessType.FIELD)
public class Customer {
private String name;
@JsonIgnore
@XmlJavaTypeAdapter(TestAdapter.class)
private List<Object> others;
@XmlTransient
@XmlAnyElement(lax = true)
private List<Element> another = new ArrayList<>();
}
So what's happening is that if I use @XmlTransient
then the field another
will not be populated during the unmarshalling
and I need to keep it @XmlTransient
because I do not want it during the marshalling
similarly I have made @JsonIgnore
for Map<String, Object> others
because I do not need it during the unmarshalling
but both things are conflicting with each other and not able to obtain the the required output.
My main goal is to convert the XML
file to JSON
and vice versa. For the above mentioned XML
file I would like to obtain the following output in JSON
:
{
"Customer": {
"name": "BATMAN",
"google:main": {
"google:sub": "bye"
}
}
}
Similarly when I convert this JSON
then I should get the original XML
.
Following is my Main.class
:
class Main {
public static void main(String[] args) throws JAXBException, XMLStreamException, JsonProcessingException {
//XML to JSON
JAXBContext jaxbContext = JAXBContext.newInstance(Customer.class);
Unmarshaller unmarshaller = jaxbContext.createUnmarshaller();
InputStream inputStream = Main.class.getClassLoader().getResourceAsStream("Customer.xml");
final XMLInputFactory xmlInputFactory = XMLInputFactory.newInstance();
final XMLStreamReader streamReader = xmlInputFactory.createXMLStreamReader(inputStream);
final Customer customer = unmarshaller.unmarshal(streamReader, Customer.class).getValue();
final ObjectMapper objectMapper = new ObjectMapper();
final String jsonEvent = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(customer);
System.out.println(jsonEvent);
//JSON to XML
Marshaller marshaller = jaxbContext.createMarshaller();
marshaller.setProperty(Marshaller.JAXB_FRAGMENT, Boolean.TRUE);
marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);
marshaller.marshal(customer, System.out);
}
}
After trying out a lot of things, I was able to get it working. Posting the solution for the same so it can be helpful to someone in the future.
I have used Project Lombok
so you can see some additional annotations such as @Getter, @Setter, etc
Method-1:
As mentioned @Thomas Fritsch you have to use the @XmlAnyElement(lax=true)
with List<Element>
I was using with the Map<String, Object>
.
Method-2:
You can continue to use Map<String,Object>
and use @XmlPath(".")
with adapter to get it working. Posting the code for the same here: (I have added one additional field age
other that everything remain the same)
@XmlRootElement(name = "Customer")
@XmlType(name = "Customer", propOrder = {"name", "age", "others"})
@XmlAccessorType(XmlAccessType.FIELD)
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class Customer {
@XmlElement(name = "name")
private String name;
private String age;
@XmlJavaTypeAdapter(TestAdapter.class)
@XmlPath(".")
private Map<String, Object> others;
}
Following is the TestAdapter.class
posting the same for reference. When you unmarhsal
the method unmarshal
in TestAdapter
will get called and you can do anything you need.
class TestAdapter extends XmlAdapter<Wrapper, Map<String, Object>> {
@Override
public Map<String, Object> unmarshal(Wrapper value) throws Exception {
System.out.println("INSIDE UNMARSHALLING METHOD TEST");
final Map<String, Object> others = new HashMap<>();
for (Object obj : value.getElements()) {
final Element element = (Element) obj;
final NodeList children = element.getChildNodes();
//Check if its direct String value field or complex
if (children.getLength() == 1) {
others.put(element.getNodeName(), element.getTextContent());
} else {
List<Object> child = new ArrayList<>();
for (int i = 0; i < children.getLength(); i++) {
final Node n = children.item(i);
if (n.getNodeType() == Node.ELEMENT_NODE) {
Wrapper wrapper = new Wrapper();
List childElements = new ArrayList();
childElements.add(n);
wrapper.elements = childElements;
child.add(unmarshal(wrapper));
}
}
others.put(element.getNodeName(), child);
}
}
return others;
}
@Override
public Wrapper marshal(Map<String, Object> v) throws Exception {
Wrapper wrapper = new Wrapper();
List elements = new ArrayList();
for (Map.Entry<String, Object> property : v.entrySet()) {
if (property.getValue() instanceof Map) {
elements.add(new JAXBElement<Wrapper>(new QName(property.getKey()), Wrapper.class, marshal((Map) property.getValue())));
} else {
elements.add(new JAXBElement<String>(new QName(property.getKey()), String.class, property.getValue().toString()));
}
}
wrapper.elements = elements;
return wrapper;
}
}
@Getter
class Wrapper {
@XmlAnyElement
List elements;
}
Although it works for specific cases, I am seeing one problem by using this approach. I have created a new post for this issue. If I get any response for that issue then I will try to update the code so it can work correctly.