4

I have a controller whose response is camelCase json value. Now we are re-writing the code with new version and the response required is in snake_case.

I have added a message converter and modified object mapper to setsetPropertyNamingStrategy(PropertyNamingStrategy.CAMEL_CASE_TO_LOWER_CASE_WITH_UNDERSCORES);

public class ResponseJSONConverter extends MappingJackson2HttpMessageConverter {

@Autowired
public ResponseJSONConverter(ObjectMapper objectMapper) {
    setObjectMapper(objectMapper);
  }
}

I have registered this convertor with spring and its working as expected. Now I want my old endpoints to return in camelCase for backward compatibility for my consumers and new endpoints with snake_case.

I have tried to have one more message convertor with simple object mapper without setting camelCase to Snake case property and registered with spring. Only one message convertor gets applied based on the order declared in the spring configuration.

Is there any way we can achieve this ? Loading message convertor based on the condition ?

EDIT

Added my spring config file

 <beans xmlns:context="http://www.springframework.org/schema/context"
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.springframework.org/schema/beans"
       xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd
        http://www.springframework.org/schema/mvc
        http://www.springframework.org/schema/mvc/spring-mvc.xsd">

<bean id="moneySerializer" class="api.serialize.MoneySerializer"/>
    <bean id="moneyDeserializer" class="api.serialize.MoneyDeserializer"/>
    <bean id="serializationModule" class="api.serialize.SerializationModule">
        <constructor-arg index="0" ref="moneySerializer"/>
        <constructor-arg index="1" ref="moneyDeserializer"/>
    </bean>

    <bean id="customObjectMapper" class="api.serialize.CustomObjectMapper" primary="true">
        <constructor-arg ref="serializationModule"/>
    </bean>
    <mvc:annotation-driven>
        <mvc:message-converters register-defaults="true">
            <bean class="api.serialize.ResponseJSONConverterCamelCaseToSnakeCase" >
                <constructor-arg ref="customObjectMapper"/>
            </bean>
            <bean class="api.serialize.ResponseJSONConverter">
                <constructor-arg ref="objectMapper"/>
            </bean>
        </mvc:message-converters>

    </mvc:annotation-driven>

    <bean id="objectMapper" class="com.fasterxml.jackson.databind.ObjectMapper"/>

</beans>

EDIT 2.0

my servlet.xml

<mvc:annotation-driven>
    <mvc:message-converters register-defaults="true">
        <bean class="com.tgt.promotions.api.serialize.ServiceJSONConverter"/>
    </mvc:message-converters>
</mvc:annotation-driven>

CustomMessageConverter

    public class ServiceJSONConverter extends MappingJackson2HttpMessageConverter {

    @Autowired
    public ServiceJSONConverter(SnakeCaseObjectMapper snakeCaseObjectMapper) {
        setObjectMapper(snakeCaseObjectMapper);
    }
}

Custom Object Mapper

@Component
public class SnakeCaseObjectMapper extends ObjectMapper {
    @Autowired
    public SnakeCaseObjectMapper(PropertyNamingStrategy propertyNamingStrategy) {
        setSerializationInclusion(JsonInclude.Include.NON_NULL);
        setPropertyNamingStrategy(propertyNamingStrategy);
    }
}

Custom Property Naming Strategy

@Component
public class CustomPropertyNamingStrategy extends PropertyNamingStrategy {

    @Autowired
    private HttpServletRequest request;

    private final PropertyNamingStrategy legacyStrategy  = PropertyNamingStrategy.LOWER_CASE;
    private final PropertyNamingStrategy defaultStrategy  = PropertyNamingStrategy.CAMEL_CASE_TO_LOWER_CASE_WITH_UNDERSCORES;


    @Override
    public String nameForConstructorParameter(MapperConfig<?> config, AnnotatedParameter ctorParam, String defaultName) {
        return getStrategy().nameForConstructorParameter(config, ctorParam, defaultName);
    }

    @Override
    public String nameForField(MapperConfig<?> config, AnnotatedField field, String defaultName) {
        return getStrategy().nameForField(config, field, defaultName);
    }

    @Override
    public String nameForGetterMethod(MapperConfig<?> config, AnnotatedMethod method, String defaultName) {
        return getStrategy().nameForGetterMethod(config, method, defaultName);
    }

    @Override
    public String nameForSetterMethod(MapperConfig<?> config, AnnotatedMethod method, String defaultName) {
        return getStrategy().nameForSetterMethod(config, method, defaultName);
    }

    private PropertyNamingStrategy getStrategy() {
        if (isLegacyEndpoint(request)) {
            return legacyStrategy;
        } else {
            return defaultStrategy;
        }
    }

    private boolean isLegacyEndpoint(HttpServletRequest request) {
        return request != null && request.getRequestURL() != null && !request.getRequestURL().toString().contains("/v3");
    }
}
Pramod
  • 787
  • 4
  • 19

2 Answers2

0

Instead of having 2 different object-mappers, I suggest creating a custom implementation of PropertyNamingStrategy, using the 2 other strategies accordingly:

public class AwesomePropertyNamingStrategy extends PropertyNamingStrategy {

  private PropertyNamingStrategy legacyStrategy  = PropertyNamingStrategy.LOWER_CASE;
  private PropertyNamingStrategy defaultStrategy  = PropertyNamingStrategy.CAMEL_CASE_TO_LOWER_CASE_WITH_UNDERSCORES;

  @Override
  public String nameForConstructorParameter(MapperConfig<?> config, AnnotatedParameter ctorParam, String defaultName) {
    return getStrategy().nameForConstructorParameter(config, ctorParam, defaultName);
  }

  // TODO: implement other nameForXXX methods

  private PropertyNamingStrategy getStrategy() {
    if (isLegacyEndpoint()) {
      return legacyStrategy;
    } else {
      return defaultStrategy;
    }
  }

  private boolean isLegacyEndpoint() {
    // TODO: get hold of the RequestContext or some other thead-local context 
    // that allows you to know it's an old or a new endpoint
    return false;
  }
}

You should come up with a way to toggle between legacy and new mode:

  1. Using the endpoint URL by accessing the request context in some way
  2. In case your old endpoint use different response objects, use the class of the object that is being converted to determine legacy/normal or your own custom @LegacyResponse annotation on all old classes instead.
  • Thanks for the reply. I tried to do whatever you suggested. I have created a message converter and passing customObjectMapper and setting AwesomePropertyNamingStrategy as you suggested. But response i get is mixture of both small case and snake_case strategy. Do I need to do anything to solve this ? – Pramod Feb 15 '17 at 17:50
  • Can you share the solution you came up with, specifically the part where you differentiate between legacy and new? – Frederik Heremans Feb 15 '17 at 17:51
  • I am autowiring HttpServletRequest request in the class and loading AwesomePropertyNamingStrategy as component through spring. – Pramod Feb 15 '17 at 17:53
  • That should be fine in order to access the current request context. But what mechanism are you using to differentiate between the old and the new resources/classes? – Frederik Heremans Feb 16 '17 at 07:46
  • Judging from your the code in your edit 2.0, it should just work. Have you tried debugging in the CustomPropertyNamingStrategy to see why the the `request.getRequestURL().toString()` differ when converting a single object to JSON in a single request? You can try using `RequestContextHolder` approach to get hold of the request instead of using the autowire approach. – Frederik Heremans Feb 16 '17 at 11:24
0

Well, nothing worked after many attempts. Finally ended up defining 2 different servlets. one without any version and one with v1 version.

web.xml

        <servlet>
            <servlet-name>snake-case</servlet-name>
            <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
            <load-on-startup>1</load-on-startup>
        </servlet>

        <servlet-mapping>
            <servlet-name>snake-case</servlet-name>
            <url-pattern>/v1</url-pattern>
        </servlet-mapping>

         <servlet>
            <servlet-name>camel-case</servlet-name>
            <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
            <load-on-startup>1</load-on-startup>
        </servlet>

        <servlet-mapping>
            <servlet-name>camel-case</servlet-name>
            <url-pattern>/</url-pattern>
        </servlet-mapping>

Accordingly defined two servlets snake-case-servlet.xml and camel-case-servlet.xml.

snake-case-servlet.xml

    <mvc:annotation-driven>
        <mvc:message-converters register-defaults="true">
            <bean class="com.tgt.promotions.api.serialize.DataJSONConverter">
            <constructor-arg ref="snakeCaseObjectMapper"/>
            </bean>
        </mvc:message-converters>
    </mvc:annotation-driven> 

camel-case-servlet.xml

    <mvc:annotation-driven>
        <mvc:message-converters register-defaults="true">
            <bean class="com.tgt.promotions.api.serialize.DataJSONConverter">
            <constructor-arg ref="objectMapper"/>
            </bean>
        </mvc:message-converters>
    </mvc:annotation-driven> 

Now, for any requests with /v1* , snakeCaseObjectMapper is used and for other requests default object mapper is used.

Pramod
  • 787
  • 4
  • 19