Binding snake_case request parameters to a Spring form
Asked Answered
H

2

6

I'm implementing a simple RESTful service using Spring Boot, with the interface defined by a .NET (I think) client. Their parameter names are snake_case, rather than camelCase, which obviously means I need to customise how they are mapped.

In the case of JSON input/output, that's fine, I've just customised the ObjectMapper, like so:

@Bean
public ObjectMapper objectMapper() {
  ObjectMapper objectMapper = new ObjectMapper();
  objectMapper.setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE);
  return objectMapper;
}

That works fine. Now my problem is form data. I have a Spring form like:

public class MyForm {
  private String myValue;

  public String getMyValue() {return myValue;}
  public void setMyValue(String myValue) {this.myValue = myValue;}
}

But the requests I need to accept will look like:

POST /foo/bar HTTP/1.1
Host: localhost:8080
Content-Type: application/x-www-form-urlencoded

my_value=5

I feel like there must be some simple hook into Spring's binding, like the equivalent setting in Jackon's ObjectMapper, but I'm struggling to find one. The only somewhat-relevant post I can find on here is this one, about completely changing the parameter names, which has some suggestions that seem like overkill for my use case.

The simple solution is simply to use snake case for the fields in MyForm, which works fine, but is a bit ugly.

A final suggestion I've seen elsewhere is to use an interceptor to modify the request parameters on the way in, which seems like it would be straightforward but it feels like there are bound to be exceptions that make it non-trivial, and I'm concerned that having code hidden away in an interceptor makes it really hard to find when you hit the one obscure case where it doesn't work.

Is there some 'proper' Spring-y way of handling this that I'm missing, or do I just need to pick one of the above not-quite-perfect solutions?

Harrod answered 22/1, 2016 at 11:3 Comment(4)
Hi, I'm struggling the same issue as you described. Any chance you have a working solution to share?Con
Sorry, no. I ended up just making the form have field names like my_value, and then getters/setters like getMy_value() and setMy_value(). Not great but it works.Harrod
Yeah, I know it works but not really Java conventions..bummer, thanks!Con
@Harrod I hate to say it, but I did the same thing. I have responses working like a charm, but no dice on requests without the above "hack" as well.Phosphocreatine
C
10

probably you already have solved this issue, I was fighting with this today and answered a question on StackOverflow PT.

So here is the deal:

Create a filter to be executed before the request reach the controller, and format the parameters accordingly (from snake case to camel case on my scenario).

Talk is cheap, show me the code!

import java.io.IOException;
import java.util.Collections;
import java.util.Enumeration;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.filter.OncePerRequestFilter;

import com.google.common.base.CaseFormat;

@Configuration
public class AppConfig {

    @Bean
    public Filter snakeConverter() {
        return new OncePerRequestFilter() {

            @Override
            protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
                    throws ServletException, IOException {
                final Map<String, String[]> formattedParams = new ConcurrentHashMap<>();

                for (String param : request.getParameterMap().keySet()) {
                    String formattedParam = CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL, param);
                    formattedParams.put(formattedParam, request.getParameterValues(param));
                }

                filterChain.doFilter(new HttpServletRequestWrapper(request) {
                    @Override
                    public String getParameter(String name) {
                        return formattedParams.containsKey(name) ? formattedParams.get(name)[0] : null;
                    }

                    @Override
                    public Enumeration<String> getParameterNames() {
                        return Collections.enumeration(formattedParams.keySet());
                    }

                    @Override
                    public String[] getParameterValues(String name) {
                        return formattedParams.get(name);
                    }

                    @Override
                    public Map<String, String[]> getParameterMap() {
                        return formattedParams;
                    }
                }, response);
            }
        };
    }

}

The snakeConverter do the magic.

In there, the doFilterInternal is executed always before the request reach the controller, the parameters are stored in a new Map in their formatted form, and are forwarded to the controller through the filterChain.doFilter.

The HttpServletRequestWrapper do the job of provide our new parameters to the controller.

This code is completely based on the azhawkes filter.


Testing it using a simple controller in the the following URL: http://localhost:8080/snakecase?foo_bar=123

enter image description here

Camden answered 25/3, 2019 at 2:32 Comment(0)
T
0

Just add this to your application.properties in a spring boot project

spring.jackson.property-naming-strategy=SNAKE_CASE

If your looking for a guide, I got this answer from this Article on BeyondCode

Terena answered 7/4, 2023 at 11:36 Comment(1)
Thanks, but this is just a different way of representing the same thing I mentioned at the start of the question. If you read on, the actual question is about how to do the same thing for form data.Harrod

© 2022 - 2024 — McMap. All rights reserved.