Using Interface In Spring Boot @RequestBody

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.

Leave a Comment