Convert XSD Schema to JSON using JAXB

0
10

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 the 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 results in 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 the following:-

  • Target destDir and package name to generate classes, for eg JAXB generate the classed in directory src/main/generated-sources and package com.example.jaxb
  • schema .xsd file location, for eg JAXB generates the java classes from schema file src/main/resources/schema/schema.xsd.
  • binding .xjb file location, for eg 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:dateand 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 eg 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 the 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 converts the 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 used the name() method of enum classes by default during conversion. If we want enum value to be used in the conversion, we need a 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 implement 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);

That’s 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

Source

LEAVE A REPLY

Please enter your comment!
Please enter your name here