Spring Boot
XML Response
Spring Boot can serve XML responses alongside JSON using Jackson's XML extension (jackson-dataformat-xml). No controller changes are required — Spring selects the serializer based on the client's Accept header. This entry covers setup, DTO annotation, content negotiation, custom serialization, and JAXB-based configuration.
Adding XML Support
Spring Boot's auto-configured HttpMessageConverter stack gains XML support as soon as jackson-dataformat-xml is on the classpath. No additional @Bean registration is needed — MappingJackson2XmlHttpMessageConverter is picked up automatically. Add the single dependency and XML becomes a supported media type across every @RestController.
XML
<!-- pom.xml -->
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
<!-- version managed by spring-boot-starter-parent -->
</dependency>
// ── Verify registration at startup (optional) ─────────────────────────
@Component
@Slf4j
public class ConverterLogger implements ApplicationRunner {
@Autowired
private RequestMappingHandlerAdapter adapter;
@Override
public void run(ApplicationArguments args) {
adapter.getMessageConverters().forEach(c ->
log.info("Converter: {}", c.getClass().getSimpleName()));
// Expect: MappingJackson2HttpMessageConverter
// MappingJackson2XmlHttpMessageConverter ← confirms XML is active
}
}Content Negotiation — JSON vs XML
Spring selects JSON or XML automatically based on the client's Accept header. No controller code changes are required. Declare both media types in produces to be explicit and to document the contract; omitting produces still works but is less clear.
Java
// ── Controller produces both JSON and XML ────────────────────────────
@RestController
@RequestMapping(
value = "/api/v1/users",
produces = {
MediaType.APPLICATION_JSON_VALUE, // Accept: application/json → JSON
MediaType.APPLICATION_XML_VALUE // Accept: application/xml → XML
}
)
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
@GetMapping("/{id}")
public ResponseEntity<UserResponse> findById(@PathVariable Long id) {
return ResponseEntity.ok(userService.findById(id));
// Same method, same return type — Spring picks the serializer
}
@GetMapping
public ResponseEntity<UserResponseList> findAll() {
return ResponseEntity.ok(new UserResponseList(userService.findAll()));
}
}
// ── Example HTTP requests ─────────────────────────────────────────────
// GET /api/v1/users/1
// Accept: application/json → {"id":1,"name":"Alice","email":"alice@example.com"}
// GET /api/v1/users/1
// Accept: application/xml → <UserResponse><id>1</id><name>Alice</name>...</UserResponse>
// ── Fallback when no Accept header is sent ────────────────────────────
// Spring defaults to the first entry in produces → JSONShaping XML Output with Annotations
Jackson XML annotations control the root element name, field element names, attribute vs element rendering, and list wrapping. Without them Jackson derives names from the class and field names, which is often good enough — but explicit annotation makes the contract stable and independent of refactoring.
Java
import com.fasterxml.jackson.dataformat.xml.annotation.*;
// ── Basic DTO ─────────────────────────────────────────────────────────
@JacksonXmlRootElement(localName = "user") // <user> not <UserResponse>
public record UserResponse(
@JacksonXmlProperty(isAttribute = true) // rendered as attribute: <user id="1">
Long id,
@JacksonXmlProperty(localName = "full_name") // <full_name>Alice</full_name>
String name,
String email // <email>alice@example.com</email>
) {}
// ── Wrapping a list ───────────────────────────────────────────────────
// Without a wrapper, Jackson XML repeats the root element for each item:
// <UserResponse><id>1</id>...</UserResponse><UserResponse>...
// Wrap with a container class instead:
@JacksonXmlRootElement(localName = "users")
public class UserResponseList {
@JacksonXmlElementWrapper(useWrapping = false)
@JacksonXmlProperty(localName = "user")
private final List<UserResponse> items;
public UserResponseList(List<UserResponse> items) {
this.items = items;
}
}
// ── Result ────────────────────────────────────────────────────────────
// <users>
// <user id="1"><full_name>Alice</full_name><email>alice@example.com</email></user>
// <user id="2"><full_name>Bob</full_name><email>bob@example.com</email></user>
// </users>XML-Only Endpoints
Some endpoints — exports, feeds, legacy integrations — must return XML exclusively. Restrict them with produces = MediaType.APPLICATION_XML_VALUE. Requests with Accept: application/json will receive 406 Not Acceptable automatically.
Java
@RestController
@RequestMapping("/api/v1/reports")
@RequiredArgsConstructor
public class ReportController {
private final ReportService reportService;
// ── XML-only endpoint ─────────────────────────────────────────────
@GetMapping(
value = "/export",
produces = MediaType.APPLICATION_XML_VALUE
)
public ResponseEntity<ReportExport> export(
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate from,
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate to) {
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=report-" + from + ".xml")
.body(reportService.export(from, to));
}
// ── RSS / Atom feed ───────────────────────────────────────────────
@GetMapping(
value = "/feed",
produces = "application/rss+xml"
)
public ResponseEntity<RssFeed> feed() {
return ResponseEntity.ok(reportService.buildFeed());
}
}
// ── DTO for the export ────────────────────────────────────────────────
@JacksonXmlRootElement(localName = "report")
public record ReportExport(
@JacksonXmlProperty(isAttribute = true)
String generatedAt,
@JacksonXmlElementWrapper(localName = "entries")
@JacksonXmlProperty(localName = "entry")
List<ReportEntry> entries
) {}Global XML Serialization Config
Customise the underlying XmlMapper — the XML counterpart of ObjectMapper — through Jackson2ObjectMapperBuilderCustomizer. Common tweaks include enabling pretty-printing, configuring namespace prefixes, and registering the JavaTimeModule so java.time types serialize correctly in XML.
Java
@Configuration
public class XmlConfig {
@Bean
public Jackson2ObjectMapperBuilderCustomizer xmlCustomizer() {
return builder -> builder
// Enable for both JSON and XML mappers:
.featuresToDisable(
SerializationFeature.WRITE_DATES_AS_TIMESTAMPS
)
.modules(new JavaTimeModule())
.timeZone(TimeZone.getTimeZone("UTC"))
// XML-specific: indent output
.featuresToEnable(SerializationFeature.INDENT_OUTPUT);
}
// ── Or configure XmlMapper directly for XML-only settings ─────────
@Bean
public XmlMapper xmlMapper() {
XmlMapper mapper = new XmlMapper();
mapper.enable(SerializationFeature.INDENT_OUTPUT);
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
mapper.registerModule(new JavaTimeModule());
return mapper;
}
}
// ── application.yml equivalents ───────────────────────────────────────
// spring:
// jackson:
// serialization:
// write-dates-as-timestamps: false
// indent-output: true
// deserialization:
// fail-on-unknown-properties: false
// time-zone: UTCAccepting XML Request Bodies
The same jackson-dataformat-xml dependency that enables XML responses also enables XML request body deserialization. Annotate the parameter with @RequestBody as usual — Spring selects the XML converter when the request's Content-Type is application/xml.
Java
// ── Controller accepts both JSON and XML bodies ───────────────────────
@RestController
@RequestMapping(
value = "/api/v1/users",
consumes = {
MediaType.APPLICATION_JSON_VALUE,
MediaType.APPLICATION_XML_VALUE
},
produces = {
MediaType.APPLICATION_JSON_VALUE,
MediaType.APPLICATION_XML_VALUE
}
)
public class UserController {
@PostMapping
public ResponseEntity<UserResponse> create(
@RequestBody @Valid CreateUserRequest request,
UriComponentsBuilder uriBuilder) {
UserResponse created = userService.create(request);
URI location = uriBuilder
.path("/api/v1/users/{id}")
.buildAndExpand(created.id())
.toUri();
return ResponseEntity.created(location).body(created);
}
}
// ── Request DTO — works for both JSON and XML input ───────────────────
@JacksonXmlRootElement(localName = "createUserRequest")
public record CreateUserRequest(
@NotBlank
@JacksonXmlProperty(localName = "full_name")
String name,
@NotBlank @Email
String email,
@NotNull
@JacksonXmlProperty(localName = "role")
String role
) {}
// ── Example XML request body ──────────────────────────────────────────
// POST /api/v1/users
// Content-Type: application/xml
//
// <createUserRequest>
// <full_name>Alice</full_name>
// <email>alice@example.com</email>
// <role>ADMIN</role>
// </createUserRequest>