Convert XSD Schema to JSON using JAXB Convert XSD Schema to JSON using JAXB

Page content

In this tutorial, we will learn how to auto generate class files from XSD schema using JAXB task and use them to convert to Json.

Overview

Consider a use case, where you are given an XML Schema .xsd file, based on which you should convert an XML to JSON format. There is no straightforward solution for this. We need to do following:-

  1. First we need to generate Java class files from XSD schema .xsd file
  2. Use Jackson for deserialization (XML to Java Object) and serialization (Java Object to JSON), which result into XML to JSON conversion.

We want to automate this as much as possible so that if there is any update in XML schema, it can be adapted with minimal change. We will create a JAXB task (gradle or maven) here, which is tied with project build phase and responsible for generating Java class files from given schema .xsd file.

When we converted the XML to JSON file using the generated Java class files, we find mainly two issues which were not meeting our requirement and solved them:-

  1. Date and Time in XSD xs:date and xs:time are converted to timestamp format instead of date and time format
  2. Enum value in XSD <xs:enumeration value = "half down"/> having space is converted to “HALF_UP” instead of “half up”

Follow the steps to automate generation of class files and solve above two issues:-

Steps

1. Add Gradle Task to generate classes

Add a jaxb gradle task in build.gradle where you specify following:-

  • Target destDir and package name to generate classes, for e.g. JAXB generate the classed in directory src/main/generated-sources and package com.example.jaxb
  • schema .xsd file location, for e.g. JAXB generates the java classes from schema file src/main/resources/schema/schema.xsd.
  • binding .xjb file location, for e.g. JAXB use the binding file src/main/resources/jaxb/bindings.xjb
build.gradle
configurations {
    jaxb
}

// Dependencies to be used by "jaxb" task
dependencies {
    jaxb(
        'com.sun.xml.bind:jaxb-xjc:2.3.1',
        'com.sun.xml.bind:jaxb-impl:2.3.1',
        'org.glassfish.jaxb:jaxb-runtime:2.3.1',
        'org.jvnet.jaxb2_commons:jaxb2-basics:0.12.0'
    )
}

// JAXB task definition
task jaxb {
    def generatedResouces = "src/main/generated-sources"
    def jaxbTargetDir = file(generatedResouces)
    jaxbTargetDir.deleteDir()

    doLast {
        jaxbTargetDir.mkdirs()
        ant.taskdef(name: 'xjc', classname: 'com.sun.tools.xjc.XJCTask', classpath: configurations.jaxb.asPath)
        ant.jaxbTargetDir = jaxbTargetDir
        ant.xjc(destDir: '${jaxbTargetDir}', package: 'com.example.jaxb', extension: true){
            schema(dir: "src/main/resources/schema", includes: "schema.xsd")
            binding(dir: "src/main/resources/jaxb", includes: "bindings.xjb")
            arg(line: '-XenumValue')
        }
    }
}

// Add generated classes directory to source
sourceSets.main.java.srcDirs += 'src/main/generated-sources'

// Run jaxb task before compile Java classes
compileJava.dependsOn jaxb

Generate classes from multiple XSD schema files

If you require generating classes from multiple XML schema .xsd files in different packages, you can add multiple ant.xjc like this:-

ant.xjc(destDir: '${jaxbTargetDir}', package: 'com.example.jaxb.schema1', extension: true){
    schema(dir: "src/main/resources/schema", includes: "schema1.xsd")
    binding(dir: "src/main/resources/jaxb", includes: "bindings.xjb")
    arg(line: '-XenumValue')
}
ant.xjc(destDir: '${jaxbTargetDir}', package: 'com.example.jaxb.schema2', extension: true){
    schema(dir: "src/main/resources/schema", includes: "schema2.xsd")
    binding(dir: "src/main/resources/jaxb", includes: "bindings.xjb")
    arg(line: '-XenumValue')
}

It will generate classes from schema1.xsd in package com.example.jaxb.schema1 and schema2.xsd in package com.example.jaxb.schema2


2. Fix date and time format issue

JAXB maps xs:time, xs:date, and xs:dateTime to javax.xml.datatype.XMLGregorianCalendar by default.

XMLGregorianCalendar lacks semantics of what the underlying data type really is:

  1. it lacks the information on whether this is a time, date or dateTime
  2. it lacks the information on whether the value is a local date/time versus one tied to a specific timezone offset.
  3. it is mutable

To avoid this, we want JAXB to map:-

  1. xs:time to java.time.LocalTime
  2. xs:date to java.time.LocalDate
  3. xs:dateTime to java.time.LocalDateTime

We need to do two things for this, first create adapter classes for e.g. com.example.xml.adapter.TimeAdapter and then tell JAXB to use these classes through binding bindings.xjb file.

TimeAdapter.java
package com.example.xml.adapter;

public class TimeAdapter extends XmlAdapter<String, LocalTime> {

    @Override
    public LocalTime unmarshal(String v) {
        if (Objects.nonNull(v)) {
            try {
                return LocalTime.parse(v);
            } catch (DateTimeParseException e) {
                throw new RuntimeException("Failed to parse time: " + v, e);
            }
        }
        return null;
    }

    @Override
    public String marshal(LocalTime v) {
        if (Objects.nonNull(v)) {
            return v.format(DateTimeFormatter.ISO_TIME);
        }
        return null;
    }
}
bindings.xjb
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<jaxb:bindings version="2.1"
               xmlns:jaxb="http://java.sun.com/xml/ns/jaxb"
               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">

    <jaxb:globalBindings typesafeEnumMaxMembers="2000">
        <xjc:serializable uid="-1"/>
        <xjc:javaType xmlType="xs:date"
                      name="java.time.LocalDate"
                      adapter="com.example.xml.adapter.DateAdapter"/>
        <xjc:javaType xmlType="xs:time"
                      name="java.time.LocalTime"
                      adapter="com.example.xml.adapter.TimeAdapter"/>
        <xjc:javaType xmlType="xs:dateTime"
                      name="java.time.LocalDateTime"
                      adapter="com.example.xml.adapter.DateTimeAdapter"/>
    </jaxb:globalBindings>
</jaxb:bindings>

That’s it! Now the generated classes will have time, date, and dateTime mapped to Java time package.


3. Fix Enum value issue

First Look at the problem statement, below is the example of enumeration in xsd schema:-

schema.xsd
<xs:simpleType name = "roundingDirection">
    <xs:restriction base = "xs:string">
        <xs:enumeration value = "up"/>
        <xs:enumeration value = "half up"/>
        <xs:enumeration value = "down"/>
        <xs:enumeration value = "half down"/>
        <xs:enumeration value = "nearest"/>
    </xs:restriction>
</xs:simpleType>

JAXB generate following enum class from schema.xsd file:-

RoundingDirection.java
public enum RoundingDirection {

    @XmlEnumValue("up")
    UP("up"),
    @XmlEnumValue("half up")
    HALF_UP("half up"),
    @XmlEnumValue("down")
    DOWN("down"),
    @XmlEnumValue("half down")
    HALF_DOWN("half down"),
    @XmlEnumValue("nearest")
    NEAREST("nearest");
}

Using the above generated enum class, Jackson by default convert following XML:-

AccountSummary.xml
<?xml version="1.0" encoding="UTF-8" ?>
<accountSummary>
    <interest rounding = "half up">27.55</interest>
</accountSummary>

to following json:-

AccountSummary.json
{
  "interest" : {
    "value" : 27.55,
    "rounding" : "HALF_UP"
  }
}

Jackson use the name() method of enum classes by default during conversion. If we want enum value to be used in the conversion, we need custom deserializer.

We need to do two things to solve this for all generated enum classes:-

  1. First we will use library org.jvnet.jaxb2_commons:jaxb2-basics and pass argument arg(line: '-XenumValue') in jaxb gradle task. All generated enum classes implements EnumValue class.
  2. Second we write custom deserializer for EnumValue to use enumValue() instead of default name() while deserializing to json. Register this serializer in ObjectMapper.
public class EnumValueDeserializer extends JsonSerializer<EnumValue> {

    @Override
    public void serialize(EnumValue value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
        gen.writeString(value.enumValue().toString());
    }
}
ObjectMapper objectMapper = new ObjectMapper();

SimpleModule module = new SimpleModule();
module.addSerializer(EnumValue.class, new EnumValueDeserializer());
objectMapper.registerModule(module);

objectMapper.registerModule(new JavaTimeModule());
objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);

Thats it! This will result our Json will have "rounding" : "half up" instead of "rounding" : "HALF_UP"

Conclusion

Download the complete source code for the examples in this post from github/springboot-xml