Marshall/Unmarshall Nested Map with JaxB - java

There are several previous questions around using JaxB to marshall/unmarshall a java.util.Map, many of which get pointed back to this example, which works great:
http://blog.bdoughan.com/2013/03/jaxb-and-javautilmap.html
However, I can't get JaxB to be able to marshall/unmarshall instances of Map if the map is not a member of the #XmlRootElement. For example, here's a root element class,
#XmlRootElement
#XmlAccessorType(XmlAccessType.FIELD)
public static class Customer {
private MyField myField
MyField getMyField() {
return myField
}
void setMyField(MyField myField) {
this.myField = myField
}
}
The definition of it's field class:
#XmlAccessorType(XmlAccessType.FIELD)
public static class MyField{
Map<String, String> getSomeMap() {
return someMap
}
void setSomeMap(Map<String, String> someMap) {
this.someMap = someMap
}
#XmlElement
private Map<String, String> someMap = new HashMap<String, String>()
}
And some code to drive the marshalling:
JAXBContext jc = JAXBContext.newInstance(Customer.class)
Customer customer = new Customer()
MyField myField1 = new MyField()
myField1.someMap.put("foo", "bar")
myField1.someMap.put("baz", "qux")
customer.myField = myField1
Marshaller marshaller = jc.createMarshaller()
marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true)
marshaller.marshal(customer, System.out)
This example results in:
java.util.Map is an interface, and JAXB can't handle interfaces.
java.util.Map does not have a no-arg default constructor.
I am writing my code in Groovy rather than Java, but I don't think it should make much of a difference.

I was able to encounter the same behavior using JAXB by creating a TestController of type #RestController, using Spring Boot.
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
#RestController
#RequestMapping(value = "test")
class TestController {
#RequestMapping(value = "findList")
List findList() {
["Test1", "Test2", "Test3"] as ArrayList<String>
}
#RequestMapping(value = "findMap")
Map findMap() {
["T1":"Test1", "T2":"Test2", "T3":"Test3"] as HashMap<String,String>
}
#RequestMapping(value = "")
String find(){
"Test Something"
}
}
With JAXB as the default implementation in SpringBoot, I could reproduce the issue that the /test/findList would correctly render XML, but /test/findMap would generate an error as described in the initial posting.
For me, the solution to the problem is to switch the XML rendering library to Jackson (there are others like XStream as well).
Using Gradle for the build file (build.gradle), I simply add the Jackson dependencies, very similar to how you would if using Maven:
'com.fasterxml.jackson.core:jackson-core:2.7.1',
'com.fasterxml.jackson.core:jackson-annotations:2.7.1',
'com.fasterxml.jackson.core:jackson-databind:2.7.1-1',
'com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.7.1',
'org.codehaus.woodstox:woodstox-core-asl:4.4.1',

I have experienced this before myself. Bottom line is that the warning is telling you exactly the problem. You have defined your field as type java.util.Map. JAXB does not support interfaces. To correct your problem, you need to change the declaration of your field to a concrete Map type like:
private HashMap<String, String> someMap = new HashMap<String, String>()
Your other option is described in the link you referenced. You need to have a
MapAdapter class as referenced in the link you provided and then include that in the annotation, hinting to JAXB how it should marshal/unmarshal the Map type.
I think this link gives a clearer example of how to create and implement the MapAdapter:
JAXB: how to marshall map into <key>value</key>

The answer to the specific issue I was having ended up being removing the #XmlElement annotation from the Map field like so:
#XmlAccessorType(XmlAccessType.FIELD)
public static class MyField{
Map<String, String> getSomeMap() {
return someMap
}
void setSomeMap(Map<String, String> someMap) {
this.someMap = someMap
}
//#XmlElement Remove this annotation
private Map<String, String> someMap = new HashMap<String, String>()
}
Without that annotation, the marshalling/unmarshalling works fine, and still interprets the Map as an XmlElement - there seems to be a bug with that annotation specifically. However, as #dlcole points out, an alternative (that would allow you to have more control over the format of your serialized representation) is to use Jackson rather than JAXB.

Related

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

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:
Why my XMLAdapter TestAdapter.class unmarshal method is not being called during the unmarshalling?
How can I unmarshal the XML fields which can appear randomly with namespaces?
Am I doing something wrong or is there something else I should do to read the namespaces and elements which appear dynamically?
*** 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);
}
}
The Map type of your others property is not suitable for #XmlAnyElement.
According to the its javadoc the #XmlAnyElement annotation
is meant to be used with a List or an array property
(typically with a List or array of org.w3c.dom.Element).
May be you have confused this with the #XmlAnyAttribute annotation
which indeed is used with a Map property.
Hence in your Customer class the others property without using an adapter would look like this:
name;
#XmlAnyElement(lax = true)
private List<Element> others = new ArrayList<>();
And the others property with using an adapter should look like this:
#XmlAnyElement(lax = true)
#XmlJavaTypeAdapter(TestAdapter.class)
private List<MyObject> others = new ArrayList<>();
When doing this way, then JAXB will actually call your adapter.
The adapter's job is to transform between Element and MyObject.
public class TestAdapter extends XmlAdapter<Element, MyObject> {
#Override
public MyObject unmarshal(Element v) throws Exception {
...
}
#Override
public Element marshal(MyObject v) throws Exception {
...
}
}
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.

Reading yaml config

I've been trying to wrap my head around this for hours but it doesn't make much sense what I'm doing wrong. I am trying to create a Map object in Java from a .yml file. Reason being for a map, I don't know what/how many children will appear under "present", so I rather have a dynamic way of creating a map object out of that..
Below is my .yml file. I would like the key-value pair under "present":
present:
now: LOCAL TESTING
later: testing
Below is my config class (everything commented out is what I've tried - in different combinations):
//#Data
#Component
#EnableConfigurationProperties
#ConfigurationProperties(prefix = "present")
//#ConfigurationProperties
public class stat {
//#Getter
//#Data
#Value("${present}")
private Map<String, String> present;
//private Map<String, String> present = new HashMap<String, String>();
}
I tried looking at other SO posts and I feel like I understand it but my Spring Boot (v1.5.8) application isn't seeing that value. It keeps throwing an error for me or the map object is null (or not being populated).
I know that I can read values from this .yml file because if I try to obtain a single value with a code snippet below, it works:
#Data
#Value("${present.now}")
private String status; // String value "LOCAL TESTING"
Here are the other links that I've tried:
Spring Boot yaml configuration for a list of strings
how to convert yml file to java pojo
Am I missing something obvious? Thanks!
You can try to create a POJO to represent the yml structure you are trying to read from.
For example:
#Configuration
#ConfigurationProperties(prefix = "present")
#Data
public class Present {
private String now;
private String later;
}
So I figured it out (for those who have this problem later):
#Value isn't needed and the prefix param in the #ConfigurationProperties isn't needed.
Then you need to have a getter method for the fields that you want - I thought the Lombok library had autogenerated these but I was wrong (probably need to read more about it later - #Setter and #Data won't work properly).
So it should look something like this:
#Component
#EnableConfigurationProperties
#ConfigurationProperties
public class stat {
private Map<String, String[]> present = new HashMap<String, String[]>();
public Map<String, String[]> getPresent() {
return present;
}
}
Now let's give a more complex example (nested maps). Say that my .yml file looks like this:
parent:
present:
foo: dey, tok
bar: ar, jerbs
later:
foo: day, dok
mar: r, darbs
The POJO would look like this:
#Component
#EnableConfigurationProperties
#ConfigurationProperties
public class stat {
private Map<String, Map<String, String[]>> parent = new HashMap<String, Map<String, String[]>>();
public Map<String, Map<String, String[]>> getParent() {
return parent;
}
}
Another key thing to note is that the field that you are obtaining a value from must match the variable name - it may not matter if you use prefix but it still didn't work for me. Hope this helps.

Entity to Map<String, Object>

I would like to clarify if it is possible to create Map<String, Object> from JPA entity through Hibernate. I mean is it possible to convert persistent object (entity) to Map that contains all entity properties as keys and properties' values as values. I understand that properties can be retrieved through Reflections but I can't figure out how to map it with proper values. I found only one solution and it is to use Spring's JdbcTemplate but it is not an option in my case. If anyone have possible solution please let me know. Thank you in advance.
You could try using #Converter as follows
#Entity
public class SomeEntity{
#Id
//...
#Convert(converter = MyConverter.class)
Map<String,Object> map;
}
and build up your converter as you wish for example convert to/from json
#Converter
public class MyConverter implements
AttributeConverter<Map<String, Object>, String> {
#Override
public String convertToDatabaseColumn(Map<String, Object> map) {
return jsonStr(map);
}
#Override
public Map<String, Object> convertToEntityAttribute(String s) {
return mapFromJson(s);
}
You can access the properties through reflection with the assistance of the Apache BeanUtil library, if your entity follows JavaBean naming conventions for all of its properties.
Step one: Wrap your entity in a WrapDynaBean object.
Step two: Create a DynaBeanPropertyMapDecorator, further wrapping the DynaBean.
Step three: Well, that's it. DynaBeanPropertyMapDecorator implemements Map<String, Object> so your job is done there. getFoo() on your original object may now be found by decorator.get("foo"). (Note that you've lost type-safety here, but you did ask for a Map<String, Object>...)

Jackson JSON serialization in Java - write k/v pairs to root instead of nested in property name

I need to write a JSON string that follows this basic format:
{
"xmlns": {
"nskey1" : "nsurl1",
"nskey2" : "nsurl2"
},
"datakey1": "datavalue1",
"datakey2": "datavalue2"
}
I'm using the following class to present the data, and an instance of this class is serialized with the Jackson ObjectMapper.
public class PayloadData {
public Map<String, String> payloadData = new TreeMap<String, String>();
#JsonProperty("xmlns")
public Map<NamespaceEnum, String> namespaces = new TreeMap<NamespaceEnum, String>();
#JsonAnyGetter
public Map<String, String> getPayloadData() {
return payloadData;
}
}
If I serialize an instance of this class as-is, the resulting JSON will be something like this:
{
"xmlns": {
"nskey1" : "nsurl1",
"nskey2" : "nsurl2"
},
"payloadData": {
"datakey1": "datavalue1",
"datakey2": "datavalue2"
},
"datakey1": "datavalue1",
"datakey2": "datavalue2"
}
That makes sense based on the naming conventions, but I'm looking for a method to have the payloadData map placed in the JSON's root context without the duplicate that contains the property identifier and nesting. I've tried a lot of annotations in various forms; I've tried disabling the ObjectMapper wrap_root_value SerializationFeature; I honestly feel like I've tried just about everything. So before I throw a computer out the window, I'm asking for a second (and beyond) set of eyes to help point out what must be painfully obvious.
Thanks in advance for your attention and advice.
edit: updated the actual output JSON I see now. The data is being duplicated, and I'd like to remove the duplicate that has the nested property.
The problem is that you have 2 accessors exposed for PayloadData: the public property and the getter, so it is being serialized twice. If it is possible, I would recommend restructuring your data class to be immutable. For example:
public class PayloadData {
private final Map<String, String> payloadData;
private final Map<NamespaceEnum, String> namespaces;
#JsonCreator
public PayloadData(#JsonProperty("xmlns") Map<NamespaceEnum, String> namespaces,
#JsonProperty("payloadData") Map<String, String> payloadData) {
this.namespaces = namespaces;
this.payloadData = payloadData;
}
#JsonAnyGetter
public Map<String, String> getPayloadData() {
return payloadData;
}
#JsonProperty("xmlns")
public Map<NamespaceEnum, String> getNamespaces() {
return namespaces;
}
}
This will give you the desired output with out any configuration of the ObjectMapper.
You can parse your json data to a HashMap not a class object:
public HashMap<String, Object> testJackson(String data) throws IOException {
JsonFactory factory = new JsonFactory();
ObjectMapper mapper = new ObjectMapper(factory);
TypeReference<HashMap<String,Object>> typeRef
= new TypeReference<HashMap<String,Object>>() {};
HashMap<String,Object> o = mapper.readValue(data, typeRef);
return o
}
Get JSON data from a HashMap:
public String getJsonFromMap(HashMap<String, Object> data) {
return new ObjectMapper().writeValueAsString(data);
}

Correct data type with Swagger/Springfox when property has #XmlJavaTypeAdapter annotation

I have annotated an endpoint with swagger annotations. In the #ResponseHeader I set the returning class as response. This class contains a property which is annotated with #XmlJavaTypeAdapter. The adapter is changing the data type of the property. Unfortunately Swagger shows the type of the property, not the return type of the Adapter. Is it possible to do this?
What I already tried is to annotate the property with #ApiModelProperty(). But it was not possible for me to set the dataType to List (Primitive data types or just a list was working).
Thanks :)
The following was not working:
#ApiModelProperty(dataType = "List<Map<String, String>>")
public Map<String, String> someMap = new HashMap<>();
I had to create an Interface
public interface ListOfMap extends List<Map<String, String>> {}
And then I used this interface in the ApiModelProperty:
#ApiModelProperty(dataType = "ListOfMap")
public Map<String, String> someMap = new HashMap<>();
With this, everything worked :)

Categories