Skip to content

Commit 6d5395d

Browse files
committed
Add a converter for Jackson 3
This mostly copies the existing Jackson (2) converter and does the necessary migration [1]. Note that Jackson 3 requires Java 17. [1]: https://github.com/FasterXML/jackson/blob/main/jackson3/MIGRATING_TO_JACKSON_3.md
1 parent 137e892 commit 6d5395d

File tree

12 files changed

+612
-0
lines changed

12 files changed

+612
-0
lines changed

gradle/libs.versions.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ autoService = "1.1.1"
2222
incap = "1.0.0"
2323
jackson = "2.20.1"
2424
jacksonAnnotations = "2.20"
25+
jackson3 = "3.0.1"
2526

2627
[libraries]
2728
androidPlugin = { module = "com.android.tools.build:gradle", version = "8.13.0" }
@@ -73,6 +74,8 @@ gson = { module = "com.google.code.gson:gson", version = "2.13.2" }
7374
jacksonAnnotations = { module = "com.fasterxml.jackson.core:jackson-annotations", version.ref = "jacksonAnnotations" }
7475
jacksonDatabind = { module = "com.fasterxml.jackson.core:jackson-databind", version.ref = "jackson" }
7576
jacksonDataformatCbor = { module = "com.fasterxml.jackson.dataformat:jackson-dataformat-cbor", version.ref = "jackson" }
77+
jackson3Databind = { module = "tools.jackson.core:jackson-databind", version.ref = "jackson3" }
78+
jackson3DataformatCbor = { module = "tools.jackson.dataformat:jackson-dataformat-cbor", version.ref = "jackson3" }
7679
jaxbApi = { module = "javax.xml.bind:jaxb-api", version = "2.3.1" }
7780
jaxbImpl = { module = "org.glassfish.jaxb:jaxb-runtime", version = "4.0.6" }
7881
jaxb3Api = { module = "jakarta.xml.bind:jakarta.xml.bind-api", version = "3.0.1" }
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
Jackson Converter
2+
=================
3+
4+
A `Converter` which uses [Jackson][1] 3 for serialization to and from JSON.
5+
6+
A default `ObjectMapper` instance will be created or one can be configured and passed to the
7+
`JacksonConverterFactory` construction to further control the serialization.
8+
9+
10+
Download
11+
--------
12+
13+
Download [the latest JAR][2] or grab via [Maven][3]:
14+
```xml
15+
<dependency>
16+
<groupId>com.squareup.retrofit2</groupId>
17+
<artifactId>converter-jackson3</artifactId>
18+
<version>latest.version</version>
19+
</dependency>
20+
```
21+
or [Gradle][3]:
22+
```groovy
23+
implementation 'com.squareup.retrofit2:converter-jackson3:latest.version'
24+
```
25+
26+
Snapshots of the development version are available in [Sonatype's `snapshots` repository][snap].
27+
28+
29+
30+
[1]: https://github.com/FasterXML/jackson
31+
[2]: https://search.maven.org/remote_content?g=com.squareup.retrofit2&a=converter-jackson3&v=LATEST
32+
[3]: http://search.maven.org/#search%7Cga%7C1%7Cg%3A%22com.squareup.retrofit2%22%20a%3A%22converter-jackson3%22
33+
[snap]: https://s01.oss.sonatype.org/content/repositories/snapshots/
34+
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
apply plugin: 'java-library'
2+
apply plugin: 'com.vanniktech.maven.publish'
3+
4+
dependencies {
5+
api projects.retrofit
6+
api libs.jackson3Databind
7+
compileOnly libs.findBugsAnnotations
8+
9+
testImplementation libs.junit
10+
testImplementation libs.truth
11+
testImplementation libs.okhttp.mockwebserver
12+
testImplementation libs.testParameterInjector
13+
testImplementation libs.jacksonAnnotations
14+
testImplementation libs.jackson3DataformatCbor
15+
}
16+
17+
jar {
18+
manifest {
19+
attributes 'Automatic-Module-Name': 'retrofit2.converter.jackson3'
20+
}
21+
}
22+
23+
java {
24+
toolchain {
25+
languageVersion = JavaLanguageVersion.of(17)
26+
}
27+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
POM_ARTIFACT_ID=converter-jackson3
2+
POM_NAME=Converter: Jackson3
3+
POM_DESCRIPTION=A Retrofit Converter which uses Jackson 3 for serialization.
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
/*
2+
* Copyright (C) 2015 Square, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package retrofit2.converter.jackson3;
17+
18+
import tools.jackson.databind.JavaType;
19+
import tools.jackson.databind.ObjectMapper;
20+
import tools.jackson.databind.ObjectReader;
21+
import tools.jackson.databind.ObjectWriter;
22+
import java.lang.annotation.Annotation;
23+
import java.lang.reflect.Type;
24+
import okhttp3.MediaType;
25+
import okhttp3.RequestBody;
26+
import okhttp3.ResponseBody;
27+
import retrofit2.Call;
28+
import retrofit2.Converter;
29+
import retrofit2.Retrofit;
30+
31+
/**
32+
* A {@linkplain Converter.Factory converter} which uses Jackson.
33+
*
34+
* <p>Because Jackson is so flexible in the types it supports, this converter assumes that it can
35+
* handle all types. If you are mixing JSON serialization with something else (such as protocol
36+
* buffers), you must {@linkplain Retrofit.Builder#addConverterFactory(Converter.Factory) add this
37+
* instance} last to allow the other converters a chance to see their types.
38+
*/
39+
public final class JacksonConverterFactory extends Converter.Factory {
40+
private static final MediaType DEFAULT_MEDIA_TYPE =
41+
MediaType.get("application/json; charset=UTF-8");
42+
43+
/** Create an instance using a default {@link ObjectMapper} instance for conversion. */
44+
public static JacksonConverterFactory create() {
45+
return new JacksonConverterFactory(new ObjectMapper(), DEFAULT_MEDIA_TYPE, false);
46+
}
47+
48+
/** Create an instance using {@code mapper} for conversion. */
49+
public static JacksonConverterFactory create(ObjectMapper mapper) {
50+
return create(mapper, DEFAULT_MEDIA_TYPE);
51+
}
52+
53+
/** Create an instance using {@code mapper} and {@code mediaType} for conversion. */
54+
@SuppressWarnings("ConstantConditions") // Guarding public API nullability.
55+
public static JacksonConverterFactory create(ObjectMapper mapper, MediaType mediaType) {
56+
if (mapper == null) throw new NullPointerException("mapper == null");
57+
if (mediaType == null) throw new NullPointerException("mediaType == null");
58+
return new JacksonConverterFactory(mapper, mediaType, false);
59+
}
60+
61+
private final ObjectMapper mapper;
62+
private final MediaType mediaType;
63+
private final boolean streaming;
64+
65+
private JacksonConverterFactory(ObjectMapper mapper, MediaType mediaType, boolean streaming) {
66+
this.mapper = mapper;
67+
this.mediaType = mediaType;
68+
this.streaming = streaming;
69+
}
70+
71+
/**
72+
* Return a new factory which streams serialization of request messages to bytes on the HTTP thread
73+
* This is either the calling thread for {@link Call#execute()}, or one of OkHttp's background
74+
* threads for {@link Call#enqueue}. Response bytes are always converted to message instances on
75+
* one of OkHttp's background threads.
76+
*/
77+
public JacksonConverterFactory withStreaming() {
78+
return new JacksonConverterFactory(mapper, mediaType, true);
79+
}
80+
81+
@Override
82+
public Converter<ResponseBody, ?> responseBodyConverter(
83+
Type type, Annotation[] annotations, Retrofit retrofit) {
84+
JavaType javaType = mapper.getTypeFactory().constructType(type);
85+
ObjectReader reader = mapper.readerFor(javaType);
86+
return new JacksonResponseBodyConverter<>(reader);
87+
}
88+
89+
@Override
90+
public Converter<?, RequestBody> requestBodyConverter(
91+
Type type,
92+
Annotation[] parameterAnnotations,
93+
Annotation[] methodAnnotations,
94+
Retrofit retrofit) {
95+
JavaType javaType = mapper.getTypeFactory().constructType(type);
96+
ObjectWriter writer = mapper.writerFor(javaType);
97+
return new JacksonRequestBodyConverter<>(writer, mediaType, streaming);
98+
}
99+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*
2+
* Copyright (C) 2015 Square, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package retrofit2.converter.jackson3;
17+
18+
import tools.jackson.databind.ObjectWriter;
19+
import java.io.IOException;
20+
import okhttp3.MediaType;
21+
import okhttp3.RequestBody;
22+
import retrofit2.Converter;
23+
24+
final class JacksonRequestBodyConverter<T> implements Converter<T, RequestBody> {
25+
private final ObjectWriter adapter;
26+
private final MediaType mediaType;
27+
private final boolean streaming;
28+
29+
JacksonRequestBodyConverter(ObjectWriter adapter, MediaType mediaType, boolean streaming) {
30+
this.adapter = adapter;
31+
this.mediaType = mediaType;
32+
this.streaming = streaming;
33+
}
34+
35+
@Override
36+
public RequestBody convert(T value) throws IOException {
37+
if (streaming) {
38+
return new JacksonStreamingRequestBody(adapter, value, mediaType);
39+
}
40+
41+
byte[] bytes = adapter.writeValueAsBytes(value);
42+
return RequestBody.create(mediaType, bytes);
43+
}
44+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*
2+
* Copyright (C) 2015 Square, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package retrofit2.converter.jackson3;
17+
18+
import tools.jackson.databind.ObjectReader;
19+
import java.io.IOException;
20+
import okhttp3.ResponseBody;
21+
import retrofit2.Converter;
22+
23+
final class JacksonResponseBodyConverter<T> implements Converter<ResponseBody, T> {
24+
private final ObjectReader adapter;
25+
26+
JacksonResponseBodyConverter(ObjectReader adapter) {
27+
this.adapter = adapter;
28+
}
29+
30+
@Override
31+
public T convert(ResponseBody value) throws IOException {
32+
try {
33+
return adapter.readValue(value.byteStream());
34+
} finally {
35+
value.close();
36+
}
37+
}
38+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*
2+
* Copyright (C) 2025 Square, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package retrofit2.converter.jackson3;
17+
18+
import tools.jackson.databind.ObjectWriter;
19+
import java.io.IOException;
20+
import okhttp3.MediaType;
21+
import okhttp3.RequestBody;
22+
import okio.BufferedSink;
23+
24+
final class JacksonStreamingRequestBody extends RequestBody {
25+
private final ObjectWriter adapter;
26+
private final Object value;
27+
private final MediaType mediaType;
28+
29+
public JacksonStreamingRequestBody(ObjectWriter adapter, Object value, MediaType mediaType) {
30+
this.adapter = adapter;
31+
this.value = value;
32+
this.mediaType = mediaType;
33+
}
34+
35+
@Override
36+
public MediaType contentType() {
37+
return mediaType;
38+
}
39+
40+
@Override
41+
public void writeTo(BufferedSink sink) throws IOException {
42+
adapter.writeValue(sink.outputStream(), value);
43+
}
44+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
@retrofit2.internal.EverythingIsNonNull
2+
package retrofit2.converter.jackson3;
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/*
2+
* Copyright (C) 2024 Square, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package retrofit2.converter.jackson3;
17+
18+
import static com.google.common.truth.Truth.assertThat;
19+
20+
import tools.jackson.dataformat.cbor.CBORMapper;
21+
import java.io.IOException;
22+
import okhttp3.MediaType;
23+
import okhttp3.mockwebserver.MockResponse;
24+
import okhttp3.mockwebserver.MockWebServer;
25+
import okhttp3.mockwebserver.RecordedRequest;
26+
import okio.Buffer;
27+
import okio.ByteString;
28+
import org.junit.Before;
29+
import org.junit.Rule;
30+
import org.junit.Test;
31+
import retrofit2.Call;
32+
import retrofit2.Response;
33+
import retrofit2.Retrofit;
34+
import retrofit2.http.Body;
35+
import retrofit2.http.POST;
36+
37+
public class JacksonCborConverterFactoryTest {
38+
static class IntWrapper {
39+
public int value;
40+
41+
public IntWrapper(int v) {
42+
value = v;
43+
}
44+
45+
protected IntWrapper() {}
46+
}
47+
48+
interface Service {
49+
@POST("/")
50+
Call<IntWrapper> post(@Body IntWrapper person);
51+
}
52+
53+
@Rule public final MockWebServer server = new MockWebServer();
54+
55+
private Service service;
56+
57+
@Before
58+
public void setUp() {
59+
Retrofit retrofit =
60+
new Retrofit.Builder()
61+
.baseUrl(server.url("/"))
62+
.addConverterFactory(
63+
JacksonConverterFactory.create(new CBORMapper(), MediaType.get("application/cbor")))
64+
.build();
65+
service = retrofit.create(Service.class);
66+
}
67+
68+
@Test
69+
public void post() throws IOException, InterruptedException {
70+
server.enqueue(
71+
new MockResponse()
72+
.setBody(new Buffer().write(ByteString.decodeHex("bf6576616c7565182aff"))));
73+
74+
Call<IntWrapper> call = service.post(new IntWrapper(12));
75+
Response<IntWrapper> response = call.execute();
76+
assertThat(response.body().value).isEqualTo(42);
77+
78+
RecordedRequest request = server.takeRequest();
79+
assertThat(request.getBody().readByteString())
80+
.isEqualTo(ByteString.decodeHex("bf6576616c75650cff"));
81+
assertThat(request.getHeader("Content-Type")).isEqualTo("application/cbor");
82+
}
83+
}

0 commit comments

Comments
 (0)