Backend Development 8 min read

How to Build a Custom HttpMessageConverter in Spring Boot 2.5

This guide explains how to implement a custom HttpMessageConverter in Spring Boot 2.5, covering the converter class, configuration, data model, controller, testing steps, and the internal request‑handling flow within the Spring MVC framework.

Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
How to Build a Custom HttpMessageConverter in Spring Boot 2.5

Environment: Spring Boot 2.5.12

Message Format

Input parameters: name:张三,age:20

Custom HttpMessageConverter

<code>public class CustomHttpMessageConverter extends AbstractHttpMessageConverter<Object> {
  private static Logger logger = LoggerFactory.getLogger(CustomHttpMessageConverter.class);

  // Only support Users type
  @Override
  protected boolean supports(Class<?> clazz) {
    return Users.class == clazz;
  }

  // Convert request body to Users instance
  @Override
  protected Object readInternal(Class<? extends Object> clazz, HttpInputMessage inputMessage)
      throws IOException, HttpMessageNotReadableException {
    String content = inToString(inputMessage.getBody());
    String[] keys = content.split(",");
    Users instance = null;
    try {
      instance = (Users) clazz.newInstance();
    } catch (Exception e1) {
      e1.printStackTrace();
    }
    for (String key : keys) {
      String[] vk = key.split(":");
      try {
        Field[] fields = clazz.getDeclaredFields();
        for (Field f : fields) {
          if (f.getName().equals(vk[0])) {
            f.setAccessible(true);
            Class<?> type = f.getType();
            if (String.class == type) {
              f.set(instance, vk[1]);
            } else if (Integer.class == type) {
              f.set(instance, Integer.parseInt(vk[1]));
            }
            break;
          }
        }
      } catch (Exception e) {
        logger.error("错误:{}", e);
      }
    }
    return instance;
  }

  // Write response using object's toString()
  @Override
  protected void writeInternal(Object t, HttpOutputMessage outputMessage)
      throws IOException, HttpMessageNotWritableException {
    outputMessage.getBody().write(t.toString().getBytes());
  }

  @Override
  protected boolean canWrite(MediaType mediaType) {
    if (mediaType == null || MediaType.ALL.equalsTypeAndSubtype(mediaType)) {
      return true;
    }
    for (MediaType supportedMediaType : getSupportedMediaTypes()) {
      if (supportedMediaType.isCompatibleWith(mediaType)) {
        return true;
      }
    }
    return false;
  }

  private String inToString(InputStream is) {
    byte[] buf = new byte[10 * 1024];
    int leng = -1;
    StringBuilder sb = new StringBuilder();
    try {
      while ((leng = is.read(buf)) != -1) {
        sb.append(new String(buf, 0, leng));
      }
      return sb.toString();
    } catch (IOException e) {
      throw new RuntimeException(e);
    }
  }
}
</code>

Registering the Converter

<code>@Configuration
public class WebConfig implements WebMvcConfigurer {

  @Override
  public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
    CustomHttpMessageConverter messageConvert = new CustomHttpMessageConverter();
    List<MediaType> supportedMediaTypes = new ArrayList<>();
    supportedMediaTypes.add(new MediaType("application", "fm"));
    messageConvert.setSupportedMediaTypes(supportedMediaTypes);
    converters.add(messageConvert);
    WebMvcConfigurer.super.configureMessageConverters(converters);
  }
}
</code>

When configuring the converter, the supported media type is set to application/fm , so clients must send requests with this Content‑Type.

Parameter Object

<code>public class Users {
  private String name;
  private Integer age;

  @Override
  public String toString() {
    return "【name = " + this.name + ", age = " + this.age + "】";
  }
}
</code>

Controller

<code>@RestController
@RequestMapping("/message")
public class MessageController {

  @PostMapping("/save")
  public Users save(@RequestBody Users user) {
    System.out.println("接受到内容:" + user);
    return user;
  }
}
</code>

Testing

Request example (Content‑Type: application/fm ) and corresponding response are shown in the images below.

Why These Methods Must Be Overridden

The @RequestBody annotation triggers the RequestResponseBodyMethodProcessor which is invoked by the DispatcherServlet . The processing flow passes through RequestMappingHandlerAdapter#handleInternal , then invokeHandlerMethod , and eventually reaches ServletInvocableHandlerMethod#invokeAndHandle . Inside this chain, AbstractMessageConverterMethodArgumentResolver#readWithMessageConverters iterates over the registered HttpMessageConverter instances. Because we added CustomHttpMessageConverter to the converter list, it is selected, and its readInternal method parses the custom application/fm payload into a Users object. The writeInternal method performs the reverse operation for responses.

During debugging, the flow reaches the custom converter, confirming that the canRead check (inherited from the parent class) succeeds for the Users type, and the readInternal method processes the message content as expected.

Backend DevelopmentSpring BootRequestBodyCustom ConverterHttpMessageConverter
Spring Full-Stack Practical Cases
Written by

Spring Full-Stack Practical Cases

Full-stack Java development with Vue 2/3 front-end suite; hands-on examples and source code analysis for Spring, Spring Boot 2/3, and Spring Cloud.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.