We have seen how content negotiation works in Spring. In this example, we will turn on ParameterContentNegotiationStrategy. It lets the client choose the response media type with a query parameter named 'format', for example: http://www.example.com/app?format=xml.
Example
Turning on ParameterContentNegotiationStrategy
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.favorParameter(true);
}
}
Calling favorParameter(true) enables the strategy, but the media type mappings must also be registered explicitly, e.g. configurer.mediaType("xml", MediaType.APPLICATION_XML). Older Spring versions resolved common extensions such as 'xml' automatically; current versions do not, and without the mapping a request like ?format=xml can fail with a 406 (Not Acceptable) response.
Writing Controller
package com.logicbig.example;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
@Controller
@RequestMapping("user")
public class UserController {
@RequestMapping(produces = MediaType.APPLICATION_XML_VALUE)
@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;
}
@RequestMapping
@ResponseBody
public String getUserStringById (@RequestParam("id") long userId) {
return "joe, id: "+userId;
}
}
package com.logicbig.example;
import com.fasterxml.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 will make a request without the 'format' parameter:
@ExtendWith(SpringExtension.class)
@WebAppConfiguration
@ContextConfiguration(classes = MyWebConfig.class)
public class UserTests {
@Autowired
private WebApplicationContext wac;
private MockMvcTester mvc;
.............
@Test
public void testUserRequestWithoutFormatParam() {
assertThat(this.mvc.get()
.uri("/user")
.param("id", "10"))
.hasStatusOk()
.bodyText()
.isEqualTo("joe, id: 10");
}
.............
}
mvn test -Dtest=UserTests#testUserRequestWithoutFormatParam Output (filtered)$ mvn test -Dtest=UserTests#testUserRequestWithoutFormatParam [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: 2.060 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: 9.211 s [INFO] Finished at: 2026-09-29T06:45:08-05:00 [INFO] ------------------------------------------------------------------------
In the above output, the request is mapped to the handler method that returns a String. Now let's send the request with the format=xml parameter:
@ExtendWith(SpringExtension.class)
@WebAppConfiguration
@ContextConfiguration(classes = MyWebConfig.class)
public class UserTests {
@Autowired
private WebApplicationContext wac;
private MockMvcTester mvc;
.............
@Test
public void testUserRequestWithFormatParam() {
assertThat(this.mvc.get()
.uri("/user")
.param("format", "xml")
.param("id", "10"))
.hasStatusOk()
.bodyText()
.contains("<id>10</id>",
"<name>joe</name",
"<emailAddress>joe@example.com"
+ "</emailAddress>");
}
}
mvn test -Dtest=UserTests#testUserRequestWithFormatParam Output (filtered)$ mvn test -Dtest=UserTests#testUserRequestWithFormatParam [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.318 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: 6.281 s [INFO] Finished at: 2026-09-29T06:46:18-05:00 [INFO] ------------------------------------------------------------------------
Output on the browser
In our example, a browser such as Chrome will always get an XML response. By default, it sends an 'Accept' header similar to the following with GET requests:
Accept:text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Because the header lists application/xml, HeaderContentNegotiationStrategy matches the handler method that declares produces = application/xml, which is more specific than the String handler. So the 'format' parameter is never needed in the browser. One way to test our code is to turn off HeaderContentNegotiationStrategy on the application side:
@EnableWebMvc
@Configuration
@ComponentScan
public class MyWebConfig
implements WebMvcConfigurer {
@Override
public void configureContentNegotiation(
ContentNegotiationConfigurer configurer) {
configurer.favorParameter(true)
.ignoreAcceptHeader(true)
.mediaType("xml", MediaType.APPLICATION_XML);
}
}
Now we get the expected results in the browser:
URL: http://localhost:8080/user?id=2
URL: http://localhost:8080/user?id=2&format=xml
Which strategy should you use?
The query parameter is handy for quick browser testing and shareable links. For REST APIs, the 'Accept' header is generally preferred because it keeps the URL free of representation details. The path extension strategy is no longer supported in current Spring versions, so the query parameter is the remaining URL-based option.
Example ProjectDependencies and Technologies Used: - spring-webmvc 7.0.6 (Spring Web MVC)
Version Compatibility: 3.2.9.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)
- junit-jupiter-engine 6.0.3 (Module "junit-jupiter-engine" of JUnit)
- hamcrest 3.0 (Core API and libraries of hamcrest matcher framework)
- jackson-dataformat-xml 2.21.1 (Data format extension for Jackson to offer
alternative support for serializing POJOs as XML and deserializing XML as POJOs)
- assertj-core 3.26.3 (Rich and fluent assertions for testing in Java)
- JDK 25
- Maven 3.9.11
|