Skip to content

JAXB type adapter classes for java.time

addicticks-dev edited this page Mar 15, 2023 · 9 revisions

The library includes classes intended for use with JAXB.

Working with date and time values in JAXB can be cumbersome because you are (normally) forced to work with XMLGregorianCalendar. However, there is a simple solution for that: make JAXB use Java’s OffsetDateTime and OffsetTime instead. This library provides a set of XmlAdapters to achieve this goal:

XmlAdapter class

Translates to/from

XML schema data type

Java class

OffsetDateTimeXmlAdapter

xs:dateTime

OffsetDateTime

OffsetTimeXmlAdapter

xs:time

OffsetTime

OffsetDateXmlAdapter

xs:date

OffsetDateTime (with time fields set to midnight)

OffsetDateClassXmlAdapter

xs:date

OffsetDate (a custom class included in the library)

Usage in classes

It is outside the scope of this guide to explain how JAXB XmlAdapters are applied in general. But here’s a little taste:

public class Customer {

    @XmlElement
    @XmlJavaTypeAdapter(OffsetDateTimeXmlAdapter.class)
    @XmlSchemaType(name="dateTime")
    public OffsetDateTime getLastOrderTime() {
        ....
    }
    
    @XmlElement
    @XmlJavaTypeAdapter(OffsetDateXmlAdapter.class)
    @XmlSchemaType(name="date")
    public OffsetDateTime getDateOfBirth() {   // returns a date-only value
        ....
    }
}

The trick is the XmlJavaTypeAdapter annotation which can be applied to fields, getters/setters, packages, etc. In fact the most convenient usage of this library is probably to apply the annotations at the package level. Thereby you do not have to annotate each element/attribute individually. Simply put something like the following in your package-info.java file:

@XmlJavaTypeAdapters
({
    @XmlJavaTypeAdapter(value=OffsetDateTimeXmlAdapter.class,  type=OffsetDateTime.class),
    @XmlJavaTypeAdapter(value=OffsetTimeXmlAdapter.class,      type=OffsetTime.class),
    @XmlJavaTypeAdapter(value=OffsetDateClassXmlAdapter.class, type=OffsetDate.class),
})
@XmlSchemaTypes
({
    @XmlSchemaType(name="dateTime", type=OffsetDateTime.class),
    @XmlSchemaType(name="time", type=OffsetTime.class),
    @XmlSchemaType(name="date", type=OffsetDate.class)
})
package org.example.mydtopackage;

import com.addicticks.texttime.jaxb.OffsetDate;
import com.addicticks.texttime.jaxb.OffsetDateTimeXmlAdapter;
import com.addicticks.texttime.jaxb.OffsetDateClassXmlAdapter;
import com.addicticks.texttime.jaxb.OffsetTimeXmlAdapter;
import java.time.OffsetDateTime;
import java.time.OffsetTime;
import javax.xml.bind.annotation.XmlSchemaType;  /* or jakarta namespace */
import javax.xml.bind.annotation.XmlSchemaTypes; /* or jakarta namespace */
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter; /* or jakarta namespace */
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapters; /* or jakarta namespace */

The adapter will now apply to all classes in the package. Furthermore, because of the @XmlSchemaTypes annotation, if you generate an XML Schema from your classes, it will use correct schema types. (otherwise you’ll see xs:string as the schema type for such elements/attributes, which is not what you want)

Usage with XJC

If you generate Java classes from XML schema using the xjc tool then you must use a so-called bindings file to instruct the xjc tool which adapter you want to use.

Using the out-of-the-box adapters from this library then the bindings file should look like this:

<?xml version="1.0" encoding="UTF-8"?>
    <!-- This file is automatically picked up by the jaxb2-maven-plugin
         if it lives in src/main/xjb                                -->
<jxb:bindings   
        xmlns:jxb="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"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://java.sun.com/xml/ns/jaxb http://java.sun.com/xml/ns/jaxb/bindingschema_2_0.xsd"
        version="2.1">

    <jxb:globalBindings>
        <!-- Avoid having to work with XMLGregorianCalendar. 
             Instead, map as follows:
             
                 XML dateTime   :   OffsetDateTime  
                 XML date       :   OffsetDateTime  (time value truncated)
                 XML time       :   OffsetTime                             -->
             
        <xjc:javaType adapter="com.addicticks.texttime.jaxb.OffsetDateTimeXmlAdapter"
                      name="java.time.OffsetDateTime" xmlType="xs:dateTime"/>
        <xjc:javaType adapter="com.addicticks.texttime.jaxb.OffsetDateXmlAdapter"
                      name="java.time.OffsetDateTime" xmlType="xs:date"/>
        <xjc:javaType adapter="com.addicticks.texttime.jaxb.OffsetTimeXmlAdapter"
                      name="java.time.OffsetTime" xmlType="xs:time"/>
        
    </jxb:globalBindings>

</jxb:bindings>

If you are using the JAXB2 Maven Plugin then name the file jaxb-datetime-bindings.xjb and place it in src/main/xjb. Thereby it will automatically be picked up by the Maven plugin.

Dealing with a missing timezone offset

In the next section you are presented with the differences between the XML Schema Date Time format and the java.time objects and why conversion between the two worlds require careful consideration.

The major problem to tackle is that the authors of the XML Schema made the mistake of making the timezone offset optional. For this reason, the XmlAdapter must be able to parse a string value such as

2018-05-22T23:44:51

Out-of-the box, the adapters provide a best guess of what the timezone might be when not explicitly specified. The default implementation is to use the system’s default ZoneId and then find the offset for that Zone at the local time from the text. Say for example that the system’s default ZoneId is 'Europe/Paris'. The resulting timezone offset for the above text value would then be '+02:00' because Paris was using DST in May 2018.

If, for whatever reason, you are unhappy with the default implementation, then you’ll need to subclass the XmlAdapter and override the getZoneOffsetFor…​() method. For example you may want the default to always be UTC:

public class OffsetDateTimeXmlAdapterUTCDefault extends OffsetDateTimeXmlAdapter {

    @Override
    public ZoneOffset getZoneOffsetForDateTime(LocalDateTime localDateTime) {
        return ZoneOffset.UTC;
    }
}

…​and then use the custom class name in your JAXB annotations or in the XJC bindings file.

Differences between XML date/time types and Java’s Offset datetime classes

The OffsetTime and OffsetDateTime classes introduced in Java 8 are the closest equivalents from the java.time package to the XML Schema date and time values. However, there are a few subtle differences:

  • The XML date/time data types allows for the timezone offset to be left out. Therefore, when unmarshalling, we may have to supply our own value for the offset. This value should depend entirely on the scenario. By default the library will supply the current offset from the JVM’s default ZoneId, but this may not be what you want. In this case then extend the adapters and override the getZoneOffsetFor…​() method.

  • Java doesn’t have a date-only data type. Two different adapters are provided for converting to/from xs:date:

    • OffsetDateClassXmlAdapter uses a custom OffsetDate class which is merely a thin wrapper around the OffsetDateTime class from the JDK. This is likely to be the most convenient alternative if you want to apply the @XmlJavaTypeAdapter annotation at the package level.

    • OffsetDateXmlAdapter uses the JDK’s own OffsetDateTimeClass with the time set to midnight. This is likely to be the most convenient alternative if you are using XJC to generate classes from XML schema.

  • The XML xs:time and xs:dateTime data types allow for unlimited number of digits in the fractional part of the seconds element. For example the following is a perfectly valid xs:time value: "23:30:28.123456789012345678901234567890". In order to overcome this discrepancy while unmarshalling (parsing text into Java type), the OffsetDateTimeXmlAdapter and OffsetTimeXmlAdapter will silently truncate such excessive digits. While marshalling they never output more then 9 fractional digits.

  • The XML xs:time and xs:dateTime data types allow for the seconds field to use a range from 0 to 60, while the JSR-310 Java types of OffsetDateTime and OffsetTime only allow the range 0 to 59. The difference is leap seconds. For example "2016-12-31T23:59:60Z" is actually a point in time because a leap second was added that year. The jTexttime library will fail in parsing such a value. However, in real life, it is very unlikely that a system will produce a value like that as most systems tend to spread out the leap second over the last part of the 31st of December or to use the value "23:59:59" twice.

  • The XML xs:time and xs:dateTime data types allow for a special midnight time value: 24:00:00. The following two values are the same according to the XSD specification: 2022-02-28T24:00:00Z and 2022-03-01T00:00:00Z. This library fully supports parsing this special midnight value during unmarshalling, however it will never marshal into this value but instead use the 00:00:00 notation. It is the opinion of the author that support for the 24:00:00 is an unnecessary complication in the XSD specification and that there is no purpose in making the situation worse for other consumers by ever producing such a value.