How to Return Custom Types in HTTP Responses using Spring Web

November 20, 2023
Written by
Twilion

Spring Boot is the most popular framework for Java applications, and with the Web module enabled is a great way to quickly start building a web app. Both Boot and Web have a lot of preconfigured behaviours to make common tasks easy, and extension points for when you need something custom. In this post we'll look at one such extension point, so that our HTTP endpoint methods need deal only in the types that are meaningful for us, while still converting to HTTP responses correctly.

With Spring Web if you return Java objects from methods annotated with @GetMapping or @PostMapping (or the catch-all @RequestMapping), Spring can turn that object into a JSON response automatically.

For a lot of cases this is enough - JSON over HTTP is the lingua franca of web APIs. It's not the only game in town though - you might find cases where you want to create a response from a custom Java type and have control over how the response body and headers are generated. This can be the case when working with Twilio as responses to Twilio webhook requests need to contain valid TwiML with an application/xml content type.  Let's look at a short method and see 3 different ways to create the same response.

Hand-written Responses

It's quite possible to return a String from your HTTP-mapped method (which needs to be in a @RestController-annotated class):

@RequestMapping(value = "/twiml/hand-written", produces = "application/xml")
public String handwritten() {
   return """
           <Response>
               <Say>Hello hello and welcome</Say>
           </Response>
           """;
}

With multi-line strings (added in Java 15) this doesn't even look too bad, but if you need to build your TwiML dynamically it can get tricky to make sure you're creating valid TwiML, and is laborious to maintain.

Object Conversion to String

Our Java Helper Library provides a few classes to build response objects programmatically and generate valid TwiML at the end. Sticking with the same example as before:

@RequestMapping(value = "/twiml/to-xml", produces = "application/xml")
public String toXml() {
   return new VoiceResponse.Builder().say(
                   new Say.Builder("Hello hello and welcome").build())
           .build()
           .toXml();
}

Here we have created a VoiceResponse and called .toXml() to convert it to a String. This is helpful but a couple of problems remain: we have to remember the produces = "application/xml" line on each method, and we have to call .toXml() on the VoiceResponse object which is another thing to remember every time. A more subtle problem is that this method is hard to unit test. By forcing the VoiceResponse into a String our tests will need to rely on fragile string-matching, or use an XML parser to turn the String back into a VoiceResponse. This is untidy, and Spring Web is hugely customizable to enable us to avoid this kind of mess.

Can we have Automatic Response Generation?

Ideally we would write our method as minimally as possible, like this:

@RequestMapping(value = "/twiml/converted")
public VoiceResponse converted() {
   return new VoiceResponse.Builder().say(
                   new Say.Builder("Hello hello and welcome").build())
           .build();
}

If you add this code Spring will assume you want the VoiceResponse turned into JSON and will do that without hesitation. It's great when you need it, but does not spark joy in this moment.

Ideally we could tell Spring that any time we return a VoiceResponse it needs to serialize the object for HTTP by calling the .toXml() method and set application/xml in the headers, and we should only need to say that once, not on every single method.

Using Spring's HttpMessageConverter

This is precisely what we can do with a MessageConverter:

import com.twilio.twiml.TwiML;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.AbstractHttpMessageConverter;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.HttpMessageNotWritableException;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.nio.charset.StandardCharsets;

@Component
public class TwiMLMessageConverter extends AbstractHttpMessageConverter<TwiML> {

   public TwiMLMessageConverter() {
       super(MediaType.APPLICATION_XML, MediaType.ALL);
   }

   @Override
   protected boolean supports(Class<?> clazz) {
       return TwiML.class.isAssignableFrom(clazz);
   }

   @Override
   protected boolean canRead(MediaType mediaType) {
       return false; // we don't ever read TwiML
   }

   @Override
   protected TwiML readInternal(Class<? extends TwiML> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
       return null;
   }

   @Override
   protected void writeInternal(TwiML twiML, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
       outputMessage.getBody().write(twiML.toXml().getBytes(StandardCharsets.UTF_8));
   }
}

Our new converter is annotated as a @Component, and extends AbstractHttpMessageConverter<TwiML>. This means that Spring will find the class when scanning the project, and use the methods in this class whenever it needs to convert a matching object to an HTTP response. VoiceResponse is one of the subclasses of TwiML so by using TwiML.class.isAssignableFrom in the booleans supports method we are claiming VoiceResponse and all the other subclasses of TwiML as ours to convert. The actual conversion happens in the writeInternal method, which is a one-liner. The content-type header we need is specified in the constructor.

HttpMessageConverters can also read HTTP requests into Java objects but we have no need for that so return false for all requests in canRead and leave the readInternal method as empty as possible (it won't be called unless canRead returns true).

That's it - add the TwiMLMessageConverter to your project and all your TwiML responses will be automatically converted to XML and have the right header set. Note that this is best used purely for creating correctly formatted responses, although you could do any kind of manipulation to the object before you write the response, this converter is not the place for extra database calls or other business logic.

Are you building with Twilio and Java? I'd love to hear from you at mgilliard@twilio.com