← Back to Writing

Building a Custom Data Binder: From HTTP Requests to Java POJOs

How I built a mechanism to automatically parse HTTP request parameters and JSON payloads into complex Java objects using Reflection.

Introduction

If you have written standard Java Servlets, you know the pain of extracting data from an HTTP request. For a simple user registration form, you typically have to do this:

String firstName = request.getParameter("firstName");
String lastName = request.getParameter("lastName");
int age = Integer.parseInt(request.getParameter("age")); // Hope it doesn't throw an exception!

User user = new User();
user.setFirstName(firstName);
user.setLastName(lastName);
user.setAge(age);

This is tedious, repetitive, and clutters your business logic with HTTP transport concerns. Modern frameworks like Spring MVC solve this using Data Binding—you just declare a User object as a method parameter, and the framework magically populates it.

While building WEBRock, my custom Servlet framework, I wanted this same magic. Here is how I implemented a custom Data Binder using Java Reflection and a custom @POJO annotation.

Step 1: The @POJO Annotation

First, we need a way to tell the framework that a specific method parameter should be instantiated and populated from the HTTP request, rather than being treated as a simple primitive.

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface POJO {
}

Now, the developer can write a clean, HTTP-agnostic method:

@PATH(value = "/registerUser", method = "POST")
public Response register(@POJO User user) {
    database.save(user);
    return new Response(200, "User saved!");
}

Step 2: Intercepting the Request

When the HTTP request hits the WEBRock Front Controller, the framework looks up the target Method in its routing table.

Before invoking the method, it inspects the method’s parameters:

Parameter[] parameters = targetMethod.getParameters();
Object[] methodArguments = new Object[parameters.length];

for (int i = 0; i < parameters.length; i++) {
    Parameter param = parameters[i];

    if (param.isAnnotationPresent(POJO.class)) {
        // This is a complex object we need to build!
        Class<?> pojoType = param.getType();
        methodArguments[i] = buildPojoFromRequest(pojoType, request);
    } else {
        // Handle standard primitive parameters (int, String, etc.)
        methodArguments[i] = extractPrimitive(param, request);
    }
}

Step 3: The Binding Logic

The buildPojoFromRequest method is where the heavy lifting happens. It must instantiate the object and map the incoming HTTP parameters to the object’s fields.

private Object buildPojoFromRequest(Class<?> pojoType, HttpServletRequest request) {
    try {
        // 1. Create an empty instance of the object (requires a no-arg constructor)
        Object instance = pojoType.newInstance();

        // 2. Iterate over all fields in the class
        for (Field field : pojoType.getDeclaredFields()) {
            String fieldName = field.getName();

            // 3. Look for a matching parameter in the HTTP request
            String requestValue = request.getParameter(fieldName);

            if (requestValue != null) {
                // 4. Convert the string to the correct type and inject it
                field.setAccessible(true);
                Object convertedValue = convertType(field.getType(), requestValue);
                field.set(instance, convertedValue);
            }
        }

        return instance;

    } catch (Exception e) {
        throw new RuntimeException("Failed to bind POJO", e);
    }
}

Step 4: Type Conversion

HTTP parameters always arrive as String arrays. If the User class has an int age field, we cannot inject the string "25" into it; we must parse it first.

The convertType method acts as a massive switch statement that handles Java’s core types:

private Object convertType(Class<?> targetType, String value) {
    if (targetType == int.class || targetType == Integer.class) {
        return Integer.parseInt(value);
    } else if (targetType == boolean.class || targetType == Boolean.class) {
        return Boolean.parseBoolean(value);
    } else if (targetType == double.class || targetType == Double.class) {
        return Double.parseDouble(value);
    }
    // Default fallback is String
    return value;
}

Conclusion

By the time targetMethod.invoke(controller, methodArguments) is called, the User object is fully constructed and populated with the exact data from the browser.

Building this custom Data Binder taught me how to bridge the gap between text-based network protocols (HTTP) and strongly-typed object-oriented languages (Java). It drastically reduced the boilerplate code required to build APIs, proving that a little bit of Reflection goes a very long way in improving developer experience.