I have problems mapping data from JAXB generated classes (using jaxb2-maven-plugin
, based on a XSD) to a simple DTO, using Mapstruct.
In general, everything works fine, but I have problems in specific situations :
- my XML element is BOTH nillable (
nillable="true"
) and optionnal (minOccurs="0"
) in the XSD definition - this xml element is not "terminal" in the source path defined for Mapstruct's mapping
I can't of course act on the XSD as it is a third-party input we can't modify.
I made a simple use case to demonstrate the situation.
Files to reproduce
pom.xml
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>jaxb2-maven-plugin</artifactId>
<version>2.5.0</version>
<executions>
<execution>
<id>xjc</id>
<goals>
<goal>xjc</goal>
</goals>
</execution>
</executions>
<configuration>
<sources>
<source>src/main/resources/xsd/test.xsd</source>
</sources>
<outputDirectory>${project.build.directory}/generated-sources</outputDirectory>
<packageName>test.generated</packageName>
<clearOutputDir>false</clearOutputDir>
</configuration>
</plugin>
<plugin>
<groupId>org.bsc.maven</groupId>
<artifactId>maven-processor-plugin</artifactId>
<version>3.3.1</version>
<configuration>
<defaultOutputDirectory>
${project.build.directory}/generated-sources
</defaultOutputDirectory>
<processors>
<processor>org.mapstruct.ap.MappingProcessor</processor>
</processors>
</configuration>
<executions>
<execution>
<id>process</id>
<phase>generate-sources</phase>
<goals>
<goal>process</goal>
</goals>
</execution>
</executions>
<dependencies>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${mapstruct.version}</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
test.xsd
<?xml version='1.0' encoding='UTF-8'?>
<?xml-stylesheet type="text/xsl" href="dico_v3.xsl"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:vc="http://www.w3.org/2007/XMLSchema-versioning" elementFormDefault="qualified" attributeFormDefault="unqualified" vc:minVersion="1.1">
<xs:element name="test">
<xs:annotation>
<xs:documentation>Version V3 - 2021-04-23</xs:documentation>
</xs:annotation>
<xs:complexType>
<xs:all>
<xs:element name="string_element" type="xs:string">
<xs:annotation>
<xs:appinfo source="test/string_element"/>
<xs:documentation>simple string element</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="string_element_nillopt" type="xs:string" minOccurs="0" nillable="true">
<xs:annotation>
<xs:appinfo source="test/string_element_nillopt"/>
<xs:documentation>nillable AND optionnal string element </xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="obj_element">
<xs:complexType>
<xs:all>
<xs:element name="obj_element_string_element" type="xs:string">
<xs:annotation>
<xs:appinfo source="test/obj_element/obj_element_string_element"/>
<xs:documentation>simple string element INSIDE object element</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="obj_element_string_element_nillopt" type="xs:string" minOccurs="0" nillable="true">
<xs:annotation>
<xs:appinfo source="test/obj_element/obj_element_string_element_nillopt"/>
<xs:documentation>nillable AND optionnal string element INSIDE object element</xs:documentation>
</xs:annotation>
</xs:element>
</xs:all>
</xs:complexType>
</xs:element>
<xs:element name="obj_element_nillopt" minOccurs="0" nillable="true">
<xs:complexType>
<xs:all>
<xs:element name="obj_element_nillopt_string_element" type="xs:string">
<xs:annotation>
<xs:appinfo source="test/obj_element_nillopt/obj_element_nillopt_string_element"/>
<xs:documentation>simple string element INSIDE nillable AND optionnal object element</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="obj_element_nillopt_string_element_nillopt" type="xs:string" minOccurs="0" nillable="true">
<xs:annotation>
<xs:appinfo source="test/obj_element_nillopt/obj_element_nillopt_string_element_nillopt"/>
<xs:documentation>nillable AND optionnal string element INSIDE nillable AND optionnal object element</xs:documentation>
</xs:annotation>
</xs:element>
</xs:all>
</xs:complexType>
</xs:element>
</xs:all>
</xs:complexType>
</xs:element>
</xs:schema>
Test.java (generated by JAXB maven plugin from the XSD)
@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "", propOrder = {
})
@XmlRootElement(name = "test")
public class Test {
@XmlElement(name = "string_element", required = true)
protected String stringElement;
@XmlElementRef(name = "string_element_nillopt", type = JAXBElement.class, required = false)
protected JAXBElement<String> stringElementNillopt;
@XmlElement(name = "obj_element", required = true)
protected Test.ObjElement objElement;
@XmlElementRef(name = "obj_element_nillopt", type = JAXBElement.class, required = false)
protected JAXBElement<Test.ObjElementNillopt> objElementNillopt;
@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "", propOrder = {
})
public static class ObjElement {
@XmlElement(name = "obj_element_string_element", required = true)
protected String objElementStringElement;
@XmlElementRef(name = "obj_element_string_element_nillopt", type = JAXBElement.class, required = false)
protected JAXBElement<String> objElementStringElementNillopt;
}
@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "", propOrder = {
})
public static class ObjElementNillopt {
@XmlElement(name = "obj_element_nillopt_string_element", required = true)
protected String objElementNilloptStringElement;
@XmlElementRef(name = "obj_element_nillopt_string_element_nillopt", type = JAXBElement.class, required = false)
protected JAXBElement<String> objElementNilloptStringElementNillopt;
}
}
TestMapper.java
@Mapper(unmappedTargetPolicy = ReportingPolicy.IGNORE)
public interface TestMapper {
// Mapping fromXml
@Mapping(source = "stringElement", target = "stringAttribute1")
@Mapping(source = "stringElementNillopt", target = "stringAttribute2")
@Mapping(source = "objElement.objElementStringElement", target = "stringAttribute3")
@Mapping(source = "objElement.objElementStringElementNillopt", target = "stringAttribute4")
@Mapping(source = "objElementNillopt.objElementNilloptStringElement", target = "stringAttribute5")
@Mapping(source = "objElementNillopt.objElementNilloptStringElementNillopt", target = "stringAttribute6")
TestDTO fromXml(Test xmlInput);
}
TestDTO
public class TestDTO {
String stringAttribute1;
String stringAttribute2;
String stringAttribute3;
String stringAttribute4;
String stringAttribute5;
String stringAttribute6;
// ... getters & setters
}
First Observations
We can see that each time an element is defined with both minOccurs="0"
and nillable="true"
in the XSD, JAXB plugin generates code that returns JAXBElement<T>
instead of T
(in our example JAXBElement<String>
instead of String
).
I expected Mapstruct to do the mapping transparently, as it tells in the doc §5.1 : https://mapstruct.org/documentation/stable/reference/html/#implicit-type-conversions
MapStruct takes care of type conversions automatically in many cases.
...
Currently the following conversions are applied automatically:
...
Between JAXBElement and T, List<JAXBElement> and List
Indeed it works, but ONLY if JAXB elements are "terminal" in the mapping source path...
TestMapper.java
Using a mapper like that will work (I removed 2 mast mappings to stringAttribute5
and stringAttribute6
):
@Mapper(unmappedTargetPolicy = ReportingPolicy.IGNORE)
public interface TestMapper {
// Mapping fromXml
@Mapping(source = "stringElement", target = "stringAttribute1")
@Mapping(source = "stringElementNillopt", target = "stringAttribute2")
@Mapping(source = "objElement.objElementStringElement", target = "stringAttribute3")
@Mapping(source = "objElement.objElementStringElementNillopt", target = "stringAttribute4")
TestDTO fromXml(Test xmlInput);
}
TestMapperImpl.java
Mapstruct generates a TestMapperImpl.java
that takes care of the source JAXBElement<T>
conversion to T
in the targetted DTO by introcuding a jaxbElemToValue
method :
@Generated(
value = "org.mapstruct.ap.MappingProcessor",
date = "2021-05-10T18:34:10+0200",
comments = "version: 1.4.2.Final, compiler: javac, environment: Java 11.0.2 (Oracle Corporation)"
)
public class TestMapperImpl implements TestMapper {
@Override
public TestDTO fromXml(Test xmlInput) {
if ( xmlInput== null ) {
return null;
}
TestDTO testDTO = new TestDTO();
testDTO.setStringAttribute1( xmlInput.getStringElement() );
testDTO.setStringAttribute2( jaxbElemToValue( xmlInput.getStringElementNillopt() ) );
testDTO.setStringAttribute3( dpeObjElementObjElementStringElement( xmlInput) );
testDTO.setStringAttribute4( jaxbElemToValue( dpeObjElementObjElementStringElementNillopt( xmlInput) ) );
return testDTO;
}
private <T> T jaxbElemToValue( JAXBElement<T> element ) {
if ( element == null ) {
return null;
}
return element.isNil() ? null : element.getValue();
}
private String dpeObjElementObjElementStringElement(Test test) {
if ( test == null ) {
return null;
}
ObjElement objElement = test.getObjElement();
if ( objElement == null ) {
return null;
}
String objElementStringElement = objElement.getObjElementStringElement();
if ( objElementStringElement == null ) {
return null;
}
return objElementStringElement;
}
private JAXBElement<String> dpeObjElementObjElementStringElementNillopt(Test test) {
if ( test == null ) {
return null;
}
ObjElement objElement = test.getObjElement();
if ( objElement == null ) {
return null;
}
JAXBElement<String> objElementStringElementNillopt = objElement.getObjElementStringElementNillopt();
if ( objElementStringElementNillopt == null ) {
return null;
}
return objElementStringElementNillopt;
}
}
Nice.
The problem
TestMapper.java
Now I put back the 2 last mappings with nillable/optionnal element in the middle of the source mapping path :
@Mapper(unmappedTargetPolicy = ReportingPolicy.IGNORE)
public interface TestMapper {
// Mapping fromXml
@Mapping(source = "stringElement", target = "stringAttribute1")
@Mapping(source = "stringElementNillopt", target = "stringAttribute2")
@Mapping(source = "objElement.objElementStringElement", target = "stringAttribute3")
@Mapping(source = "objElement.objElementStringElementNillopt", target = "stringAttribute4")
@Mapping(source = "objElementNillopt.objElementNilloptStringElement", target = "stringAttribute5")
@Mapping(source = "objElementNillopt.objElementNilloptStringElementNillopt", target = "stringAttribute6")
TestDTO fromXml(Test xmlInput);
}
The Mapstruct maven goal will fail like that :
No property named "objElementNillopt.objElementNilloptStringElement" exists in source parameter(s).
No property named "objElementNillopt.objElementNilloptStringElementNillopt" exists in source parameter(s).
Complete error log:
[INFO] --- maven-processor-plugin:3.3.1:process (process) @ DPE-API ---
[ERROR] diagnostic: C:\xxx\mapper\TestMapper.java:21: error: No property named "objElementNillopt.objElementNilloptStringElement" exists in source parameter(s). Did you mean "objElementNillopt.typeSubstituted"?
TestDTO fromXml(Test xmlInput);
^
[ERROR] diagnostic: C:\xxx\mapper\TestMapper.java:21: error: No property named "objElementNillopt.objElementNilloptStringElementNillopt" exists in source parameter(s). Did you mean "objElementNillopt.declaredType"?
TestDTO fromXml(Test xmlInput);
^
[ERROR] error on execute: use -X to have details
[INFO] ------------------------------------------------------------------------
[INFO] BUILD FAILURE
[INFO] ------------------------------------------------------------------------
The (workaround?) solution
The solution is to explicitly tells Mapstruct to take the .value
attribute of the intermediate objElementNillopt
JAXBElement...
TestMapper.java
@Mapper(unmappedTargetPolicy = ReportingPolicy.IGNORE)
public interface TestMapper {
// Mapping fromXml
@Mapping(source = "stringElement", target = "stringAttribute1")
@Mapping(source = "stringElementNillopt", target = "stringAttribute2")
@Mapping(source = "objElement.objElementStringElement", target = "stringAttribute3")
@Mapping(source = "objElement.objElementStringElementNillopt", target = "stringAttribute4")
@Mapping(source = "objElementNillopt.value.objElementNilloptStringElement", target = "stringAttribute5")
@Mapping(source = "objElementNillopt.value.objElementNilloptStringElementNillopt", target = "stringAttribute6")
TestDTO fromXml(Test xmlInput);
}
TestMapperImpl.java
This time it works without errors and it generates this new mapper implementation:
@Generated(
value = "org.mapstruct.ap.MappingProcessor",
date = "2021-05-10T18:50:43+0200",
comments = "version: 1.4.2.Final, compiler: javac, environment: Java 11.0.2 (Oracle Corporation)"
)
public class TestMapperImpl implements TestMapper {
@Override
public TestDTO fromXml(Test xmlInput) {
if ( xmlInput == null ) {
return null;
}
TestDTO testDTO = new TestDTO();
testDTO.setStringAttribute1( xmlInput.getStringElement() );
testDTO.setStringAttribute2( jaxbElemToValue( xmlInput.getStringElementNillopt() ) );
testDTO.setStringAttribute3( xmlInputObjElementObjElementStringElement( xmlInput ) );
testDTO.setStringAttribute4( jaxbElemToValue( xmlInputObjElementObjElementStringElementNillopt( xmlInput ) ) );
testDTO.setStringAttribute5( xmlInputObjElementNilloptValueObjElementNilloptStringElement( xmlInput ) );
testDTO.setStringAttribute6( jaxbElemToValue( xmlInputObjElementNilloptValueObjElementNilloptStringElementNillopt( xmlInput ) ) );
return testDTO;
}
private <T> T jaxbElemToValue( JAXBElement<T> element ) {
if ( element == null ) {
return null;
}
return element.isNil() ? null : element.getValue();
}
private String xmlInputObjElementObjElementStringElement(Test test) {
if ( test == null ) {
return null;
}
ObjElement objElement = test.getObjElement();
if ( objElement == null ) {
return null;
}
String objElementStringElement = objElement.getObjElementStringElement();
if ( objElementStringElement == null ) {
return null;
}
return objElementStringElement;
}
private JAXBElement<String> xmlInputObjElementObjElementStringElementNillopt(Test test) {
if ( test == null ) {
return null;
}
ObjElement objElement = test.getObjElement();
if ( objElement == null ) {
return null;
}
JAXBElement<String> objElementStringElementNillopt = objElement.getObjElementStringElementNillopt();
if ( objElementStringElementNillopt == null ) {
return null;
}
return objElementStringElementNillopt;
}
private String xmlInputObjElementNilloptValueObjElementNilloptStringElement(Test test) {
if ( test == null ) {
return null;
}
JAXBElement<ObjElementNillopt> objElementNillopt = test.getObjElementNillopt();
if ( objElementNillopt == null ) {
return null;
}
ObjElementNillopt value = objElementNillopt.getValue();
if ( value == null ) {
return null;
}
String objElementNilloptStringElement = value.getObjElementNilloptStringElement();
if ( objElementNilloptStringElement == null ) {
return null;
}
return objElementNilloptStringElement;
}
private JAXBElement<String> xmlInputObjElementNilloptValueObjElementNilloptStringElementNillopt(Test test) {
if ( test == null ) {
return null;
}
JAXBElement<ObjElementNillopt> objElementNillopt = test.getObjElementNillopt();
if ( objElementNillopt == null ) {
return null;
}
ObjElementNillopt value = objElementNillopt.getValue();
if ( value == null ) {
return null;
}
JAXBElement<String> objElementNilloptStringElementNillopt = value.getObjElementNilloptStringElementNillopt();
if ( objElementNilloptStringElementNillopt == null ) {
return null;
}
return objElementNilloptStringElementNillopt;
}
}
The question !
So why asking for help if I managed to figure it out?
My input XSD is HUGE and often changed with new elements that became nillable/optionnal (or not) from time to time...
It is crazy to track for each change and find the right place to add the .value
item in the Mapstruct's @Mapping
source path.
I was expecting that Mapstruct will handle JAXBElements for items in the source path, whenever they are intermediate or terminal.
So perhaps I missed something in my code or JAXB / Mapstruct maven plugins configuration ?
If you have any feedback that can help, it will be appreciated!
Thanks in advance for your help! :-)