Hylianux Hylianux - 2 months ago 35
Java Question

JAXB marshalling dateTime to blank value when time not specified

I've got an XMLGregorianCalendar object that I'm trying to marshal into an xml string. I received this object by unmarshalling another xml object. Both are of type "dateTime", so they should be exactly the same...

And yet, when I marshal it, it shows up blank in the xml.

To illustrate this issue, I stripped everything down to the bare bones and made it generic in this example here. 2 java files, copy, paste, run as-is. The output you should receive would be:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<TheObject>
<DOB>2016-09-16</DOB>
</TheObject>


But, alas, it returns:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<TheObject>
<DOB></DOB>
</TheObject>


Note: in the pastebin example, I create a xmlGregorianCalendar on the fly rather than grab one from another object like the code below, so technically it's not the same thing, but I think ultimately it illustrates the exact same issue... correct me if I'm wrong...

To add more context to my specific issue:

//Here are the objects themselves (names changed to protect the innocent)
//complete with annotations...
public class Object1{
...
@XmlElement(name = "DOB")
@XmlSchemaType(name = "dateTime")
protected XMLGregorianCalendar dob;
...
}

public class Object2{
...
@XmlElement(name = "DOB")
@XmlSchemaType(name = "dateTime")
protected XMLGregorianCalendar dob;
...
}

//and here's the snippet where the date object(date of birth) gets set
//from one object to another.
object2.setDOB(object1.getDOB());

//and finally, marshalling it to an xml string
private String marshallTheObject(Object2 theObject) throws JAXBException{
JAXBContext jaxbContext = JAXBContext.newInstance(Object2.class);
Marshaller jaxbMarshaller = jaxbContext.createMarshaller();
jaxbMarshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
StringWriter sw = new StringWriter();
jaxbMarshaller.marshal(object2, sw);
String output = sw.toString();
return output;
}

//the xml output shows: <DOB></DOB> instead of the date


I'm using the jaxb version that java 8 comes bundled with...

So my question is: is this some kind of bug? And if not, what am I doing wrong? How can I get around this issue without having to modify the generated java code? I cannot edit the xsd file used to generate it either...

Edit: for reference, the xsd file lists the DOB as:

<xs:element name="DOB" type="xs:dateTime" />

Answer

While I wasn't allowed to modify the xsd file directly, what I didn't realize was that I could change how the classes themselves were generated through a bindings file. I wasn't using maven in this case, so it was a bit difficult to determine the solution. I was generating the classes by using a feature built into eclipse that generates jaxb classes from an xsd file.

I learned more about bindings files here

I'm not exactly sure where that file needs to be placed in respect to maven, but in doing it the eclipse way it doesn't matter - you get to specify where the bindings file is during the jaxb class generation wizard.

Once generated, you'll need to code your own xml adapter.

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

import javax.xml.bind.annotation.adapters.XmlAdapter;

public class CalendarDateTimeAdapter extends XmlAdapter<String, Date> {

    //Sadly my specific situation requires me to strip the time off of all dateTime objects
    //It's bad, but I didn't get to design the system, so this is the best compromise...
    private final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");

    @Override public Date unmarshal(String value) throws ParseException {
        synchronized (sdf){
            return sdf.parse(value);
        }
    }

    @Override public String marshal(Date value) {
        if(value == null) { return null; }
        synchronized(sdf){
            return sdf.format(value);
        }
    }
}

Make sure your classes match what you specified in your bindings file...

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE jaxb:bindings>

<jaxb:bindings version="2.1"
    xmlns:jaxb="http://java.sun.com/xml/ns/jaxb"
    xmlns:xs="http://www.w3.org/2001/XMLSchema"
    xmlns:xjc="http://java.sun.com/xml/ns/jaxb/xjc">

    <!-- Prevent wrapping data types into JAXBElements. -->
    <jaxb:globalBindings generateElementProperty="false">

        <!-- Use java.util.Date instead of XMLGregorianCalendar. -->
        <xjc:javaType name="java.util.Date" xmlType="xs:dateTime"
            adapter="com.package.location.adapter.CalendarDateTimeAdapter"/>
        <xjc:javaType name="java.util.Date" xmlType="xs:date"
            adapter="com.package.location.adapter.CalendarDateAdapter"/>
        <xjc:javaType name="java.util.Date" xmlType="xs:time"
            adapter="com.package.location.adapter.CalendarTimeAdapter"/>

    </jaxb:globalBindings>
</jaxb:bindings>

Then, when doing your marshalling, use the setAdapter function to use it.

private String marshallObject(MyObject myObject) throws JAXBException{
    JAXBContext jaxbContext = JAXBContext.newInstance(MyObject.class);
    Marshaller jaxbMarshaller = jaxbContext.createMarshaller();
    jaxbMarshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
    jaxbMarshaller.setAdapter(new CalendarDateTimeAdapter());
    StringWriter sw = new StringWriter();
    jaxbMarshaller.marshal(myObject, sw);
    String output = sw.toString();
    return output;
}

Now they'll resolve to Date objects instead of XMLGregorianCalendar objects. And apparently, Date objects work better than XMLGregorianCalendars...

I'm still perturbed that the unmarshalled xml that went in doesn't marshal to become the same xml going out, but at any rate, this is precisely what I did to make it all start working. I'm sure I've done something against convention here, and if I have, please let me know.

Again I remind readers that I'm not using maven, nor any kind of framework (e.g. SpringMVC).

Comments