Spring MVC decides which media type (for example JSON or XML) to use for a response by asking a ContentNegotiationManager, which delegates to an ordered list of ContentNegotiationStrategy implementations. In this example, we will register a custom strategy as the default strategy. It is the last one consulted: it is used only when none of the other enabled strategies (such as the 'Accept' header strategy or the request parameter strategy) can determine a media type for the request. It is custom because we decide programmatically which media types to return. Let's see how to do that.
Example
Creating a custom ContentNegotiationStrategy
package com.logicbig.example;
import org.springframework.http.MediaType;
import org.springframework.web.HttpMediaTypeNotAcceptableException;
import org.springframework.web.accept.ContentNegotiationStrategy;
import org.springframework.web.context.request.NativeWebRequest;
import java.util.ArrayList;
import java.util.List;
public class MyCustomContentNegotiationStrategy
implements ContentNegotiationStrategy {
@Override
public List<MediaType> resolveMediaTypes(NativeWebRequest nativeWebRequest)
throws HttpMediaTypeNotAcceptableException {
List<MediaType> mediaTypes = new ArrayList<>();
String acceptLang = nativeWebRequest.getHeader("Accept-Language");
if (acceptLang == null || !acceptLang.toLowerCase().contains("en")) {
mediaTypes.add(MediaType.APPLICATION_XML);
} else {
mediaTypes.add(MediaType.APPLICATION_JSON);
}
return mediaTypes;
}
}
Registering the custom ContentNegotiationStrategy
package com.logicbig.example;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ContentNegotiationConfigurer;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.config.annotation.ViewResolverRegistry;
@EnableWebMvc
@Configuration
@ComponentScan
public class MyWebConfig implements WebMvcConfigurer {
@Override
public void configureContentNegotiation(
ContentNegotiationConfigurer configurer) {
configurer.defaultContentTypeStrategy(
new MyCustomContentNegotiationStrategy());
}
}
Writing the Controller
package com.logicbig.example;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
@RequestMapping("user")
public class UserController {
@RequestMapping
@ResponseBody
public User getUserById (@RequestParam("id") long userId) {
//creating dummy user
User user = new User();
user.setId(userId);
user.setName("joe");
user.setEmailAddress("joe@example.com");
return user;
}
}
package com.logicbig.example;
import tools.jackson.dataformat.xml.annotation.JacksonXmlRootElement;
import java.io.Serializable;
@JacksonXmlRootElement
public class User implements Serializable {
private Long id;
private String name;
private String password;
private String emailAddress;
.............
}
Writing JUnit tests
In this test, we send neither an 'Accept' header nor a 'format' request parameter, so none of the other strategies can resolve a media type and our custom strategy is used. We also don't send an 'Accept-Language' header, so, per our logic, the returned media type is XML:
@ExtendWith(SpringExtension.class)
@WebAppConfiguration
@ContextConfiguration(classes = MyWebConfig.class)
public class UserTests {
@Autowired
private WebApplicationContext wac;
private MockMvcTester mvc;
.............
@Test
public void testUserRequest() {
assertThat(this.mvc.get()
.uri("/user")
.param("id", "100"))
.hasStatusOk()
.bodyText()
.contains("<emailAddress>joe@example.com"
+ "</emailAddress><id>100</id><name>joe"
+ "</name>");
}
.............
}
mvn test -Dtest=UserTests#testUserRequest Output (filtered)$ mvn test -Dtest=UserTests#testUserRequest [INFO] ------------------------------------------------------- [INFO] T E S T S [INFO] ------------------------------------------------------- [INFO] Running com.logicbig.example.UserTests [INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.592 s -- in com.logicbig.example.UserTests [INFO] [INFO] Results: [INFO] [INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0 [INFO] [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 4.610 s [INFO] Finished at: 2026-09-29T21:28:42-05:00 [INFO] ------------------------------------------------------------------------
In the following test, we set the 'Accept-Language' header:
@ExtendWith(SpringExtension.class)
@WebAppConfiguration
@ContextConfiguration(classes = MyWebConfig.class)
public class UserTests {
@Autowired
private WebApplicationContext wac;
private MockMvcTester mvc;
.............
@Test
public void testUserRequestWithAcceptLangHeader() {
assertThat(this.mvc.get()
.uri("/user")
.header("Accept-Language", "en-US")
.param("id", "100"))
.hasStatusOk()
.bodyText()
.isEqualTo(
"{\"emailAddress\":\"joe@example.com\","
+ "\"id\":100,\"name\":\"joe\","
+ "\"password\":null}");
}
}
mvn test -Dtest=UserTests#testUserRequestWithAcceptLangHeader Output (filtered)$ mvn test -Dtest=UserTests#testUserRequestWithAcceptLangHeader [INFO] ------------------------------------------------------- [INFO] T E S T S [INFO] ------------------------------------------------------- [INFO] Running com.logicbig.example.UserTests [INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.253 s -- in com.logicbig.example.UserTests [INFO] [INFO] Results: [INFO] [INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0 [INFO] [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 3.265 s [INFO] Finished at: 2026-09-29T11:45:32-05:00 [INFO] ------------------------------------------------------------------------
Note that a browser (like Chrome) will typically get an XML response from this example. Browsers implicitly send an 'Accept' header listing several media types, including XML, so on the server side HeaderContentNegotiationStrategy resolves the media type before our custom strategy is consulted.
Example ProjectDependencies and Technologies Used: - spring-webmvc 7.0.6 (Spring Web MVC)
Version Compatibility: 4.1.2.RELEASE - 7.0.6 Version compatibilities of spring-webmvc with this example: Versions in green have been tested.
- spring-test 7.0.6 (Spring TestContext Framework)
- jakarta.servlet-api 6.1.0 (Jakarta Servlet API documentation)
- jackson-databind 3.1.0 (General data-binding functionality for Jackson: works on core streaming API)
- jackson-dataformat-xml 3.1.0 (Data format extension for Jackson to offer
alternative support for serializing POJOs as XML and deserializing XML as POJOs)
- junit-jupiter-engine 6.0.3 (Module "junit-jupiter-engine" of JUnit)
- hamcrest 3.0 (Core API and libraries of hamcrest matcher framework)
- assertj-core 3.26.3 (Rich and fluent assertions for testing in Java)
- JDK 25
- Maven 3.9.11
|