Spring Boot Tutorial
Content Negotiation: JSON and XML
Most clients want JSON, but some — older enterprise systems, banking and government integrations, RSS consumers — expect XML. Content negotiation lets one endpoint return different representations of the same resource, chosen by the client's Accept header, and accept different formats in request bodies based on Content-Type.
This lesson shows how Spring MVC chooses a message converter, how to add XML support with Jackson's XML module, how to shape XML output, how to restrict or add media types per endpoint, and how the 406 and 415 errors arise.
How Spring Chooses a Format
For a @RestController return value, Spring MVC compares the client's Accept header with the media types that registered HttpMessageConverters can produce (and the produces attribute of the mapping, if set), then picks the best match. For @RequestBody, it picks the converter that can read the request's Content-Type. If nothing matches the Accept header you get 406 Not Acceptable; if nothing can read the body, 415 Unsupported Media Type.
Adding XML Support
Add jackson-dataformat-xml (the Jackson 3 artifact is tools.jackson.dataformat:jackson-dataformat-xml, version managed by Spring Boot). Spring Boot detects it and registers an XML converter automatically; the same DTOs now serialise to XML. Jackson XML annotations — @JacksonXmlRootElement, @JacksonXmlProperty(isAttribute = true), @JacksonXmlElementWrapper — control element names, attributes and list wrapping. Spring Boot 4.1 also exposes XML factory customisation through XmlFactoryBuilderCustomizer.
Restricting Formats per Endpoint
Use produces to limit an endpoint to certain formats (produces = {APPLICATION_JSON_VALUE, APPLICATION_XML_VALUE}) and consumes for accepted request formats. JSON is the default when a client sends no Accept header or */* because the JSON converter is registered first.
Query Parameter or Extension Strategies
Some clients cannot set headers (e.g. a link in a browser). You can enable a query parameter strategy such as ?format=xml with spring.mvc.contentnegotiation.favor-parameter=true. Path extensions like /users.xml are no longer supported by default because they caused security and ambiguity problems.
Examples
Adding XML and returning both formats from one endpoint
<dependency>
<groupId>tools.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
</dependency>
@JacksonXmlRootElement(localName = "course")
public record CourseDto(
@JacksonXmlProperty(isAttribute = true) String slug,
String title,
int lessons,
@JacksonXmlElementWrapper(localName = "tags") @JacksonXmlProperty(localName = "tag") List<String> tags) {}
@RestController
@RequestMapping("/api/courses")
public class CourseController {
@GetMapping(value = "/{slug}", produces = {MediaType.APPLICATION_JSON_VALUE, MediaType.APPLICATION_XML_VALUE})
public CourseDto get(@PathVariable String slug) {
return new CourseDto(slug, "Spring Boot", 100, List.of("java", "spring", "backend"));
}
}
curl -H "Accept: application/json" localhost:8080/api/courses/spring-boot
{"slug":"spring-boot","title":"Spring Boot","lessons":100,"tags":["java","spring","backend"]}
curl -H "Accept: application/xml" localhost:8080/api/courses/spring-boot
<course slug="spring-boot"><title>Spring Boot</title><lessons>100</lessons><tags><tag>java</tag><tag>spring</tag><tag>backend</tag></tags></course>
curl -H "Accept: text/csv" localhost:8080/api/courses/spring-boot
-> 406 Not Acceptable
Accepting XML request bodies
@PostMapping(consumes = {MediaType.APPLICATION_JSON_VALUE, MediaType.APPLICATION_XML_VALUE})
public ResponseEntity<CourseDto> create(@RequestBody CourseDto course) {
return ResponseEntity.status(HttpStatus.CREATED).body(course);
}
curl -X POST -H "Content-Type: application/xml" -H "Accept: application/json" \
-d '<course slug="sql"><title>SQL</title><lessons>40</lessons><tags><tag>db</tag></tags></course>' \
localhost:8080/api/courses
-> 201 {"slug":"sql","title":"SQL","lessons":40,"tags":["db"]}
curl -X POST -H "Content-Type: text/plain" -d 'hello' localhost:8080/api/courses
-> 415 Unsupported Media Type
Choosing the format with a query parameter
# application.yml
spring:
mvc:
contentnegotiation:
favor-parameter: true
parameter-name: format
media-types:
xml: application/xml
json: application/json
GET /api/courses/spring-boot?format=xml -> XML response
GET /api/courses/spring-boot?format=json -> JSON response
Common Mistakes
- Adding the Jackson 2 artifact com.fasterxml.jackson.dataformat:jackson-dataformat-xml to a Spring Boot 4 app instead of the tools.jackson one.
- Expecting XML without sending Accept: application/xml — JSON remains the default.
- Using path extensions (/users.xml), which are disabled by default in modern Spring MVC.
- Setting produces to XML only and breaking existing JSON clients.
- Forgetting that XML lists need wrapper configuration to produce the element structure clients expect.
Key Points to Remember
- Spring MVC picks message converters from the Accept and Content-Type headers.
- Adding jackson-dataformat-xml enables XML automatically for the same DTOs.
- Jackson XML annotations shape root elements, attributes and list wrappers.
- produces/consumes restrict formats; mismatches return 406 or 415.
- A query parameter strategy (?format=xml) helps clients that cannot set headers.
Practice the examples
Change an input, predict the result, then compare it with the output. Explain why the result changes.
Use your local JDK or project IDE for these examples. Codelab currently runs Python and HTML/CSS/JavaScript; framework examples may need project dependencies.