Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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**

Expand Down
5 changes: 5 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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" }
Expand Down
1 change: 1 addition & 0 deletions retrofit-converters/jackson/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ dependencies {
testImplementation libs.truth
testImplementation libs.okhttp.mockwebserver
testImplementation libs.testParameterInjector
testImplementation libs.jacksonAnnotations
testImplementation libs.jacksonDataformatCbor
}

Expand Down
34 changes: 34 additions & 0 deletions retrofit-converters/jackson3/README.md
Original file line number Diff line number Diff line change
@@ -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
<dependency>
<groupId>com.squareup.retrofit2</groupId>
<artifactId>converter-jackson3</artifactId>
<version>latest.version</version>
</dependency>
```
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/

27 changes: 27 additions & 0 deletions retrofit-converters/jackson3/build.gradle
Original file line number Diff line number Diff line change
@@ -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)
}
}
3 changes: 3 additions & 0 deletions retrofit-converters/jackson3/gradle.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
POM_ARTIFACT_ID=converter-jackson3
POM_NAME=Converter: Jackson3
POM_DESCRIPTION=A Retrofit Converter which uses Jackson 3 for serialization.
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>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<ResponseBody, ?> 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);
}
}
Original file line number Diff line number Diff line change
@@ -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<T> implements Converter<T, RequestBody> {
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);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In my opinion we can use a Buffer here?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You mean like

ByteBuffer bytes = ByteBuffer.wrap(adapter.writeValueAsBytes(value));
return RequestBody.create(bytes.array(), mediaType);

Why, what's the advantage?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean okio.Buffer which reuses internal byte[]

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, I could do

ByteString bytes = ByteString.of(adapter.writeValueAsBytes(value));
return RequestBody.create(bytes, mediaType);

but I still don't see the benefit, esp. as I believe ByteString.of() copies the data underneath.

If you have a different proposal, please share the exact code.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't have a dev env of this repo so I can only provide following pseudo code.

var buffer = new okio.Buffer();
adapter.writeValue(buffer.outputStream(), value);
return RequestBody.create....

Like this: https://github.com/square/retrofit/blob/8e83a242eac1afc22a13c52098eddb27421353ff/retrofit-converters/gson/src/main/java/retrofit2/converter/gson/GsonRequestBodyConverter.java#L51-L53

But don't call readByteString at the end of it.

Use this (copied from my code):

private fun Buffer.asRequestBody(mediaType: MediaType) = object : RequestBody() {
    override fun contentType() = mediaType
    override fun contentLength(): Long = size
    override fun writeTo(sink: BufferedSink) {
        sink.writeAll(this@asRequestBody.copy()) // <- This is a shallow copy
    }
}

Well, if you want it to be simple you can ignore my comment.

return RequestBody.create(bytes, mediaType);
} catch (JacksonException e) {
throw new IOException(e);
}
}
}
Original file line number Diff line number Diff line change
@@ -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<T> implements Converter<ResponseBody, T> {
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);
}
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
@retrofit2.internal.EverythingIsNonNull
package retrofit2.converter.jackson3;
Loading