Search code examples
spring-bootspring-mvcx-www-form-urlencoded

FormUrlEncoded POST request, I need to convert snake case values into camelCase with SpringBoot and Jackson


I am integrating with a third-party's vendor API.

I have a SpringBoot and Jackson setup

They are sending me a POST request that is of type formUrlEncoded and with the params in snake_case (over 10 params in total and no body)

e.g.


POST www.example.com?player_id=somePlayerId&product_id=someProductId&total_amount=totalAmount...

There are many out of the box helpers for JSON but I cannot find any for formUrlEncoded (I hope I am missing something obvious).

I have tried @ModelAttribute and @RequestParam but had no luck.

I am trying to avoid the @RequestParam MultiValueMap<String, String> params + custom mapper option


Solution

  • @RequestParam is the simplest way which allows you to define the exact name of the query parameter something like:

    @PostMapping
    public String foo(@RequestParam("player_id") String playerId){
        
    
    }
    

    If you want to bind all the query parameters to an object , you have to use @ModelAttribute. It is based on the DataBinder and is nothing to do with Jackson. By default it only supports binding the query parameter to an object which fields have the same name as the query parameter. So you can consider to bind the query paramater to the following object :

    public class Request {
        private String player_id;
        private String product_id;
        private Long total_amount;
    }
    

    If you really want to bind to the object that follow traditional java naming convention (i.e lower camel case) from the query parameter that has snake case values , you have to cusomtize WebDataBinder.

    The idea is to override its addBindValues() and check if the query parameter name is in snake case format , convert it the lower camel case format and also add it as the bind values for the request. Something like :

    public class MyServletRequestDataBinder extends ExtendedServletRequestDataBinder {
    
        private static Converter<String, String> snakeCaseToLowerCamelConverter = CaseFormat.LOWER_UNDERSCORE
                .converterTo(CaseFormat.LOWER_CAMEL);
    
        public MyServletRequestDataBinder(Object target) {
            super(target);
        }
    
        public MyServletRequestDataBinder(Object target, String objectName) {
            super(target, objectName);
        }
    
        @Override
        protected void addBindValues(MutablePropertyValues mpvs, ServletRequest request) {
            super.addBindValues(mpvs, request);
    
            Enumeration<String> paramNames = request.getParameterNames();
            while (paramNames != null && paramNames.hasMoreElements()) {
                String paramName = paramNames.nextElement();
                
                if(paramName.contains("_")) {
                    String[] values = request.getParameterValues(paramName);
                    if (values == null || values.length == 0) {
                        // Do nothing, no values found at all.
                    } else if (values.length > 1) {
                        mpvs.addPropertyValue(snakeCaseToLowerCamelConverter.convert(paramName), values);
                    } else {
                        mpvs.addPropertyValue(snakeCaseToLowerCamelConverter.convert(paramName), values[0]);
                    }
                }
            }
        }
    }
    
    

    P.S I am using Guava for helping me to convert snake case to lowerCamelCase.

    But in order to use the customized WebDataBinder , you have to in turn customize WebDataBinderFactory and RequestMappingHandlerAdapter because :

    • customize WebDataBinderFactory in order to create the customised WebDataBinder
    • customize RequestMappingHandlerAdapter in order to create the WebDataBinderFactory

    Something like:

    public class MyServletRequestDataBinderFactory extends ServletRequestDataBinderFactory {
    
            public MyServletRequestDataBinderFactory(List<InvocableHandlerMethod> binderMethods,
                    WebBindingInitializer initializer) {
                super(binderMethods, initializer);
            }
    
            @Override
            protected ServletRequestDataBinder createBinderInstance(Object target, String objectName,
                    NativeWebRequest request) throws Exception {
                return new MyServletRequestDataBinder(target, objectName);
            }
    
        }
    

    and

    public class MyRequestMappingHandlerAdapter extends RequestMappingHandlerAdapter {
    
        @Override
        protected InitBinderDataBinderFactory createDataBinderFactory(List<InvocableHandlerMethod> binderMethods)
                throws Exception {
            return new MyServletRequestDataBinderFactory(binderMethods, getWebBindingInitializer());
        }
    
    }
        
    

    And finally register to use the customised RequestMappingHandlerAdapter in your configuration :

    @Configuration
    public class Config extends DelegatingWebMvcConfiguration {
    
        @Override
        protected RequestMappingHandlerAdapter createRequestMappingHandlerAdapter() {
            return new MyRequestMappingHandlerAdapter();
        }
    }