Convert XSD Schema to JSON using JAXB
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:-
- First we need to generate Java class files from XSD schema
.xsd
file - 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:-
- Date and Time in XSD
xs:date
andxs:time
are converted to timestamp format instead of date and time format - 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 packagecom.example.jaxb
- schema
.xsd
file location, for e.g. JAXB generates the java classes from schema filesrc/main/resources/schema/schema.xsd
. - binding
.xjb
file location, for e.g. JAXB use the binding filesrc/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:
- it lacks the information on whether this is a time, date or dateTime
- it lacks the information on whether the value is a local date/time versus one tied to a specific timezone offset.
- it is mutable
To avoid this, we want JAXB to map:-
xs:time
tojava.time.LocalTime
xs:date
tojava.time.LocalDate
xs:dateTime
tojava.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:-
- First we will use library
org.jvnet.jaxb2_commons:jaxb2-basics
and pass argumentarg(line: '-XenumValue')
injaxb
gradle task. All generated enum classes implementsEnumValue
class. - Second we write custom deserializer for
EnumValue
to useenumValue()
instead of defaultname()
while deserializing to json. Register this serializer inObjectMapper
.
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