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! :-)

0

There are 0 best solutions below