diff --git a/CHANGELOG.md b/CHANGELOG.md
index 582b98174c..20b05cbf60 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,7 @@
- Add explicit keep rules for RxJava `Result` types to prevent their generic information from being removed.
- Add `allowoptimization` flags for most kept types.
- Add `Invocation.annotationUrl` which returns the original URL from the method annotation.
+ - Add a converter for Jackson 3.
**Changed**
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 7fd8a71eca..e384e7e750 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -21,6 +21,8 @@ kotlinx-serialization = "1.10.0"
autoService = "1.1.1"
incap = "1.0.0"
jackson = "2.21.1"
+jacksonAnnotations = "2.21"
+jackson3 = "3.1.3"
[libraries]
androidPlugin = "com.android.tools.build:gradle:9.1.0"
@@ -63,8 +65,11 @@ rxjava3 = { module = "io.reactivex.rxjava3:rxjava", version = "3.1.12" }
reactiveStreams = { module = "org.reactivestreams:reactive-streams", version = "1.0.4" }
scalaLibrary = { module = "org.scala-lang:scala-library", version = "2.13.18" }
gson = { module = "com.google.code.gson:gson", version = "2.13.2" }
+jacksonAnnotations = { module = "com.fasterxml.jackson.core:jackson-annotations", version.ref = "jacksonAnnotations" }
jacksonDatabind = { module = "com.fasterxml.jackson.core:jackson-databind", version.ref = "jackson" }
jacksonDataformatCbor = { module = "com.fasterxml.jackson.dataformat:jackson-dataformat-cbor", version.ref = "jackson" }
+jackson3Databind = { module = "tools.jackson.core:jackson-databind", version.ref = "jackson3" }
+jackson3DataformatCbor = { module = "tools.jackson.dataformat:jackson-dataformat-cbor", version.ref = "jackson3" }
jaxbApi = { module = "javax.xml.bind:jaxb-api", version = "2.3.1" }
jaxbImpl = { module = "org.glassfish.jaxb:jaxb-runtime", version = "4.0.7" }
jaxb3Api = { module = "jakarta.xml.bind:jakarta.xml.bind-api", version = "3.0.1" }
diff --git a/retrofit-converters/jackson/build.gradle b/retrofit-converters/jackson/build.gradle
index 7900d02c48..3024360eb2 100644
--- a/retrofit-converters/jackson/build.gradle
+++ b/retrofit-converters/jackson/build.gradle
@@ -10,6 +10,7 @@ dependencies {
testImplementation libs.truth
testImplementation libs.okhttp.mockwebserver
testImplementation libs.testParameterInjector
+ testImplementation libs.jacksonAnnotations
testImplementation libs.jacksonDataformatCbor
}
diff --git a/retrofit-converters/jackson3/README.md b/retrofit-converters/jackson3/README.md
new file mode 100644
index 0000000000..46710fbcbb
--- /dev/null
+++ b/retrofit-converters/jackson3/README.md
@@ -0,0 +1,34 @@
+Jackson Converter
+=================
+
+A `Converter` which uses [Jackson][1] 3 for serialization to and from JSON.
+
+A default `ObjectMapper` instance will be created or one can be configured and passed to the
+`JacksonConverterFactory` construction to further control the serialization.
+
+
+Download
+--------
+
+Download [the latest JAR][2] or grab via [Maven][3]:
+```xml
+
+ com.squareup.retrofit2
+ converter-jackson3
+ latest.version
+
+```
+or [Gradle][3]:
+```groovy
+implementation 'com.squareup.retrofit2:converter-jackson3:latest.version'
+```
+
+Snapshots of the development version are available in [Sonatype's `snapshots` repository][snap].
+
+
+
+ [1]: https://github.com/FasterXML/jackson
+ [2]: https://search.maven.org/remote_content?g=com.squareup.retrofit2&a=converter-jackson3&v=LATEST
+ [3]: http://search.maven.org/#search%7Cga%7C1%7Cg%3A%22com.squareup.retrofit2%22%20a%3A%22converter-jackson3%22
+ [snap]: https://s01.oss.sonatype.org/content/repositories/snapshots/
+
diff --git a/retrofit-converters/jackson3/build.gradle b/retrofit-converters/jackson3/build.gradle
new file mode 100644
index 0000000000..3e90ae70cd
--- /dev/null
+++ b/retrofit-converters/jackson3/build.gradle
@@ -0,0 +1,27 @@
+apply plugin: 'java-library'
+apply plugin: 'com.vanniktech.maven.publish'
+
+dependencies {
+ api projects.retrofit
+ api libs.jackson3Databind
+ compileOnly libs.findBugsAnnotations
+
+ testImplementation libs.junit
+ testImplementation libs.truth
+ testImplementation libs.okhttp.mockwebserver
+ testImplementation libs.testParameterInjector
+ testImplementation libs.jacksonAnnotations
+ testImplementation libs.jackson3DataformatCbor
+}
+
+jar {
+ manifest {
+ attributes 'Automatic-Module-Name': 'retrofit2.converter.jackson3'
+ }
+}
+
+java {
+ toolchain {
+ languageVersion = JavaLanguageVersion.of(17)
+ }
+}
diff --git a/retrofit-converters/jackson3/gradle.properties b/retrofit-converters/jackson3/gradle.properties
new file mode 100644
index 0000000000..32b13fca14
--- /dev/null
+++ b/retrofit-converters/jackson3/gradle.properties
@@ -0,0 +1,3 @@
+POM_ARTIFACT_ID=converter-jackson3
+POM_NAME=Converter: Jackson3
+POM_DESCRIPTION=A Retrofit Converter which uses Jackson 3 for serialization.
diff --git a/retrofit-converters/jackson3/src/main/java/retrofit2/converter/jackson3/JacksonConverterFactory.java b/retrofit-converters/jackson3/src/main/java/retrofit2/converter/jackson3/JacksonConverterFactory.java
new file mode 100644
index 0000000000..4c05113bf8
--- /dev/null
+++ b/retrofit-converters/jackson3/src/main/java/retrofit2/converter/jackson3/JacksonConverterFactory.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2015 Square, Inc.
+ *
+ * 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
+ *
+ * http://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 retrofit2.converter.jackson3;
+
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Type;
+import okhttp3.MediaType;
+import okhttp3.RequestBody;
+import okhttp3.ResponseBody;
+import retrofit2.Call;
+import retrofit2.Converter;
+import retrofit2.Retrofit;
+import tools.jackson.databind.JavaType;
+import tools.jackson.databind.ObjectMapper;
+import tools.jackson.databind.ObjectReader;
+import tools.jackson.databind.ObjectWriter;
+
+/**
+ * A {@linkplain Converter.Factory converter} which uses Jackson.
+ *
+ *
Because Jackson is so flexible in the types it supports, this converter assumes that it can
+ * handle all types. If you are mixing JSON serialization with something else (such as protocol
+ * buffers), you must {@linkplain Retrofit.Builder#addConverterFactory(Converter.Factory) add this
+ * instance} last to allow the other converters a chance to see their types.
+ */
+public final class JacksonConverterFactory extends Converter.Factory {
+ private static final MediaType DEFAULT_MEDIA_TYPE =
+ MediaType.get("application/json; charset=UTF-8");
+
+ /** Create an instance using a default {@link ObjectMapper} instance for conversion. */
+ public static JacksonConverterFactory create() {
+ return new JacksonConverterFactory(new ObjectMapper(), DEFAULT_MEDIA_TYPE, false);
+ }
+
+ /** Create an instance using {@code mapper} for conversion. */
+ public static JacksonConverterFactory create(ObjectMapper mapper) {
+ return create(mapper, DEFAULT_MEDIA_TYPE);
+ }
+
+ /** Create an instance using {@code mapper} and {@code mediaType} for conversion. */
+ @SuppressWarnings("ConstantConditions") // Guarding public API nullability.
+ public static JacksonConverterFactory create(ObjectMapper mapper, MediaType mediaType) {
+ if (mapper == null) throw new NullPointerException("mapper == null");
+ if (mediaType == null) throw new NullPointerException("mediaType == null");
+ return new JacksonConverterFactory(mapper, mediaType, false);
+ }
+
+ private final ObjectMapper mapper;
+ private final MediaType mediaType;
+ private final boolean streaming;
+
+ private JacksonConverterFactory(ObjectMapper mapper, MediaType mediaType, boolean streaming) {
+ this.mapper = mapper;
+ this.mediaType = mediaType;
+ this.streaming = streaming;
+ }
+
+ /**
+ * Return a new factory which streams serialization of request messages to bytes on the HTTP thread
+ * This is either the calling thread for {@link Call#execute()}, or one of OkHttp's background
+ * threads for {@link Call#enqueue}. Response bytes are always converted to message instances on
+ * one of OkHttp's background threads.
+ */
+ public JacksonConverterFactory withStreaming() {
+ return new JacksonConverterFactory(mapper, mediaType, true);
+ }
+
+ @Override
+ public Converter responseBodyConverter(
+ Type type, Annotation[] annotations, Retrofit retrofit) {
+ JavaType javaType = mapper.getTypeFactory().constructType(type);
+ ObjectReader reader = mapper.readerFor(javaType);
+ return new JacksonResponseBodyConverter<>(reader);
+ }
+
+ @Override
+ public Converter, RequestBody> requestBodyConverter(
+ Type type,
+ Annotation[] parameterAnnotations,
+ Annotation[] methodAnnotations,
+ Retrofit retrofit) {
+ JavaType javaType = mapper.getTypeFactory().constructType(type);
+ ObjectWriter writer = mapper.writerFor(javaType);
+ return new JacksonRequestBodyConverter<>(writer, mediaType, streaming);
+ }
+}
diff --git a/retrofit-converters/jackson3/src/main/java/retrofit2/converter/jackson3/JacksonRequestBodyConverter.java b/retrofit-converters/jackson3/src/main/java/retrofit2/converter/jackson3/JacksonRequestBodyConverter.java
new file mode 100644
index 0000000000..251b987087
--- /dev/null
+++ b/retrofit-converters/jackson3/src/main/java/retrofit2/converter/jackson3/JacksonRequestBodyConverter.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2015 Square, Inc.
+ *
+ * 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
+ *
+ * http://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 retrofit2.converter.jackson3;
+
+import okhttp3.MediaType;
+import okhttp3.RequestBody;
+import retrofit2.Converter;
+import tools.jackson.core.JacksonException;
+import tools.jackson.databind.ObjectWriter;
+
+import java.io.IOException;
+
+final class JacksonRequestBodyConverter implements Converter {
+ private final ObjectWriter adapter;
+ private final MediaType mediaType;
+ private final boolean streaming;
+
+ JacksonRequestBodyConverter(ObjectWriter adapter, MediaType mediaType, boolean streaming) {
+ this.adapter = adapter;
+ this.mediaType = mediaType;
+ this.streaming = streaming;
+ }
+
+ @Override
+ public RequestBody convert(T value) throws IOException {
+ if (streaming) {
+ return new JacksonStreamingRequestBody(adapter, value, mediaType);
+ }
+
+ try {
+ byte[] bytes = adapter.writeValueAsBytes(value);
+ return RequestBody.create(bytes, mediaType);
+ } catch (JacksonException e) {
+ throw new IOException(e);
+ }
+ }
+}
diff --git a/retrofit-converters/jackson3/src/main/java/retrofit2/converter/jackson3/JacksonResponseBodyConverter.java b/retrofit-converters/jackson3/src/main/java/retrofit2/converter/jackson3/JacksonResponseBodyConverter.java
new file mode 100644
index 0000000000..f13a888210
--- /dev/null
+++ b/retrofit-converters/jackson3/src/main/java/retrofit2/converter/jackson3/JacksonResponseBodyConverter.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2015 Square, Inc.
+ *
+ * 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
+ *
+ * http://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 retrofit2.converter.jackson3;
+
+import okhttp3.ResponseBody;
+import retrofit2.Converter;
+import tools.jackson.core.JacksonException;
+import tools.jackson.databind.ObjectReader;
+
+import java.io.IOException;
+
+final class JacksonResponseBodyConverter implements Converter {
+ private final ObjectReader adapter;
+
+ JacksonResponseBodyConverter(ObjectReader adapter) {
+ this.adapter = adapter;
+ }
+
+ @Override
+ public T convert(ResponseBody value) throws IOException {
+ try (value) {
+ return adapter.readValue(value.byteStream());
+ } catch (JacksonException e) {
+ throw new IOException(e);
+ }
+ }
+}
diff --git a/retrofit-converters/jackson3/src/main/java/retrofit2/converter/jackson3/JacksonStreamingRequestBody.java b/retrofit-converters/jackson3/src/main/java/retrofit2/converter/jackson3/JacksonStreamingRequestBody.java
new file mode 100644
index 0000000000..62255ba3ac
--- /dev/null
+++ b/retrofit-converters/jackson3/src/main/java/retrofit2/converter/jackson3/JacksonStreamingRequestBody.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2025 Square, Inc.
+ *
+ * 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
+ *
+ * http://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 retrofit2.converter.jackson3;
+
+import okhttp3.MediaType;
+import okhttp3.RequestBody;
+import okio.BufferedSink;
+import tools.jackson.core.JacksonException;
+import tools.jackson.databind.ObjectWriter;
+
+import java.io.IOException;
+
+final class JacksonStreamingRequestBody extends RequestBody {
+ private final ObjectWriter adapter;
+ private final Object value;
+ private final MediaType mediaType;
+
+ public JacksonStreamingRequestBody(ObjectWriter adapter, Object value, MediaType mediaType) {
+ this.adapter = adapter;
+ this.value = value;
+ this.mediaType = mediaType;
+ }
+
+ @Override
+ public MediaType contentType() {
+ return mediaType;
+ }
+
+ @Override
+ public void writeTo(BufferedSink sink) throws IOException {
+ try {
+ adapter.writeValue(sink.outputStream(), value);
+ } catch (JacksonException e) {
+ throw new IOException(e);
+ }
+ }
+}
diff --git a/retrofit-converters/jackson3/src/main/java/retrofit2/converter/jackson3/package-info.java b/retrofit-converters/jackson3/src/main/java/retrofit2/converter/jackson3/package-info.java
new file mode 100644
index 0000000000..d7f9874e32
--- /dev/null
+++ b/retrofit-converters/jackson3/src/main/java/retrofit2/converter/jackson3/package-info.java
@@ -0,0 +1,2 @@
+@retrofit2.internal.EverythingIsNonNull
+package retrofit2.converter.jackson3;
diff --git a/retrofit-converters/jackson3/src/test/java/retrofit2/converter/jackson3/JacksonCborConverterFactoryTest.java b/retrofit-converters/jackson3/src/test/java/retrofit2/converter/jackson3/JacksonCborConverterFactoryTest.java
new file mode 100644
index 0000000000..092583281e
--- /dev/null
+++ b/retrofit-converters/jackson3/src/test/java/retrofit2/converter/jackson3/JacksonCborConverterFactoryTest.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2024 Square, Inc.
+ *
+ * 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
+ *
+ * http://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 retrofit2.converter.jackson3;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import java.io.IOException;
+import okhttp3.MediaType;
+import okhttp3.mockwebserver.MockResponse;
+import okhttp3.mockwebserver.MockWebServer;
+import okhttp3.mockwebserver.RecordedRequest;
+import okio.Buffer;
+import okio.ByteString;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import retrofit2.Call;
+import retrofit2.Response;
+import retrofit2.Retrofit;
+import retrofit2.http.Body;
+import retrofit2.http.POST;
+import tools.jackson.dataformat.cbor.CBORMapper;
+
+public class JacksonCborConverterFactoryTest {
+ static class IntWrapper {
+ public int value;
+
+ public IntWrapper(int v) {
+ value = v;
+ }
+
+ protected IntWrapper() {}
+ }
+
+ interface Service {
+ @POST("/")
+ Call post(@Body IntWrapper person);
+ }
+
+ @Rule public final MockWebServer server = new MockWebServer();
+
+ private Service service;
+
+ @Before
+ public void setUp() {
+ Retrofit retrofit =
+ new Retrofit.Builder()
+ .baseUrl(server.url("/"))
+ .addConverterFactory(
+ JacksonConverterFactory.create(new CBORMapper(), MediaType.get("application/cbor")))
+ .build();
+ service = retrofit.create(Service.class);
+ }
+
+ @Test
+ public void post() throws IOException, InterruptedException {
+ server.enqueue(
+ new MockResponse()
+ .setBody(new Buffer().write(ByteString.decodeHex("bf6576616c7565182aff"))));
+
+ Call call = service.post(new IntWrapper(12));
+ Response response = call.execute();
+ assertThat(response.body().value).isEqualTo(42);
+
+ RecordedRequest request = server.takeRequest();
+ assertThat(request.getBody().readByteString())
+ .isEqualTo(ByteString.decodeHex("bf6576616c75650cff"));
+ assertThat(request.getHeader("Content-Type")).isEqualTo("application/cbor");
+ }
+}
diff --git a/retrofit-converters/jackson3/src/test/java/retrofit2/converter/jackson3/JacksonConverterFactoryTest.java b/retrofit-converters/jackson3/src/test/java/retrofit2/converter/jackson3/JacksonConverterFactoryTest.java
new file mode 100644
index 0000000000..325b754ed1
--- /dev/null
+++ b/retrofit-converters/jackson3/src/test/java/retrofit2/converter/jackson3/JacksonConverterFactoryTest.java
@@ -0,0 +1,235 @@
+/*
+ * Copyright (C) 2013 Square, Inc.
+ *
+ * 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
+ *
+ * http://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 retrofit2.converter.jackson3;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assume.assumeTrue;
+
+import com.fasterxml.jackson.annotation.JsonAutoDetect;
+import com.google.testing.junit.testparameterinjector.TestParameter;
+import com.google.testing.junit.testparameterinjector.TestParameterInjector;
+import java.io.IOException;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.atomic.AtomicReference;
+import okhttp3.mockwebserver.MockResponse;
+import okhttp3.mockwebserver.MockWebServer;
+import okhttp3.mockwebserver.RecordedRequest;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import retrofit2.Call;
+import retrofit2.Callback;
+import retrofit2.Response;
+import retrofit2.Retrofit;
+import retrofit2.http.Body;
+import retrofit2.http.POST;
+import tools.jackson.core.JacksonException;
+import tools.jackson.core.JsonGenerator;
+import tools.jackson.core.JsonParser;
+import tools.jackson.core.JsonToken;
+import tools.jackson.core.exc.StreamConstraintsException;
+import tools.jackson.databind.DeserializationContext;
+import tools.jackson.databind.SerializationContext;
+import tools.jackson.databind.deser.std.StdDeserializer;
+import tools.jackson.databind.json.JsonMapper;
+import tools.jackson.databind.module.SimpleModule;
+import tools.jackson.databind.ser.std.StdSerializer;
+
+@RunWith(TestParameterInjector.class)
+public final class JacksonConverterFactoryTest {
+ interface AnInterface {
+ String getName();
+ }
+
+ static class AnImplementation implements AnInterface {
+ private String theName;
+
+ AnImplementation() {}
+
+ AnImplementation(String name) {
+ theName = name;
+ }
+
+ @Override
+ public String getName() {
+ return theName;
+ }
+ }
+
+ static class AnInterfaceSerializer extends StdSerializer {
+ AnInterfaceSerializer() {
+ super(AnInterface.class);
+ }
+
+ @Override
+ public void serialize(
+ AnInterface anInterface, JsonGenerator jsonGenerator, SerializationContext ctxt) {
+ jsonGenerator.writeStartObject();
+ jsonGenerator.writeName("name");
+ jsonGenerator.writeString(anInterface.getName());
+ jsonGenerator.writeEndObject();
+ }
+ }
+
+ static class AnInterfaceDeserializer extends StdDeserializer {
+ AnInterfaceDeserializer() {
+ super(AnInterface.class);
+ }
+
+ @Override
+ public AnInterface deserialize(JsonParser jp, DeserializationContext ctxt) {
+ if (jp.currentToken() != JsonToken.START_OBJECT) {
+ throw new AssertionError("Expected start object.");
+ }
+
+ String name = null;
+
+ while (jp.nextToken() != JsonToken.END_OBJECT) {
+ switch (jp.currentName()) {
+ case "name":
+ name = jp.getValueAsString();
+ break;
+ }
+ }
+
+ return new AnImplementation(name);
+ }
+ }
+
+ static final class ErroringValue {
+ final String theName;
+
+ ErroringValue(String theName) {
+ this.theName = theName;
+ }
+ }
+
+ static final class ErroringValueSerializer extends StdSerializer {
+ ErroringValueSerializer() {
+ super(ErroringValue.class);
+ }
+
+ @Override
+ public void serialize(
+ ErroringValue erroringValue, JsonGenerator jsonGenerator, SerializationContext ctxt)
+ throws JacksonException {
+ throw new StreamConstraintsException("oops!");
+ }
+ }
+
+ interface Service {
+ @POST("/")
+ Call anImplementation(@Body AnImplementation impl);
+
+ @POST("/")
+ Call anInterface(@Body AnInterface impl);
+
+ @POST("/")
+ Call erroringValue(@Body ErroringValue value);
+ }
+
+ @Rule public final MockWebServer server = new MockWebServer();
+
+ private final Service service;
+ private final boolean streaming;
+
+ public JacksonConverterFactoryTest(@TestParameter boolean streaming) {
+ this.streaming = streaming;
+
+ SimpleModule module = new SimpleModule();
+ module.addSerializer(AnInterface.class, new AnInterfaceSerializer());
+ module.addSerializer(ErroringValue.class, new ErroringValueSerializer());
+ module.addDeserializer(AnInterface.class, new AnInterfaceDeserializer());
+
+ JsonMapper mapper =
+ JsonMapper.builder()
+ .addModule(module)
+ .changeDefaultVisibility(vc -> vc.withGetterVisibility(JsonAutoDetect.Visibility.NONE))
+ .changeDefaultVisibility(vc -> vc.withSetterVisibility(JsonAutoDetect.Visibility.NONE))
+ .changeDefaultVisibility(
+ vc -> vc.withIsGetterVisibility(JsonAutoDetect.Visibility.NONE))
+ .changeDefaultVisibility(vc -> vc.withFieldVisibility(JsonAutoDetect.Visibility.ANY))
+ .build();
+
+ JacksonConverterFactory factory = JacksonConverterFactory.create(mapper);
+ if (streaming) {
+ factory = factory.withStreaming();
+ }
+
+ Retrofit retrofit =
+ new Retrofit.Builder().baseUrl(server.url("/")).addConverterFactory(factory).build();
+ service = retrofit.create(Service.class);
+ }
+
+ @Test
+ public void anInterface() throws IOException, InterruptedException {
+ server.enqueue(new MockResponse().setBody("{\"name\":\"value\"}"));
+
+ Call call = service.anInterface(new AnImplementation("value"));
+ Response response = call.execute();
+ AnInterface body = response.body();
+ assertThat(body.getName()).isEqualTo("value");
+
+ RecordedRequest request = server.takeRequest();
+ assertThat(request.getBody().readUtf8()).isEqualTo("{\"name\":\"value\"}");
+ assertThat(request.getHeader("Content-Type")).isEqualTo("application/json; charset=UTF-8");
+ }
+
+ @Test
+ public void anImplementation() throws IOException, InterruptedException {
+ server.enqueue(new MockResponse().setBody("{\"theName\":\"value\"}"));
+
+ Call call = service.anImplementation(new AnImplementation("value"));
+ Response response = call.execute();
+ AnImplementation body = response.body();
+ assertThat(body.theName).isEqualTo("value");
+
+ RecordedRequest request = server.takeRequest();
+ // TODO figure out how to get Jackson to stop using AnInterface's serializer here.
+ assertThat(request.getBody().readUtf8()).isEqualTo("{\"name\":\"value\"}");
+ assertThat(request.getHeader("Content-Type")).isEqualTo("application/json; charset=UTF-8");
+ }
+
+ @Test
+ public void serializeIsStreamed() throws InterruptedException {
+ assumeTrue(streaming);
+
+ Call call = service.erroringValue(new ErroringValue("hi"));
+
+ final AtomicReference throwableRef = new AtomicReference<>();
+ final CountDownLatch latch = new CountDownLatch(1);
+
+ // If streaming were broken, the call to enqueue would throw the exception synchronously.
+ call.enqueue(
+ new Callback() {
+ @Override
+ public void onResponse(Call call, Response response) {
+ latch.countDown();
+ }
+
+ @Override
+ public void onFailure(Call call, Throwable t) {
+ throwableRef.set(t.getCause());
+ latch.countDown();
+ }
+ });
+ latch.await();
+
+ Throwable throwable = throwableRef.get();
+ assertThat(throwable).isInstanceOf(JacksonException.class);
+ assertThat(throwable).hasMessageThat().isEqualTo("oops!");
+ }
+}
diff --git a/settings.gradle b/settings.gradle
index e94afa5343..7c0ce27863 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -34,6 +34,7 @@ include ':retrofit-adapters:scala'
include ':retrofit-converters:gson'
include ':retrofit-converters:guava'
include ':retrofit-converters:jackson'
+include ':retrofit-converters:jackson3'
include ':retrofit-converters:java8'
include ':retrofit-converters:jaxb'
include ':retrofit-converters:jaxb3'