Custom deserialization in Spring
Summary
This post try to resolve a problem where given a REST service with a already defined API, we want to add new API that uses the same service and without implement new logic.
The problem
Let’s suppose we have a REST service to register an object Person in our system and this receives a request with this JSON structure:
1
2
3
4
{
"fullName": "...."
}
Now imagine we have an old client which is invoking other service to create Person but with different API request:
1
2
3
4
{
"full_name": "...."
}
In the case we want the old client uses our service, we have to be compatible. For this case we have two different options:
- Create a new entry in our controller creating a new model compatible with the old client.
- Keep the same request object and modify the deserialization process in order to be compatible with the old client. This post will treat about this case.
NOTE: For this example, we suppose that the old client is sending a header to difference with the current request model.
Creating a custom deserializer
We need a custom deserializar for transform the old format in our current request model. Our custom deserializer will get the full_name
field and will return the resquest with this value setted.
1
2
3
4
5
6
7
8
9
10
public class PersonRequestCustomDeserializer {
public PersonRequest deserialize(JsonParser jsonParser) throws IOException {
JsonNode jsonNode = jsonParser.getCodec().readTree(jsonParser);
String fullName = jsonNode.get("full_name").textValue();
return PersonRequest.builder().fullName(fullName).build();
}
}
Defining a delegate
The delegate will be the responsible for select the correct deserializer depending on the header that we have received.
If we receive the header “custom-api” with some value then the delegate will use the custom deserializer. Otherwise it will use default deserializer.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class PersonDelegatingDeserializer extends DelegatingDeserializer {
private final PersonRequestCustomDeserializer personRequestCustomDeserializer = new PersonRequestCustomDeserializer();
public PersonDelegatingDeserializer(JsonDeserializer defaultJsonDeserializer) {
super(defaultJsonDeserializer);
}
@Override
public Object deserialize(JsonParser jp, DeserializationContext dc) throws IOException {
if (MDC.get(CUSTOM_API) == null) {
return super.deserialize(jp, dc);
} else {
return personRequestCustomDeserializer.deserialize(jp);
}
}
@Override
protected JsonDeserializer<?> newDelegatingInstance(JsonDeserializer<?> jsonDeserializer) {
return jsonDeserializer;
}
}
NOTE: We have used MDC to store the header. This header is setted using a
HandlerInterceptorAdapter
that is invoked before the deserializer. SeeRequestsHandlerInterceptorAdapterConfig
in the code for more details.
Registering custom deserializer in Spring
The last step is to register in Spring our custom delegate. To do this we have to add a SimpleModule in MappingJackson2HttpMessageConverter
which is the class responsible for the conversions in controllers.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Configuration
@EnableWebMvc
public class MvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new RequestsHandlerInterceptorAdapterConfig());
}
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
for (HttpMessageConverter<?> converter : converters) {
if (converter instanceof MappingJackson2HttpMessageConverter) {
MappingJackson2HttpMessageConverter jacksonMessageConverter =
(MappingJackson2HttpMessageConverter) converter;
ObjectMapper objectMapper = jacksonMessageConverter.getObjectMapper();
SimpleModule simpleModule = new SimpleModule();
simpleModule.setDeserializerModifier(new PersonRequestBeanDeserializerModifier());
objectMapper.registerModule(simpleModule);
break;
}
}
}
}
The SimpleModule receives a deserializar modifier that is a class that connect with our deserializer delegate.
1
2
3
4
5
6
7
8
9
10
11
12
public class PersonRequestBeanDeserializerModifier extends BeanDeserializerModifier {
@Override
public JsonDeserializer<?> modifyDeserializer(DeserializationConfig dc, BeanDescription bd,
JsonDeserializer<?> deserializer) {
if (PersonRequest.class.equals(bd.getBeanClass())) {
return new PersonDelegatingDeserializer(deserializer);
}
return super.modifyDeserializer(dc, beanDesc, deserializer);
}
}
Testing the project
Standard request:
1
2
3
4
curl -X POST \
-d '{"fullName":"test"}' \
-H "Content-Type: application/json" \
localhost:8080/person
Old request:
1
2
3
4
5
curl -X POST \
-d '{"full_name":"test"}' \
-H "Content-Type: application/json" \
-H "custom-api: true" \
localhost:8080/person
References