Wenzhong
Wenzhong

Reputation: 507

Configuring snake_case query parameter using Gson in Spring Boot

I try to configure Gson as my JSON mapper to accept "snake_case" query parameter, and translate them into standard Java "camelCase" parameters.

First of all, I know I could use the @SerializedName annotation to customise the serialized name of each field, but this will involve some manual work.

After doing some search, I believe the following approach should work (please correct me if I am wrong).

  1. Use Gson as the default JSON mapper of Spring Boot

spring.http.converters.preferred-json-mapper=gson

  1. Configuring Gson before GsonHttpMessageConverter is created as described here

  2. Customising the Gson naming policy in step 2 according to GSON Field Naming Policy

private GsonHttpMessageConverter createGsonHttpMessageConverter() {
    Gson gson = new GsonBuilder()
                   .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
                   .create();

    GsonHttpMessageConverter gsonConverter = new GsonHttpMessageConverter();
    gsonConverter.setGson(gson);

    return gsonConverter;
}

Then I create a simple controller like this:

@RequestMapping(value = "/example/gson-naming-policy")
public Object testNamingPolicy(ExampleParam data) {
    return data.getCamelCase();
}

With the following Param class:

import lombok.Data;

@Data
public class ExampleParam {

    private String camelCase;

}

But when I call the controller with query parameter ?camel_case=hello, the data.camelCase could not been populated (and it's null). When I change the query parameters to ?camelCase=hello then it could be set, which mean my setting is not working as expected.

Any hint would be highly appreciated. Thanks in advance!

Upvotes: 1

Views: 4235

Answers (1)

Lyubomyr Shaydariv
Lyubomyr Shaydariv

Reputation: 21105

It's a nice question. If I understand how Spring MVC works behind the scenes, no HTTP converters are used for @ModelAttribute-driven. It can be inspected easily when throwing an exception from your ExampleParam constructor or the ExampleParam.setCamelCase method (de-Lombok first) -- Spring uses its bean utilities that use public (!) ExampleParam.setCamelCase to set the DTO value. Another proof is that no Gson.fromJson is never invoked regardless how your Gson converter is configured. So, your camelCase confuses you because the default Gson instance uses this strategy as well as Spring does -- so this is just a matter of confusion.

In order to make it work, you have to create a custom Gson-aware HandlerMethodArgumentResolver implementation. Let's assume we support POJO only (not lists, maps or primitives).

@Configuration
@EnableWebMvc
class WebMvcConfiguration
        extends WebMvcConfigurerAdapter {

    private static final Gson gson = new GsonBuilder()
            .setFieldNamingPolicy(LOWER_CASE_WITH_UNDERSCORES)
            .create();

    @Override
    public void addArgumentResolvers(final List<HandlerMethodArgumentResolver> argumentResolvers) {
        argumentResolvers.add(new HandlerMethodArgumentResolver() {
            @Override
            public boolean supportsParameter(final MethodParameter parameter) {
                // It must be never a primitive, array, string, boxed number, map or list -- and whatever you configure ;)
                final Class<?> parameterType = parameter.getParameterType();
                return !parameterType.isPrimitive()
                        && !parameterType.isArray()
                        && parameterType != String.class
                        && !Number.class.isAssignableFrom(parameterType)
                        && !Map.class.isAssignableFrom(parameterType)
                        && !List.class.isAssignableFrom(parameterType);
            }

            @Override
            public Object resolveArgument(final MethodParameter parameter, final ModelAndViewContainer mavContainer, final NativeWebRequest webRequest,
                    final WebDataBinderFactory binderFactory) {
                // Now we're deconstructing the request parameters creating a JSON tree, because Gson can convert from JSON trees to POJOs transparently
                // Also note parameter.getGenericParameterType() -- it's better that Class<?> that cannot hold generic types parameterization
                return gson.fromJson(
                        parameterMapToJsonElement(webRequest.getParameterMap()),
                        parameter.getGenericParameterType()
                );
            }
        });
    }

    ...

    private static JsonElement parameterMapToJsonElement(final Map<String, String[]> parameters) {
        final JsonObject jsonObject = new JsonObject();
        for ( final Entry<String, String[]> e : parameters.entrySet() ) {
            final String key = e.getKey();
            final String[] value = e.getValue();
            final JsonElement jsonValue;
            switch ( value.length ) {
            case 0:
                // As far as I understand, this must never happen, but I'm not sure
                jsonValue = JsonNull.INSTANCE;
                break;
            case 1:
                // If there's a single value only, let's convert it to a string literal
                // Gson is good at "weak typing": strings can be parsed automatically to numbers and booleans
                jsonValue = new JsonPrimitive(value[0]);
                break;
            default:
                // If there are more than 1 element -- make it an array
                final JsonArray jsonArray = new JsonArray();
                for ( int i = 0; i < value.length; i++ ) {
                    jsonArray.add(value[i]);
                }
                jsonValue = jsonArray;
                break;
            }
            jsonObject.add(key, jsonValue);
        }
        return jsonObject;
    }

}

So, here are the results:

  • http://localhost:8080/?camelCase=hello => (empty)
  • http://localhost:8080/?camel_case=hello => "hello"

Upvotes: 1

Related Questions