Backend Development 8 min read

Why Property Copy Tools in Java Can Be Dangerous and How MapStruct Provides a Safer Alternative

The article explains the performance drawbacks, hidden type‑conversion bugs, and runtime errors caused by generic Java property‑copy utilities such as BeanUtils and CGLIB, and demonstrates how compile‑time‑checked tools like MapStruct can safely handle conversions.

Java Architect Essentials
Java Architect Essentials
Java Architect Essentials
Why Property Copy Tools in Java Can Be Dangerous and How MapStruct Provides a Safer Alternative

In a previous column the author warned against using generic property‑copy utilities and suggested defining explicit conversion classes, optionally generated by IDE plugins.

The main reasons for avoiding these tools are poor performance, occasional bugs, and hidden pitfalls that only surface at runtime.

For example, a real case showed that org.apache.commons.beanutils.BeanUtils performed poorly compared with org.springframework.beans.BeanUtils , though the article does not include a benchmark.

Using Spring's BeanUtils.copyProperties to copy an object A (with List<Integer> ids ) to an object B (with List<String> ids ) compiles but throws a ClassCastException at runtime because the generic types are erased:

import lombok.Data;
import java.util.List;

@Data
public class A {
    private String name;
    private List
ids;
}

@Data
public class B {
    private String name;
    private List
ids;
}

import org.springframework.beans.BeanUtils;
import java.util.Arrays;

public class BeanUtilDemo {
    public static void main(String[] args) {
        A first = new A();
        first.setName("demo");
        first.setIds(Arrays.asList(1, 2, 3));
        B second = new B();
        BeanUtils.copyProperties(first, second);
        for (String each : second.getIds()) {
            System.out.println(each); // ClassCastException
        }
    }
}

The same problem appears when using CGLIB's BeanCopier without a custom converter.

import org.easymock.cglib.beans.BeanCopier;
import java.util.Arrays;

public class BeanUtilDemo {
    public static void main(String[] args) {
        A first = new A();
        first.setName("demo");
        first.setIds(Arrays.asList(1, 2, 3));
        B second = new B();
        BeanCopier beanCopier = BeanCopier.create(A.class, B.class, false);
        beanCopier.copy(first, second, null);
        for (String each : second.getIds()) {
            System.out.println(each); // ClassCastException
        }
    }
}

MapStruct solves the issue by generating a mapper that converts the list types correctly at compile time:

import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;

@Mapper
public interface Converter {
    Converter INSTANCE = Mappers.getMapper(Converter.class);
    B aToB(A car);
}

public class BeanUtilDemo {
    public static void main(String[] args) {
        A first = new A();
        first.setName("demo");
        first.setIds(Arrays.asList(1, 2, 3));
        B second = Converter.INSTANCE.aToB(first);
        for (String each : second.getIds()) {
            System.out.println(each); // works fine
        }
    }
}

The generated implementation shows how MapStruct creates a helper method to convert List to List :

@Generated(...)
@Component
public class ConverterImpl implements Converter {
    @Override
    public B aToB(A car) {
        if (car == null) {
            return null;
        }
        B b = new B();
        b.setName(car.getName());
        b.setIds(integerListToStringList(car.getIds()));
        return b;
    }
    protected List
integerListToStringList(List
list) {
        if (list == null) {
            return null;
        }
        List
list1 = new ArrayList<>(list.size());
        for (Integer integer : list) {
            list1.add(String.valueOf(integer));
        }
        return list1;
    }
}

If a field type mismatch is introduced (e.g., String number in A and Long number in B ), MapStruct will generate code that attempts Long.parseLong , which throws NumberFormatException for non‑numeric strings, while CGLIB simply leaves the target field null.

Because Java generics are erased at runtime, both List and List appear as plain List , making such errors hard to detect without compile‑time checks. The author recommends writing explicit conversion classes or using IDE‑generated ones, which catch type mismatches early and avoid the hidden costs of generic copy utilities.

BackendJavaBeanUtilsMapStructType Conversionproperty-copy
Java Architect Essentials
Written by

Java Architect Essentials

Committed to sharing quality articles and tutorials to help Java programmers progress from junior to mid-level to senior architect. We curate high-quality learning resources, interview questions, videos, and projects from across the internet to help you systematically improve your Java architecture skills. Follow and reply '1024' to get Java programming resources. Learn together, grow together.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.