Table of Contents
Overview
Recently, for the first time, I encounter a need to use an interface in @RequestBody in my Spring Boot app.
Here is the diagram:
This is the request that contains one IAudioContent
public record CreateQuestionGroupRequest( String content, IAudioContent audioContent ) { }
However, when I sent a request to create a question group with TTSSingleRequest
in JSON format for example, I got the following error:
org.springframework.http.converter.HttpMessageConversionException: Type definition error: [simple type, class io.ukata.api.helpers.http.request.IAudioContent] .... Caused by: com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `io.ukata.api.helpers.http.request.IAudioContent` (no Creators, like default constructor, exist): abstract types either need to be mapped to concrete types, have custom deserializer, or contain additional type information
This is because Jackson could not create an instance of an interface. In this case, that’s IAudioContent
.
I’m going to show you how to fix this using a custom deserializer.
Using a Custom Deserializer
The solution to the problem above is to use a custom deserializer.
First, create a new deserializer:
public class AudioContentDeserializer extends JsonDeserializer<IAudioContent> { @Override public IAudioContent deserialize(JsonParser parser, DeserializationContext context) throws IOException { ObjectMapper mapper = (ObjectMapper) parser.getCodec(); ObjectNode root = mapper.readTree(parser); if (root.has("type")) { String type = root.get("type").asText(); if (type.equals(AudioContentType.NARRATION.getType())) { return mapper.readValue(root.toString(), TTSSingleRequest.class); } else if (type.equals(AudioContentType.CONVERSATION.getType())) { return mapper.readValue(root.toString(), TTSConversationRequest.class); } else { throw new RuntimeException("Unknown audio content type"); } } else { throw new RuntimeException("Unknown audio content type"); } } }
As you can see, here I categorized the implementation by a key called “type”. Your case could be different.
After creating this deserializer, you add this annotation at the class level of the interface:
@JsonDeserialize(using = AudioContentDeserializer.class) public interface IAudioContent { //... }
Now, this custom deserializer will be used to deserialize JSON to implementation of IAudioContent.
We are not done yet. At the class level of all implementations, you also need to put @JsonDeserialize
to tell Jackson not to use the custom deserializer again. If you forget to put this in the implementations, you may encounter an infinite loop.
@JsonDeserialize(using = JsonDeserializer.None.class) public class TOEICStatementAudio implements IAudioContent { //... }
That’s it! You can now use interfaces, abstract classes in @RequestBody in Spring Boot.
Conclusion
In this post, I’ve shown you how you can use interfaces and abstract classes in Spring Boot.
I build softwares that solve problems. I also love writing/documenting things I learn/want to learn.