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<Integer> ids;
}

@Data
public class B {
    private String name;
    private List<String> 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<Integer> to List<String>:

@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<String> integerListToStringList(List<Integer> list) {
        if (list == null) {
            return null;
        }
        List<String> 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<Integer> and List<String> 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.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

BackendperformanceBeanUtilsmapstructtype 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

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.