Refactor java methods that do not have a common hierarchy but have same API
Are you able to change your code to rely on a wrapper class then?  - myself


Going on a limb here, but do you want to consider using a wrapper class to encapsulate the fact that you have three similar generated classes, and provide a common way of accessing/modifying them? This assumes:


The methods on the generated classes have the same parameter and return types.
For the case of getDetail() method, that returns the same type as the input object, i.e. recursion.


If both conditions satisfy, then a sample wrapper implementation can be as such (using poorly named class names and variables, please excuse that):

public class WrapperClass {

    private final C1 o1;
    private final C2 o2;

    private WrapperClass(C1 o1, C2 o2) {
        this.o1 = o1;
        this.o2 = o2;
    }

    public String getCode() {
        return findAny(mapC1(C1::getCode), mapC2(C2::getCode));
    }

    // other getter methods

    private &lt;T&gt; T mapC1(Function&lt;C1, T&gt; mapper) {
        return Optional.ofNullable(o1).map(mapper).orElse(null);
    }

    private &lt;T&gt; T mapC2(Function&lt;C2, T&gt; mapper) {
        return Optional.ofNullable(o2).map(mapper).orElse(null);
    }

    private static &lt;T&gt; T findAny(Object... items) {
        return (T) Arrays.stream(items).filter(Objects::nonNull).findAny().orElse(null);
    }

    public static WrapperClass withC1(C1 o1) {
        return new WrapperClass(o1, null);
    }

    public static WrapperClass withC2(C2 o2) {
        return new WrapperClass(null, o2);
    }
}


You will need a map...(Function&lt;?, T&gt;) method for each oh your message class that performs the conversion, and the findAny(Object...) method to get the desired non-null value, if applicable. Each getter method, e.g. getCode() as shown, is relatively trivial to implement. In your case, since you are only dealing with three different classes (which is already a lot), you can also consider a simpler if-ladder instead of streaming through the values.

A slight benefit of the wrapper class is that it lets you perform additional conversions too, for example to an ErrorDetails object:

public final WrapperClass {

    // ...

    public ErrorDetails toErrorDetails() {
        ErrorDetails result = new ErrorDetails(getCode(), getDescription(),
                                                getSourceExceptionName());
        Optional.ofNullable(getSeverity())
                .map(Severity::value)
                .ifPresent(result::setSeverity);
        Optional.ofNullable(getType())
                .map(Type::value)
                .ifPresent(result::setType);
        return result;
    }

    // ...

}


Your current implementations of (convertToList()) will then just become:

public void convertToList(C1 o1, List&lt;ErrorDetails&gt; errors) {
    convertToList(WrapperClass.withC1(o1), errors);
}

public void convertToList(C2 o2, List&lt;ErrorDetails&gt; errors) {
    convertToList(WrapperClass.withC2(o2), errors);
}

private void convertToList(WrapperClass wrapper, List&lt;ErrorDetails&gt; errors) {
    errors.add(wrapper.toErrorDetails());
    convertToList(wrapper.getDetail(), errors);
}


Implementing the wrapper class's getDetail() will be quite similar, other than the slight difference that it should also return its own type:

public WrapperClass getDetail() {
    return new WrapperClass(mapC1(C1::getDetail), mapC2(C2::getDetail));
}