DEV Community

Cover image for How not to make a framework for default request values in Spring
Artem Ptushkin
Artem Ptushkin

Posted on • Updated on

How not to make a framework for default request values in Spring

I used to look for common solutions for the problems I face. And I fail sometimes within my researches of the best solution like many developers. But it's still important to describe your path for the future researcher.

Problem


There are a lot of written Spring REST API services with a lot of common code lines like converters, formatters, custom marshallers, etc. But it is a question still:

How to make dynamic Spring default values for composite objects input into a REST controller?

There are a lot of questions and discussions about it [1]

What do we have?

  1. Default values for request parameters and request headers

    @RequestParam(defaultValue = "19") Integer age,
    @RequestHeader(defaultValue = "John Snow") String name
    
  2. Dynamic values for request parameters and request headers

    @RequestParam(defaultValue = "${some.property.default.age}") Integer size,
    @RequestHeader(defaultValue = "${some.property.default.name}") String name
    
  3. Query parameters collection into a custom object. It doesnt require RequestParam annotation and doesnt work with headers by default

    @GetMapping("/characters")
    public Character getCharacter(Character character)
    

    DTO class:

    @Data
    public class Character {
        private String name;
        private Integer age;
    }
    

    Example call:

    curl --location --request GET 'http://localhost:8080/characters? age=19&name=John%20Snow'
    
  4. Request body collection into a custom object

    @PostMapping("/characters")
    public Character getCharacter(@RequestBody Character character)
    

In this way, we can customize default values or dynamic one by Spring properties in cases of:

Default values Dynamic default values
Query TRUE TRUE
Header TRUE TRUE
Composite query TRUE FALSE
Composite header TRUE FALSE
Body TRUE FALSE

What do we need?

The most rational way is to have a mechanism to put default values by annotation/annotation's parameters as we do with default values in annotations: @Value, @RequestParam, @RequestHeader

Requirements:

Documentation: It is possible that in future this annotation could be used for value defaulting, and especially for default values of Creator properties, since they support required() in 2.6 and above.

  • Spring must provide custom properties to Jackson module. For example:
spring.servlet:
    defaults:
      unit-request.value:
        name: Artem
Enter fullscreen mode Exit fullscreen mode

Workaround with a decorator class

There is a workaround for single cases to have an object of default values.
The hard part that we have to verify nullable values at every field and then use a field from another one.

  1. Have a common interface because we'll write a wrapper:

    public interface Unit {
        String getName();
    
        Long getId();
    }
    
  2. Request body implementation class:

    @Data
    public class UnitRequest implements Unit {
        @NotNull
        private String name;
        @Nullable
        private Long id;
    }
    
  3. Declare default properties:

    spring.servlet:
        defaults:
          unit-request.value:
            name: Artem
    
  4. Declare a class for default properties:

    @Data
    public class DefaultProperties<T> {
        @Nullable
        private T value;
    }
    
  5. Declare a configuration properties bean. Please notice, that I suggest to reuse the same class UnitRequest for properties because it is the most rational way to reuse existed class behavior. The other way - create another implementation:

    @Configuration
    public class DefaultPropertiesConfiguration {
    
        @Bean
        @ConfigurationProperties("spring.servlet.defaults.unit-request")
        public DefaultProperties<UnitRequest> unitRequestDefaultProperties() {
            return new DefaultProperties<>();
        }
    }
    
  6. Create a wrapper class to use values from another object on null cases:

    public class UnitRequestWrapper implements Unit {
    
        private final Unit decorated;
        private final Unit defaultUnit;
    
        public UnitRequestWrapper(Unit decorated, Unit defaultUnit) {
            this.decorated = decorated;
            this.defaultUnit = defaultUnit;
        }
    
        @Override
        public String getName() {
            String actual = decorated.getName();
            return actual == null ? defaultUnit.getName() : actual;
        }
    
        @Override
        public Long getId() {
            Long actual = decorated.getId();
            return actual == null ? defaultUnit.getId() : actual;
        }
    }
    
  7. Change your RestController code

    private final DefaultProperties<UnitRequest> unitRequestDefaultProperties; //custom
    
    @PostMapping("/units")
    public UnitResponse post(@RequestBody UnitRequest unitRequest) {
        Unit unit = new UnitRequestWrapper(unitRequest, unitRequestDefaultProperties.getValue()); //custom
        return UnitResponse
                .builder()
                .id(1L)
                .name(unit.getName())
                .subUnit(unit.getSubUnit())
                .build();   
    }
    

Workaround with a proxy class

This solution is very close to a reusable one and could be a part of another solution.

  1. Proxy methods of web request DTO to call the methods from another object on null values:

    public class NullableProxyFactory {
    
    public <T> T getProxy(Class<? super T> type, T first, T second) {
        return (T) Proxy.newProxyInstance(
                type.getClassLoader(),
                new Class[]{type},
                new NullableInvocationHandler<>(first, second)
        );
    }}
    
  2. Invocation Handler code:

    public class NullableInvocationHandler<T> implements InvocationHandler {
        private final T first;
        private final T second;
    
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            Method m = findMethod(second.getClass(), method);
            if (m != null) {
                Object invocationResult = m.invoke(first, args);
                if (invocationResult == null) {
                    return m.invoke(second, args);
                }
                return invocationResult;
            }
            return null;
        }
    
        private Method findMethod(Class<?> clazz, Method method) {
            try {
                return clazz.getDeclaredMethod(method.getName(), method.getParameterTypes());
            } catch (NoSuchMethodException e) {
                return null;
            }
        }
    }
    
  3. Usage:

    @PostMapping("/units")
        public UnitResponse post(@RequestBody UnitRequest unitRequest) {
            Unit unit = proxyFactory.getProxy(Unit.class, unitRequest, unitRequestDefaultProperties.getValue());
    ...
    }
    

Pros

  1. We have only one additional line inside the controller
  2. The Proxy solution looks interesting for reusable cases. We could not write boilerplate code for field values verification.
  3. We can take inside a library three classes from the proxy example.

Cons

  1. It will take many more lines of code to proxy the value of the internal composite fields. Like when UnitRequest has a field SubUnit.
  2. It requires modifying the controller code and this is redundant cause we have a lot of Spring and Jackson code from above.
  3. We have to write boilerplate code inside our wrapper.
  4. The Proxy example is somehow applicable to dynamic query parameters and dynamic headers but the Wrapper example not so much.

What I have tried also?

  • AOP. I've written an aspect to interact with all the calls for getter methods with my custom annotation. The annotation is needed to reduce method quantity. But there is a complicated moment with Spring bean injection into aspect. You have to put @Configurable on every DTO class plus AOP code seemed too risky, I've got a lot of problems on this way.
  • @ControllerAdvice + @InitBinder. It works for single cases also. You either override all the fields or have to verify (as in my workaround above) all the fields values
  • Proxy + BeanDeserializerModifier. It is possible to wrap the result of Jackson deserialization method. I have wrapped the result by proxy object but got the java.lang.IllegalArgumentException: argument type mismatch exception. The Proxy type object came to the controller it is obviously unresolvable.

Conclusions

I wanted to share the unsuccessful example of software solution research in Java backend world. It helps me to understand better the Jackson library and Proxy mechanisms.

Keep researching the best solution. It broadens the mind on the way.

Hope this article describes the question fully.

Problem links

  1. https://github.com/FasterXML/jackson-databind/issues/1420
  2. https://stackoverflow.com/questions/18805455/setting-default-values-to-null-fields-when-mapping-with-jackson
  3. https://stackoverflow.com/questions/32587551/how-to-make-requestparam-configurable-through-properties-file
  4. https://stackoverflow.com/questions/42329235/how-to-provide-default-values-for-array-parameters-in-spring-mvc-url-mapping
  5. https://stackoverflow.com/questions/15213752/spring-requestbody-and-default-values
  6. https://stackoverflow.com/questions/20469938/spring-boot-and-mvc-how-to-set-default-value-for-requestbody-object-fields-fro
  7. https://stackoverflow.com/questions/38882639/handle-null-and-default-values-in-requestbody-with-jackson
  8. https://stackoverflow.com/questions/58053179/spring-jacksonrest-modify-request-body-before-reaching-to-controller-add
  9. https://stackoverflow.com/questions/18088955/markdown-continue-numbered-list

Top comments (0)