Mapping freeform XML/JSON to Moxy/JAXB annotated class

1.9k Views Asked by At

I'm trying to find a way to correctly map the following XML/JSON document to an equivalent JAXB/Moxy annotated class.
NOTE that the model element of the document, which in my example describes a person, is freeform, i.e. might be any kind of XML element/JSON object, which is not statically known.

XML document:

<form>
   <title>Person Form</title>
   <model>
      <person>
         <name>John</name>
         <surname>Smith</surname>
         <address>
           <street>Main St.</street>
           <city>NY</city>
           <country>USA</country>
         </address>
     <person>
   </model>
</form>

Equivalent JSON document:

{  
   "title":"Form Title",  
   "model":{  
      "person":{  
         "name":"John",  
         "surname":"Smith",  
         "address":{  
            "street":"Main St.",  
            "city":"NY",  
            "country":"USA"  
         }          
      }  
   }
}

I thought to map the model field as a Map, where the values might be primitive types or Map themselves. This mapping would be enough expressive for my needs.

I tried to play with the @XmlReadTransformer, @XmlWriteTransformer MOXY annotations, but with no success (the record parameter I get in the buildAttributeValue is always null)

@XmlRootElement @XmlAccessorType(XmlAccessType.FIELD) 
public class Form {
   private String title; 
   private Model model;
   ....getters and setters....
}

@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
public class Model {

    @XmlElement
    @XmlReadTransformer(transformerClass = Transformer.class)
    @XmlWriteTransformers({ @XmlWriteTransformer(xmlPath = "./*", transformerClass = Transformer.class) })
    private Map<String, Object> data;

    public Map<String, Object> getData() {
        return data;
    }

    public void setData(Map<String, Object> data) {
        this.data = data;
    }

    public static class Transformer implements AttributeTransformer, FieldTransformer {

        private AbstractTransformationMapping tm;

        public Transformer() {
        }

        @Override
        public void initialize(AbstractTransformationMapping tm) {
            this.tm = tm;
        }

        @Override
        public Map<String, Object> buildAttributeValue(Record r, Object o,
                Session s) {
            Map<String, Object> data = new HashMap<String, Object>();
            // TODO: ????
            return data;
        }

        @Override
        public Object buildFieldValue(Object arg0, String arg1, Session arg2) {
// TODO
            return null;
        }

    }
}

Can you suggest me a proper way of solve this problem or a different way of modeling the "model" field?

3

There are 3 best solutions below

1
On

Is Person (or whatever other class you have) a class that is available at runtime? Could the XML/JSON be modified slightly to include a type attribute that indicates which class it corresponds to? If so the example here may help you http://blog.bdoughan.com/2012/02/xmlanyelement-and-xmladapter.html

Your Model would then have something like this

@XmlAnyElement
private List<Parameter> data;

and using the ParameterAdapter in the linked example you would need to include the type attribute that it is using in the XML and JSON

{
   "title" : "Form Title",
   "model" : {
      "person" : {
         "type" : "test.Person",
         "name" : "John",
         "surname" : "Smith",
...
}

or

<form>
   <title>Person Form</title>
   <model>
      <person type="test.Person">
         <name>John</name>
         <surname>Smith</surname>
...
</form>
4
On

Any JAXB (JSR-222) implementation (including EclipseLink MOXy) can keep free from information as a DOM structure. You could use an XmlAdapter to convert that DOM structure to/from a Map.

XmlAdapter (DomMapAdapter)

Below is a skeleton for the XmlAdapter, I haven't actually included the logic for converting between a java.util.Map and org.w3c.dom.Document.

import java.util.*;
import javax.xml.bind.annotation.adapters.XmlAdapter;
import javax.xml.parsers.DocumentBuilderFactory;
import org.w3c.dom.*;

public class DomMapAdapter extends XmlAdapter<Object, Map<String, Object>> {

    private Document document;

    public DomMapAdapter() {
        try {
            document = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument();
        } catch(Exception e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public Map<String, Object> unmarshal(Object v) throws Exception {
        Document document = (Document) v;
        Map<String, Object> map = new HashMap<String, Object>();
        // TODO - Convert Document to Map
        return map;
    }

    @Override
    public Object marshal(Map<String, Object> v) throws Exception {
        // TODO - Convert Map to Document
        return document;
    }

}

Model

Below is how you specify the XmlAdapter using the XmlJavaTypeAdapter annotation.

import java.util.Map;
import javax.xml.bind.annotation.*;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;

@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
public class Model {

    @XmlJavaTypeAdapter(DomMapAdapter.class)
    private Map<String, Object> data;

}

I have entered the following enhancement request that would make this use case easier. Please add any information that you feel would be helpful:

0
On

Would it work for you if Model keeps the data as a org.w3c.dom.Node then?

 @XmlAnyElement
 public Node data;

Then MOXy will handle this scenario but I believe there are is a bug if you read in the formatted XML and then write back out to JSON (extra newline/whitespace as a value in the output). However, with this setup I was able to read/write the XML you provided and read/write the JSON you provided.

There is one thing to note with the solution. Currently the way the JSON unmarshal reports events it tries to distingish between attributes and elements and if you read/write the JSON in your example you will notice duplicate key/value pairs. I've opened a bug for this issue (https://bugs.eclipse.org/bugs/show_bug.cgi?id=407452) but there is a workaround which is to set the UnmarshallerProperties.JSON_ATTRIBUTE_PREFIX property on the unmarshaller.

If that JSON_ATTRIBUTE_PREFIX is not set it reports each key as an element and an attribute to see if it matches either, but if you set the JSON_ATTRIBUTE_PREFIX then only things that start with that prefix will be treated as attributes so you can set the the JSON_ATTRIBUTE_PREFIX to anything that your key names don't normally start with ie:

   unmarshaller.setProperty(UnmarshallerProperties.JSON_ATTRIBUTE_PREFIX, "@");  

or to be clear

   unmarshaller.setProperty(UnmarshallerProperties.JSON_ATTRIBUTE_PREFIX, "THIS_STRING_WILL_NEVER_BE_USED_AS_THE_START_OF_A_KEY_NAME");

In the example you have you will also need to set the JSON_INCLUDE_ROOT property to false as you don't have a root in the JSON ie: no wrapping "form" key in your JSON.

   u.setProperty(UnmarshallerProperties.JSON_INCLUDE_ROOT,false);