Jaxb/Moxy Unmarshalling Assigns All Field Values to Map<String,Object> Rather Than the Specific Field Provided for It

JAXB/Moxy Unmarshalling assigns all field values to MapString,Object rather than the specific field provided for it

ah, finally some relief. This issue ate my head a lot but I was finally able to find a workaround. Tried a lot of things and reached out to many people but nothing seems to work and I thought it's an issue from the JAXB/Moxy library. I was able to find a workaround. Hope it helps someone in the future and do not get frustrated like me :)

I used 2 fields one with @XmlAnyElement(lax=true) List<Object> for storing the elements during the marshaling and another Map<String, Object> with custom serialization for JSON. In addition to this, I got to know that we can use beforeMarshal, afterMarshal, beforeUnmarshal, afterMarshal methods. The name itself suggests what it does.

In my case, I used the beforeMarshal method to add the unknown data from my Map<String, Object> to List<Object> so during the marshaling values from List<Object> will be used. I removed the XMLAdapter.

Also, the afterUnmarshal method to add the read unknown elements from List<Object> to Map<String, Object> so Jackson can utilize it and write to JSON using CustomSearlizer.

Basically, it's a kind of hide-and-show approach. List<Object> will be used during the unmarshalling and marshaling by JAXB/Moxy. Map<String, Object> will be used during the serialization and deserialization by Jackson.

Custome.class with my beforeMarshal and afterUnmarshalling: (It seems bit complex basically it exchanges the data as mentioned above. I will have complex data so I need to recursively loop and arrange. You can make changes according to your need)

@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, visible = true, property = "isA")
@JsonInclude(Include.NON_NULL)
@JsonIgnoreProperties(ignoreUnknown = true)
@XmlRootElement(name = "Customer")
@XmlType(name = "Customer", propOrder = {"name", "age", "otherElements"})
@XmlAccessorType(XmlAccessType.FIELD)
@Getter
@Setter
@AllArgsConstructor
@ToString
public class Customer {
@XmlTransient
private String isA;
private String name;
private String age;

@XmlAnyElement(lax = true)
@JsonIgnore
private List<Object> otherElements = new ArrayList<>();

@JsonIgnore
@XmlTransient
private Map<String, Object> userExtensions = new HashMap<>();

@JsonAnyGetter
@JsonSerialize(using = CustomExtensionsSerializer.class)
public Map<String, Object> getUserExtensions() {
return userExtensions;
}

@JsonAnySetter
public void setUserExtensions(String key, Object value) {
userExtensions.put(key, value);
}

private void beforeMarshal(Marshaller m) throws ParserConfigurationException {
System.out.println("Before Marshalling User Extension: " + userExtensions);
ExtensionsModifier extensionsModifier = new ExtensionsModifier();
otherElements = extensionsModifier.Marshalling(userExtensions);
System.out.println("Before Marshalling Final Other Elements " + otherElements);
userExtensions = new HashMap<>();
}

private void afterUnmarshal(Unmarshaller m, Object parent) throws ParserConfigurationException {
System.out.println("After Unmarshalling : " + otherElements);
ExtensionsModifier extensionsModifier = new ExtensionsModifier();
userExtensions = extensionsModifier.Unmarshalling(otherElements);
otherElements = new ArrayList();
}
}

Then the ExtensionsModifier.class which will be called by beforeMarshal and afterUnmarshalling method:

import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class ExtensionsModifier {
private javax.xml.parsers.DocumentBuilderFactory documentFactory;
private javax.xml.parsers.DocumentBuilder documentBuilder;
private org.w3c.dom.Document document;

public ExtensionsModifier() throws ParserConfigurationException {
documentFactory = DocumentBuilderFactory.newInstance();
documentBuilder = documentFactory.newDocumentBuilder();
document = documentBuilder.newDocument();
}

public List<Object> Marshalling(Map<String, Object> userExtensions) throws ParserConfigurationException {
if (userExtensions == null) {
return null;
}
List<Object> tempElement = new ArrayList<>();

for (Map.Entry<String, Object> property : userExtensions.entrySet()) {
Element root = document.createElement(property.getKey());
if (property.getValue() instanceof Map) {
List<Object> mapElements = Marshalling((Map<String, Object>) property.getValue());
mapElements.forEach(innerChildren -> {
if (innerChildren instanceof Element) {
if (((Element) innerChildren).getTextContent() != null) {
root.appendChild(document.appendChild((Element) innerChildren));
}
}
});
tempElement.add(root);
} else if (property.getValue() instanceof String) {
root.setTextContent(((String) property.getValue()));
tempElement.add(root);
} else if (property.getValue() instanceof ArrayList) {
for (Object dupItems : (ArrayList<Object>) property.getValue()) {
if (dupItems instanceof Map) {
Element arrayMap = document.createElement(property.getKey());
List<Object> arrayMapElements = Marshalling((Map<String, Object>) dupItems);
arrayMapElements.forEach(mapChildren -> {
if (mapChildren instanceof Element) {
if (((Element) mapChildren).getTextContent() != null) {
arrayMap.appendChild(document.appendChild((Element) mapChildren));
}
}
});
tempElement.add(arrayMap);
} else if (dupItems instanceof String) {
Element arrayString = document.createElement(property.getKey());
arrayString.setTextContent((String) dupItems);
tempElement.add(arrayString);
}
}
}
}
return tempElement;
}

public Map<String, Object> Unmarshalling(List<Object> value) {
if (value == null) {
return null;
}
final Map<String, Object> extensions = new HashMap<>();
for (Object obj : value) {
org.w3c.dom.Element element = (org.w3c.dom.Element) obj;
final NodeList children = element.getChildNodes();

//System.out.println("Node Name : " + element.getNodeName() + " Value : " + element.getTextContent());
List<Object> values = (List<Object>) extensions.get(element.getNodeName());

if (values == null) {
values = new ArrayList<Object>();
}

if (children.getLength() == 1) {
values.add(element.getTextContent());
extensions.put(element.getNodeName(), values);
} 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) {
List<Object> childElements = new ArrayList();
childElements.add(n);
values.add(Unmarshalling(childElements));
child.add(Unmarshalling(childElements));

}
}
extensions.put(element.getNodeName(), values);
}
}
return extensions;
}
}

Following is my CustomSearlizer which will be used by Jackson to create JSON:

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializerProvider;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Map;

public class CustomExtensionsSerializer extends JsonSerializer<Map<String, Object>> {

private static final ObjectMapper mapper = new ObjectMapper();

@Override
public void serialize(Map<String, Object> value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
System.out.println("Custom Json Searlizer: " + value);
recusiveSerializer(value, gen, serializers);
}

public void recusiveSerializer(Map<String, Object> value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
for (Map.Entry<String, Object> extension : value.entrySet()) {
if (extension.getValue() instanceof Map) {
//If instance is MAP then call the recursive method
recusiveSerializer((Map) extension.getValue(), gen, serializers);
} else if (extension.getValue() instanceof String) {
//If instance is String directly add it to the JSON
gen.writeStringField(extension.getKey(), (String) extension.getValue());
} else if (extension.getValue() instanceof ArrayList) {
//If instance if ArrayList then loop over it and add it to the JSON after calling recursive method
//If size more than 1 add outer elements
if (((ArrayList<Object>) extension.getValue()).size() > 1) {
gen.writeFieldName(extension.getKey());
gen.writeStartObject();
for (Object dupItems : (ArrayList<Object>) extension.getValue()) {
if (dupItems instanceof Map) {
recusiveSerializer((Map) dupItems, gen, serializers);
} else {
gen.writeStringField(extension.getKey(), (String) dupItems);
}
}
gen.writeEndObject();
} else {
for (Object dupItems : (ArrayList<Object>) extension.getValue()) {
if (dupItems instanceof Map) {
gen.writeFieldName(extension.getKey());
gen.writeStartObject();
recusiveSerializer((Map) dupItems, gen, serializers);
gen.writeEndObject();
} else {
gen.writeStringField(extension.getKey(), (String) dupItems);
}
}
}
}
}
}
}

If I provide input as following XML:

<Customer xmlns:google="https://google.com">
<name>Rise Against</name>
<age>2000</age>
<google:main>
<google:sub>MyValue</google:sub>
<google:sub>MyValue</google:sub>
</google:main>
</Customer>

Then I get the following JSON as output:

{
"isA" : "Customer",
"name" : "Rise Against",
"age" : "2000",
"google:main" : {
"google:sub" : "MyValue",
"google:sub" : "MyValue"
}
}

Viceversa would also work fine. Hope it's clear if not leave a comment will try to respond.

JAXB unmarshal undefined elements to Map?

I have a solution for you, but slightly different than what you try above.

Let's take the root class:

@XmlAccessorType(XmlAccessType.FIELD)
@XmlRootElement(name = "ROW")
public class Row {

@XmlAttribute
private int id;
@XmlElement(name = "MOBILE")
private int mobileNo;

@XmlMixed
@XmlAnyElement
@XmlJavaTypeAdapter(MyMapAdapter.class)
private Map<String, String> otherElements;
}

And the adapter for turning the uknown values into a map:

import org.w3c.dom.Document;
import org.w3c.dom.Element;

import javax.xml.bind.annotation.adapters.XmlAdapter;
import javax.xml.parsers.DocumentBuilderFactory;
import java.util.HashMap;
import java.util.Map;

public class MyMapAdapter extends XmlAdapter<Element, Map<String, String>> {

private Map<String, String> hashMap = new HashMap<>();

@Override
public Element marshal(Map<String, String> map) throws Exception {
// expensive, but keeps the example simpler
Document document = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument();

Element root = document.createElement("dynamic-elements");

for(Map.Entry<String, String> entry : map.entrySet()) {
Element element = document.createElement(entry.getKey());
element.setTextContent(entry.getValue());
root.appendChild(element);

}

return root;
}

@Override
public Map<String, String> unmarshal(Element element) {
String tagName = element.getTagName();
String elementValue = element.getChildNodes().item(0).getNodeValue();
hashMap.put(tagName, elementValue);

return hashMap;
}
}

This will put id and mobile number in the fields, and the rest, the uknown into a map.

The marshalling will not be exactly as the xml you showed. It puts a wrapper around the dynamic values and looks like this:

<ROW id="1">
<MOBILE>1241204091</MOBILE>
<dynamic-elements>
<A>1</A>
<B>2</B>
</dynamic-elements>
</ROW>

MOXy XPath unmarshalling Element is null

I made the following changes to your approach, and I am able to access the duration and start_date properties data from your XML file.

I am using OpenJDK 14, by the way. But this approach runs OK using version 8 also.

The POM I am using has the following dependencies:

        <dependency>
<groupId>com.sun.activation</groupId>
<artifactId>javax.activation</artifactId>
<version>1.2.0</version>
</dependency>

<!--
Use 2.3.1 below to prevent "illegal
reflective access operation" warnings.
-->
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.3.1</version>
</dependency>

<dependency>
<groupId>com.sun.xml.bind</groupId>
<artifactId>jaxb-core</artifactId>
<version>2.3.0.1</version>
</dependency>

<dependency>
<groupId>com.sun.xml.bind</groupId>
<artifactId>jaxb-impl</artifactId>
<version>2.3.1</version>
</dependency>

<dependency>
<groupId>org.eclipse.persistence</groupId>
<artifactId>eclipselink</artifactId>
<version>2.7.6</version>
</dependency>

(I skipped the Jackson dependency just for this test.)

I also added the following section at the end of my POM, to handle the properties file:

    <!-- to copy the jaxb.properties file to its class package: -->
<build>
<resources>
<resource>
<directory>src/main/java</directory>
<excludes>
<exclude>**/*.java</exclude>
</excludes>
</resource>
</resources>
</build>

This ensures the properties file is deployed to the correct location with its related class files.

I added the code to check for which JAXB provider is being used - just as a positive confirmation:

    private void checkProvider() throws JAXBException {
JAXBContext jc = JAXBContext.newInstance(Definitions.class);

String jaxbContextImpl = jc.getClass().getName();
if(MOXY_JAXB_CONTEXT.equals(jaxbContextImpl)) {
System.out.println("EclipseLink MOXy");
} else if(METRO_JAXB_CONTEXT.equals(jaxbContextImpl)) {
System.out.println("Metro");
} else {
System.out.println("Other");
}
}

I modified the code to loop through the properties data, to explicitly print the final properties values:

List<Property> props = definitions.getProcess().getTaskList().get(0).getPropertyList();
props.forEach(prop -> {
System.out.println(prop.getName() + " - " + prop.getValue());
});
//System.out.println(definitions.getProcess().getTaskList().get(0).getPropertyList());

The resulting output is:

EclipseLink MOXy
start_date - 01-04-2018
duration - 5

JAXB @XmlAnyElement with XmlAdapter does not make a call to the unmarshal method

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.

How to marshal Map into {key: value, key: value, ...} with MOXy

On a side note, I'm also not able to use the new XmlVariableNode in
Eclipselink 2.6 as I'm only allowed to use stable releases of the API
:(

@XmlVariableNode has also been included in EclipseLink 2.5.1 which is now released:

  • http://www.eclipse.org/eclipselink/downloads/

This annotation is well suited for mapping your use case:

  • http://blog.bdoughan.com/2013/06/moxys-xmlvariablenode-using-maps-key-as.html

With MOXy and XPath, is it possible to unmarshal two lists of attributes?

Short Answer

This isn't a use case that is currently supported with @XmlPath in EclipseLink MOXy. I have entered the following enhancement request for this, feel free to add additional information to to vote for this bug:

  • https://bugs.eclipse.org/355225

Long Answer

MOXy will support mapping:

@XmlPath("items/item/@type")
private ArrayList<String> itemList = new ArrayList<String>();

to:

<test>
<items>
<item type="cookie"/>
<item type="crackers"/>
</items>
</test>

but not:

@XmlPath("items/item/@type")
private ArrayList<String> itemList = new ArrayList<String>();

@XmlPath("items/item/@brand")
private ArrayList<String> brandList = new ArrayList<String>();

to:

<test>
<items>
<item type="cookie" brand="oreo"/>
<item type="crackers" brand="ritz"/>
</items>
</test>

Workaround

You could introduce an intermediate object (Item) to map this use case:

@XmlElementWrapper(name="items")
@XmlElement(name="item")
private ArrayList<Item> itemList = new ArrayList<Item>();

 

public class Item {

@XmlAttribute
private String type;

@XmlAttribute
private String brand;
}

For More Information on @XmlPath

  • http://blog.bdoughan.com/2010/07/xpath-based-mapping.html
  • http://blog.bdoughan.com/2010/09/xpath-based-mapping-geocode-example.html
  • http://blog.bdoughan.com/2011/03/map-to-element-based-on-attribute-value.html
  • http://blog.bdoughan.com/2011/08/binding-to-json-xml-geocode-example.html


Related Topics



Leave a reply



Submit