Why Spring BeanUtils Fails to Copy Static Inner Classes and How to Fix It
This article explains why Spring's BeanUtils.copyProperties cannot copy static inner class fields and list properties, demonstrates the pitfalls with missing getters/setters, and provides a step‑by‑step solution to correctly copy such objects in Java applications.
Background: In a recent project we needed to send an HTTP request to a third‑party SDK. The SDK’s Request class had a bug, so we rewrote our own Request class with similar fields, including a static inner class and two List properties.
private List<Order> orders;
private AddRequest.Ticket ticket;
private List<Payment> payments;We assembled the request parameters in our AddRequest class and used Spring’s BeanUtils.copyProperties to copy them into the SDK’s MixAddRequest before sending. The request should have succeeded, but the third‑party reported a missing required field inside the Ticket inner class.
Investigation revealed that BeanUtils.copyProperties did not copy the static inner class field, leaving it null. The first pitfall was that the fields were declared public without Lombok’s @Data, so no getters/setters existed and everything copied as null.
After adding @Data, simple fields (String) were copied, but the static inner class remained null, confirming the inner‑class issue.
@ToString
@Data
public class CopyTest1 {
public String outerName;
public CopyTest1.InnerClass innerClass;
public List<CopyTest1.InnerClass> clazz;
@ToString
@Data
public static class InnerClass {
public String InnerName;
}
}
@ToString
@Data
public class CopyTest2 {
public String outerName;
public CopyTest2.InnerClass innerClass;
public List<CopyTest2.InnerClass> clazz;
@ToString
@Data
public static class InnerClass {
public String InnerName;
}
}
CopyTest1 test1 = new CopyTest1();
test1.outerName = "hahaha";
CopyTest1.InnerClass innerClass = new CopyTest1.InnerClass();
innerClass.InnerName = "hohoho";
test1.innerClass = innerClass;
System.out.println(test1.toString());
CopyTest2 test2 = new CopyTest2();
BeanUtils.copyProperties(test1, test2);
System.out.println(test2.toString());The second pitfall was that even with getters/setters, BeanUtils does not copy properties whose types are different static inner classes, because they are considered distinct types.
Solution: copy the inner class separately and ensure it also has setters.
CopyTest1 test1 = new CopyTest1();
test1.outerName = "hahaha";
CopyTest1.InnerClass innerClass = new CopyTest1.InnerClass();
innerClass.InnerName = "hohoho";
test1.innerClass = innerClass;
System.out.println(test1.toString());
CopyTest2 test2 = new CopyTest2();
test2.innerClass = new CopyTest2.InnerClass();
BeanUtils.copyProperties(test1, test2);
BeanUtils.copyProperties(test1.innerClass, test2.innerClass);
System.out.println(test2.toString());Note that list properties also work because Java generics are erased at runtime; the List contains Objects, so the elements are copied without type conflicts.
Summary
Spring BeanUtils.copyProperties requires matching getter and setter methods for each property.
Static inner classes with identical field names but different enclosing classes are treated as different types and are not copied automatically.
Java generics only affect compile‑time; at runtime a List is just a collection of Objects.
The order of source and target parameters in BeanUtils (and Apache Commons) copy methods is crucial; swapping them leads to unexpected results.
Final Note
Spring’s source shows that copyProperties uses the target’s setter (write method); without a setter the copy fails.
private static void copyProperties(Object source, Object target, @Nullable Class<?> editable, @Nullable String... ignoreProperties) throws BeansException {
Assert.notNull(source, "Source must not be null");
Assert.notNull(target, "Target must not be null");
Class<?> actualEditable = target.getClass();
if (editable != null) {
if (!editable.isInstance(target)) {
throw new IllegalArgumentException("Target class [" + target.getClass().getName() + "] not assignable to Editable class [" + editable.getName() + "]");
}
actualEditable = editable;
}
PropertyDescriptor[] targetPds = getPropertyDescriptors(actualEditable);
List<String> ignoreList = ignoreProperties != null ? Arrays.asList(ignoreProperties) : null;
for (PropertyDescriptor targetPd : targetPds) {
Method writeMethod = targetPd.getWriteMethod();
if (writeMethod != null && (ignoreList == null || !ignoreList.contains(targetPd.getName()))) {
PropertyDescriptor sourcePd = getPropertyDescriptor(source.getClass(), targetPd.getName());
if (sourcePd != null) {
Method readMethod = sourcePd.getReadMethod();
if (readMethod != null && ClassUtils.isAssignable(writeMethod.getParameterTypes()[0], readMethod.getReturnType())) {
try {
if (!Modifier.isPublic(readMethod.getDeclaringClass().getModifiers())) {
readMethod.setAccessible(true);
}
Object value = readMethod.invoke(source);
if (!Modifier.isPublic(writeMethod.getDeclaringClass().getModifiers())) {
writeMethod.setAccessible(true);
}
writeMethod.invoke(target, value);
} catch (Throwable ex) {
throw new FatalBeanException("Could not copy property '" + targetPd.getName() + "' from source to target", ex);
}
}
}
}
}
}Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Programmer DD
A tinkering programmer and author of "Spring Cloud Microservices in Action"
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
