diff --git a/spring-web/src/main/java/org/springframework/http/codec/xml/JacksonXmlDecoder.java b/spring-web/src/main/java/org/springframework/http/codec/xml/JacksonXmlDecoder.java
new file mode 100644
index 00000000000..d75e13f692b
--- /dev/null
+++ b/spring-web/src/main/java/org/springframework/http/codec/xml/JacksonXmlDecoder.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright 2002-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.http.codec.xml;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Map;
+
+import org.jspecify.annotations.Nullable;
+import org.reactivestreams.Publisher;
+import reactor.core.publisher.Flux;
+import tools.jackson.databind.cfg.MapperBuilder;
+import tools.jackson.dataformat.xml.XmlFactory;
+import tools.jackson.dataformat.xml.XmlMapper;
+
+import org.springframework.core.ResolvableType;
+import org.springframework.core.io.buffer.DataBuffer;
+import org.springframework.http.MediaType;
+import org.springframework.http.codec.AbstractJacksonDecoder;
+import org.springframework.util.MimeType;
+import org.springframework.util.xml.StaxUtils;
+
+/**
+ * Decode bytes into XML and convert to Objects with Jackson 3.x.
+ *
+ *
Stream decoding is currently not supported.
+ *
+ * @author Sebastien Deleuze
+ * @since 7.0.3
+ * @see JacksonXmlEncoder
+ */
+public class JacksonXmlDecoder extends AbstractJacksonDecoder {
+
+ private static final MediaType[] DEFAULT_XML_MIME_TYPES = new MediaType[] {
+ new MediaType("application", "xml", StandardCharsets.UTF_8),
+ new MediaType("text", "xml", StandardCharsets.UTF_8),
+ new MediaType("application", "*+xml", StandardCharsets.UTF_8)
+ };
+
+
+ /**
+ * Construct a new instance with a {@link XmlMapper} customized with the
+ * {@link tools.jackson.databind.JacksonModule}s found by
+ * {@link MapperBuilder#findModules(ClassLoader)}.
+ */
+ public JacksonXmlDecoder() {
+ super(XmlMapper.builder(defensiveXmlFactory()), DEFAULT_XML_MIME_TYPES);
+ }
+
+ /**
+ * Construct a new instance with the provided {@link XmlMapper.Builder}
+ * customized with the {@link tools.jackson.databind.JacksonModule}s
+ * found by {@link MapperBuilder#findModules(ClassLoader)}.
+ * @see XmlMapper#builder()
+ * @see #defensiveXmlFactory()
+ */
+ public JacksonXmlDecoder(XmlMapper.Builder builder) {
+ super(builder, DEFAULT_XML_MIME_TYPES);
+ }
+
+ /**
+ * Construct a new instance with the provided {@link XmlMapper}.
+ * @see XmlMapper#builder()
+ * @see #defensiveXmlFactory()
+ */
+ public JacksonXmlDecoder(XmlMapper mapper) {
+ super(mapper, DEFAULT_XML_MIME_TYPES);
+ }
+
+ /**
+ * Construct a new instance with the provided {@link XmlMapper.Builder}
+ * customized with the {@link tools.jackson.databind.JacksonModule}s
+ * found by {@link MapperBuilder#findModules(ClassLoader)}, and
+ * {@link MimeType}s.
+ * @see XmlMapper#builder()
+ * @see #defensiveXmlFactory()
+ */
+ public JacksonXmlDecoder(XmlMapper.Builder builder, MimeType... mimeTypes) {
+ super(builder, mimeTypes);
+ }
+
+ /**
+ * Construct a new instance with the provided {@link XmlMapper} and {@link MimeType}s.
+ * @see XmlMapper#builder()
+ * @see #defensiveXmlFactory()
+ */
+ public JacksonXmlDecoder(XmlMapper mapper, MimeType... mimeTypes) {
+ super(mapper, mimeTypes);
+ }
+
+
+ @Override
+ public Flux decode(Publisher input, ResolvableType elementType, @Nullable MimeType mimeType,
+ @Nullable Map hints) {
+
+ throw new UnsupportedOperationException("Stream decoding is currently not supported");
+ }
+
+ /**
+ * Return an {@link XmlFactory} created from {@link StaxUtils#createDefensiveInputFactory}
+ * with Spring's defensive setup, i.e. no support for the resolution of DTDs and external
+ * entities.
+ */
+ public static XmlFactory defensiveXmlFactory() {
+ return new XmlFactory(StaxUtils.createDefensiveInputFactory());
+ }
+
+}
diff --git a/spring-web/src/main/java/org/springframework/http/codec/xml/JacksonXmlEncoder.java b/spring-web/src/main/java/org/springframework/http/codec/xml/JacksonXmlEncoder.java
new file mode 100644
index 00000000000..a298a7b3d0f
--- /dev/null
+++ b/spring-web/src/main/java/org/springframework/http/codec/xml/JacksonXmlEncoder.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright 2002-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.http.codec.xml;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Map;
+
+import org.jspecify.annotations.Nullable;
+import org.reactivestreams.Publisher;
+import reactor.core.publisher.Flux;
+import tools.jackson.databind.cfg.MapperBuilder;
+import tools.jackson.dataformat.xml.XmlMapper;
+
+import org.springframework.core.ResolvableType;
+import org.springframework.core.io.buffer.DataBuffer;
+import org.springframework.core.io.buffer.DataBufferFactory;
+import org.springframework.http.MediaType;
+import org.springframework.http.codec.AbstractJacksonEncoder;
+import org.springframework.util.MimeType;
+
+/**
+ * Encode from an {@code Object} to bytes of XML objects using Jackson 3.x.
+ *
+ * Stream encoding is currently not supported.
+ *
+ * @author Sebastien Deleuze
+ * @since 7.0.3
+ * @see JacksonXmlDecoder
+ */
+public class JacksonXmlEncoder extends AbstractJacksonEncoder {
+
+ private static final MediaType[] DEFAULT_XML_MIME_TYPES = new MediaType[] {
+ new MediaType("application", "xml", StandardCharsets.UTF_8),
+ new MediaType("text", "xml", StandardCharsets.UTF_8),
+ new MediaType("application", "*+xml", StandardCharsets.UTF_8)
+ };
+
+
+ /**
+ * Construct a new instance with a {@link XmlMapper} customized with the
+ * {@link tools.jackson.databind.JacksonModule}s found by
+ * {@link MapperBuilder#findModules(ClassLoader)}.
+ */
+ public JacksonXmlEncoder() {
+ super(XmlMapper.builder(), DEFAULT_XML_MIME_TYPES);
+ }
+
+ /**
+ * Construct a new instance with the provided {@link XmlMapper.Builder}
+ * customized with the {@link tools.jackson.databind.JacksonModule}s
+ * found by {@link MapperBuilder#findModules(ClassLoader)}.
+ * @see XmlMapper#builder()
+ */
+ public JacksonXmlEncoder(XmlMapper.Builder builder) {
+ super(builder, DEFAULT_XML_MIME_TYPES);
+ }
+
+ /**
+ * Construct a new instance with the provided {@link XmlMapper}.
+ * @see XmlMapper#builder()
+ */
+ public JacksonXmlEncoder(XmlMapper mapper) {
+ super(mapper, DEFAULT_XML_MIME_TYPES);
+ }
+
+ /**
+ * Construct a new instance with the provided {@link XmlMapper.Builder}
+ * customized with the {@link tools.jackson.databind.JacksonModule}s
+ * found by {@link MapperBuilder#findModules(ClassLoader)}, and
+ * {@link MimeType}s.
+ * @see XmlMapper#builder()
+ */
+ public JacksonXmlEncoder(XmlMapper.Builder builder, MimeType... mimeTypes) {
+ super(builder, mimeTypes);
+ }
+
+ /**
+ * Construct a new instance with the provided {@link XmlMapper} and {@link MimeType}s.
+ * @see XmlMapper#builder()
+ */
+ public JacksonXmlEncoder(XmlMapper mapper, MimeType... mimeTypes) {
+ super(mapper, mimeTypes);
+ }
+
+
+ @Override
+ public Flux encode(Publisher> inputStream, DataBufferFactory bufferFactory, ResolvableType elementType,
+ @Nullable MimeType mimeType, @Nullable Map hints) {
+
+ throw new UnsupportedOperationException("Stream encoding is currently not supported");
+ }
+
+}
diff --git a/spring-web/src/test/java/org/springframework/http/codec/xml/JacksonXmlDecoderTests.java b/spring-web/src/test/java/org/springframework/http/codec/xml/JacksonXmlDecoderTests.java
new file mode 100644
index 00000000000..b9c62214799
--- /dev/null
+++ b/spring-web/src/test/java/org/springframework/http/codec/xml/JacksonXmlDecoderTests.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright 2002-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.http.codec.xml;
+
+import java.util.Arrays;
+import java.util.List;
+
+import org.junit.jupiter.api.Test;
+import reactor.core.publisher.Flux;
+import tools.jackson.core.JacksonException;
+import tools.jackson.dataformat.xml.XmlMapper;
+
+import org.springframework.core.ResolvableType;
+import org.springframework.core.io.buffer.DataBuffer;
+import org.springframework.core.testfixture.codec.AbstractDecoderTests;
+import org.springframework.http.MediaType;
+import org.springframework.web.testfixture.xml.Pojo;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
+
+/**
+ * Tests for {@link JacksonXmlDecoder}.
+ *
+ * @author Sebastien Deleuze
+ */
+class JacksonXmlDecoderTests extends AbstractDecoderTests {
+
+ private final Pojo pojo1 = new Pojo("f1", "b1");
+
+ private final Pojo pojo2 = new Pojo("f2", "b2");
+
+ private final XmlMapper mapper = XmlMapper.builder().build();
+
+
+ public JacksonXmlDecoderTests() {
+ super(new JacksonXmlDecoder());
+ }
+
+
+ @Override
+ @Test
+ protected void canDecode() {
+ assertThat(decoder.canDecode(ResolvableType.forClass(Pojo.class), MediaType.APPLICATION_XML)).isTrue();
+ assertThat(decoder.canDecode(ResolvableType.forClass(Pojo.class), MediaType.TEXT_XML)).isTrue();
+ assertThat(decoder.canDecode(ResolvableType.forClass(Pojo.class), new MediaType("application", "soap+xml"))).isTrue();
+ assertThat(decoder.canDecode(ResolvableType.forClass(Pojo.class), null)).isTrue();
+ assertThat(decoder.canDecode(ResolvableType.forClass(String.class), null)).isTrue();
+ assertThat(decoder.canDecode(ResolvableType.forClass(Pojo.class), MediaType.APPLICATION_JSON)).isFalse();
+ }
+
+ @Override
+ @Test
+ protected void decode() {
+ Flux input = Flux.just(this.pojo1, this.pojo2)
+ .map(this::writeObject)
+ .flatMap(this::dataBuffer);
+ assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() ->
+ testDecodeAll(input, Pojo.class, step -> step
+ .expectNext(pojo1)
+ .expectNext(pojo2)
+ .verifyComplete()));
+
+ }
+
+ private byte[] writeObject(Object o) {
+ try {
+ return this.mapper.writer().writeValueAsBytes(o);
+ }
+ catch (JacksonException e) {
+ throw new AssertionError(e);
+ }
+
+ }
+
+ @Override
+ @Test
+ protected void decodeToMono() {
+ List expected = Arrays.asList(pojo1, pojo2);
+
+ Flux input = Flux.just(expected)
+ .map(this::writeObject)
+ .flatMap(this::dataBuffer);
+
+ ResolvableType elementType = ResolvableType.forClassWithGenerics(List.class, Pojo.class);
+ testDecodeToMono(input, elementType, step -> step
+ .expectNext(expected)
+ .expectComplete()
+ .verify(), null, null);
+ }
+
+}
diff --git a/spring-web/src/test/java/org/springframework/http/codec/xml/JacksonXmlEncoderTests.java b/spring-web/src/test/java/org/springframework/http/codec/xml/JacksonXmlEncoderTests.java
new file mode 100644
index 00000000000..3306e072153
--- /dev/null
+++ b/spring-web/src/test/java/org/springframework/http/codec/xml/JacksonXmlEncoderTests.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2002-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.http.codec.xml;
+
+import java.util.function.Consumer;
+
+import org.junit.jupiter.api.Test;
+import reactor.core.publisher.Flux;
+import tools.jackson.dataformat.xml.XmlMapper;
+
+import org.springframework.core.ResolvableType;
+import org.springframework.core.io.buffer.DataBuffer;
+import org.springframework.core.testfixture.io.buffer.AbstractLeakCheckingTests;
+import org.springframework.core.testfixture.io.buffer.DataBufferTestUtils;
+import org.springframework.http.MediaType;
+import org.springframework.web.testfixture.xml.Pojo;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
+import static org.springframework.core.io.buffer.DataBufferUtils.release;
+
+/**
+ * Tests for {@link JacksonXmlEncoder}.
+ *
+ * @author Sebastien Deleuze
+ */
+class JacksonXmlEncoderTests extends AbstractLeakCheckingTests {
+
+ private final XmlMapper mapper = XmlMapper.builder().build();
+
+ private final JacksonXmlEncoder encoder = new JacksonXmlEncoder();
+
+
+ @Test
+ protected void canEncode() {
+ ResolvableType pojoType = ResolvableType.forClass(Pojo.class);
+ assertThat(this.encoder.canEncode(pojoType, MediaType.APPLICATION_XML)).isTrue();
+ assertThat(this.encoder.canEncode(pojoType, MediaType.TEXT_XML)).isTrue();
+ assertThat(this.encoder.canEncode(pojoType, new MediaType("application", "soap+xml"))).isTrue();
+ assertThat(this.encoder.canEncode(pojoType, null)).isTrue();
+ assertThat(this.encoder.canEncode(ResolvableType.forClass(String.class), null)).isTrue();
+ assertThat(this.encoder.canEncode(ResolvableType.NONE, null)).isTrue();
+ assertThat(this.encoder.canEncode(ResolvableType.forClass(Pojo.class), MediaType.APPLICATION_JSON)).isFalse();
+ }
+
+ @Test
+ protected void encode() {
+ Pojo value = new Pojo("foo", "bar");
+ DataBuffer result = encoder.encodeValue(value, this.bufferFactory, ResolvableType.forClass(Pojo.class),
+ MediaType.APPLICATION_XML, null);
+ pojoConsumer(value).accept(result);
+ }
+
+ private Consumer pojoConsumer(Pojo expected) {
+ return dataBuffer -> {
+ Pojo actual = this.mapper.reader().forType(Pojo.class)
+ .readValue(DataBufferTestUtils.dumpBytes(dataBuffer));
+ assertThat(actual).isEqualTo(expected);
+ release(dataBuffer);
+ };
+ }
+
+ @Test
+ void encodeStream() {
+ Pojo pojo1 = new Pojo("foo", "bar");
+ Pojo pojo2 = new Pojo("foofoo", "barbar");
+ Pojo pojo3 = new Pojo("foofoofoo", "barbarbar");
+ Flux input = Flux.just(pojo1, pojo2, pojo3);
+ ResolvableType type = ResolvableType.forClass(Pojo.class);
+ assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() ->
+ encoder.encode(input, this.bufferFactory, type, MediaType.APPLICATION_XML, null));
+ }
+
+}