Retrofit turns your HTTP API into a type-safe Java or Kotlin interface. You describe each endpoint with an annotated method, and Retrofit generates a working client that builds requests, executes them over OkHttp, and deserializes responses into your own objects. This guide shows you how to use every part of that surface — from your first request to multipart uploads, streaming downloads, RxJava and coroutine integration, and testing.
This guide covers Retrofit 3.0.0 (Maven coordinates com.squareup.retrofit2:retrofit:3.0.0). Written from the library source.
1. Quick Start
Define an interface that describes the API, ask Retrofit to implement it, then call a method.
// 1. Describe the endpoint.
public interface GitHubService {
@GET("users/{user}/repos")
Call<List<Repo>> listRepos(@Path("user") String user);
}
// 2. Build a Retrofit instance and create the implementation.
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://api.github.com/")
.addConverterFactory(GsonConverterFactory.create())
.build();
GitHubService service = retrofit.create(GitHubService.class);
// 3. Make a call.
Call<List<Repo>> repos = service.listRepos("octocat");
List<Repo> result = repos.execute().body();
The Retrofit class generates an implementation of GitHubService at runtime. Each Call returned by the service can make a synchronous or asynchronous HTTP request to the remote server.
Note
Retrofit only deserializes response bodies into OkHttp's
ResponseBodytype out of the box. To map a response onto your own class (such asList<Repo>above), you must add a converter like Gson. See §5 Converting bodies.
Tip
Reuse a single
Retrofitinstance (and the singleOkHttpClientbehind it) across your whole application. They are expensive to create and hold the connection pool and thread pools that make repeated requests fast.
2. Installation
Retrofit requires at minimum Java 8+ or Android API 21+.
Add the dependency
Gradle (Kotlin DSL)
implementation("com.squareup.retrofit2:retrofit:3.0.0")
Gradle (Groovy DSL)
implementation 'com.squareup.retrofit2:retrofit:3.0.0'
Maven
<dependency>
<groupId>com.squareup.retrofit2</groupId>
<artifactId>retrofit</artifactId>
<version>3.0.0</version>
</dependency>
Retrofit depends on OkHttp and pulls it in transitively — you do not need to declare OkHttp yourself unless you want to pin a specific version. As of Retrofit 3.0, OkHttp is the Kotlin-based 4.12 release, so a small transitive Kotlin runtime dependency comes along with it.
Note
Retrofit 3.x is forward binary-compatible with 2.x. Libraries compiled against Retrofit 2.x can still run against 3.x.
Add a converter (almost always needed)
Pick the converter that matches your API's wire format and add it alongside the core dependency. For JSON with Gson:
Gradle (Kotlin DSL)
implementation("com.squareup.retrofit2:converter-gson:3.0.0")
Maven
<dependency>
<groupId>com.squareup.retrofit2</groupId>
<artifactId>converter-gson</artifactId>
<version>3.0.0</version>
</dependency>
The full list of converters and adapters is in §5 and §7.
Keep your versions aligned with the BOM (optional)
If you use several Retrofit modules (core + converters + adapters), import the bill of materials so every module stays on the same version:
implementation(platform("com.squareup.retrofit2:retrofit-bom:3.0.0"))
implementation("com.squareup.retrofit2:retrofit")
implementation("com.squareup.retrofit2:converter-moshi")
implementation("com.squareup.retrofit2:adapter-rxjava3")
With the BOM in place you omit version numbers on the individual modules.
Tip
Want the bleeding edge? In-development snapshots are published to the Central Portal Snapshots repository at
https://central.sonatype.com/repository/maven-snapshots/. Add that as a repository to use a-SNAPSHOTbuild.
3. Defining Your API Interface
An API in Retrofit is a plain Java (or Kotlin) interface. Every method maps to one HTTP request, and annotations on the method and its parameters describe exactly how that request is shaped.
public interface GitHubService {
@GET("/repos/{owner}/{repo}/contributors")
Call<List<Contributor>> contributors(
@Path("owner") String owner,
@Path("repo") String repo);
}
Important
API declarations must be interfaces — Retrofit throws
IllegalArgumentException: API declarations must be interfaces.if you pass a class. Interfaces also may not declare type parameters (generics on the interface itself are rejected).
You can spread methods across multiple interfaces (one per resource, for example) and create each with its own retrofit.create(...) call against the same Retrofit instance.
4. Request Declarations: The Annotation Reference
This is the heart of Retrofit. Every annotation below lives in the retrofit2.http package. They fall into five groups: request method, URL & parameters, headers, request body, and form / multipart.
4.1 Request method annotations
Every service method must carry exactly one HTTP method annotation. There are eight built in:
| Annotation | HTTP verb | Has request body? |
|---|---|---|
@GET |
GET | No |
@POST |
POST | Yes |
@PUT |
PUT | Yes |
@PATCH |
PATCH | Yes |
@DELETE |
DELETE | No (use @HTTP for a body) |
@HEAD |
HEAD | No |
@OPTIONS |
OPTIONS | No |
@HTTP |
custom | Configurable |
Each verb annotation takes the endpoint path as its value:
@GET("users/list")
Call<List<User>> userList();
You can include static query parameters directly in the path:
@GET("users/list?sort=desc")
Call<List<User>> sortedUserList();
Custom verbs and DELETE with a body — @HTTP
@HTTP lets you use a non-standard method, set the path, and declare whether the request carries a body:
public interface Service {
@HTTP(method = "CUSTOM", path = "custom/endpoint/")
Call<ResponseBody> customEndpoint();
// DELETE with a request body (which @DELETE alone cannot express):
@HTTP(method = "DELETE", path = "remove/", hasBody = true)
Call<ResponseBody> deleteObject(@Body RequestBody object);
}
@HTTP element |
Meaning | Default |
|---|---|---|
method |
The HTTP verb to send (required) | — |
path |
Relative/absolute path or full URL | "" |
hasBody |
Whether the request sends a body | false |
4.2 URL manipulation
@Path — fill in URL segments
A replacement block is an alphanumeric string wrapped in { and }. A parameter annotated with @Path using the same name fills it in. Values are converted to strings and URL-encoded.
@GET("group/{id}/users")
Call<List<User>> groupList(@Path("id") int groupId);
Calling groupList(42) requests group/42/users.
By default the value is URL-encoded. Use encoded = true if your argument is already encoded:
@GET("/user/{name}")
Call<ResponseBody> notEncoded(@Path(value = "name", encoded = true) String name);
encoded("John%Doe") yields /user/John%25Doe; notEncoded("John%Doe") yields /user/John%Doe.
Caution
@Pathparameters may not benull. Passingnullthrows an exception when the request is built.
@Query — append query parameters
@GET("group/{id}/users")
Call<List<User>> groupList(@Path("id") int groupId, @Query("sort") String sort);
@Query values are URL-encoded and appended to the URL. null values are ignored (the parameter is omitted). Passing a List or array produces one query parameter per non-null item:
@GET("/friends")
Call<ResponseBody> friends(@Query("group") String... groups);
friends("coworker", "bowling") yields /friends?group=coworker&group=bowling. Use @Query(value = "group", encoded = true) to skip encoding.
@QueryMap — many query parameters at once
@GET("group/{id}/users")
Call<List<User>> groupList(@Path("id") int groupId, @QueryMap Map<String, String> options);
Every map entry becomes a query parameter. With @QueryMap the map itself, its keys, and its values may not be null.
@QueryName — value-less query parameters
For query parameters that are just a name with no =value:
@GET("/friends")
Call<ResponseBody> friends(@QueryName String filter);
friends("contains(Bob)") yields /friends?contains(Bob). Lists and arrays produce one entry each.
@Url — fully dynamic URLs
When the whole URL is computed at runtime, omit the path on the verb annotation and annotate the first parameter with @Url:
@GET
Call<ResponseBody> list(@Url String url);
The @Url value is resolved against the base URL using the same rules as a relative link in a web page (see §6.1).
4.3 Header manipulation
@Headers — static headers on a method
@Headers("Cache-Control: max-age=640000")
@GET("widget/list")
Call<List<Widget>> widgetList();
@Headers({
"Accept: application/vnd.github.v3.full+json",
"User-Agent: Retrofit-Sample-App"
})
@GET("users/{username}")
Call<User> getUser(@Path("username") String username);
@Header — a header from a parameter
@GET("user")
Call<User> getUser(@Header("Authorization") String authorization);
If the value is null, the header is omitted. Otherwise toString() is called on the value. A List or array produces one header per non-null item.
@HeaderMap — many headers from a map
@GET("user")
Call<User> getUser(@HeaderMap Map<String, String> headers);
Warning
Headers do not overwrite each other. Every header with the same name is included in the request. If you need replacement semantics, manage it yourself.
Note
Header parameter keys and values allow only ASCII by default. To send non-ASCII values (not strictly HTTP-compliant but often required), set
allowUnsafeNonAsciiValues = trueon@Header,@Headers, or@HeaderMap:@GET("/") Call<ResponseBody> foo(@Header(value = "Accept-Language", allowUnsafeNonAsciiValues = true) String lang);
Headers that must be added to every request are best set with an OkHttp interceptor rather than repeated on each method (see §6.2).
4.4 The request body — @Body
Send any object as the request body with @Body. It is serialized by the Retrofit instance's converter.
@POST("users/new")
Call<User> createUser(@Body User user);
Important
If no converter is installed, only OkHttp's
RequestBodytype can be used as a@Body.@Bodyparameters may not benull.
4.5 Form-encoded requests
Add @FormUrlEncoded to the method and annotate each pair with @Field. The request is sent with application/x-www-form-urlencoded.
@FormUrlEncoded
@POST("user/edit")
Call<User> updateUser(@Field("first_name") String first, @Field("last_name") String last);
updateUser("Bob Smith", "President") sends the body first_name=Bob+Smith&last_name=President. null field values are ignored; passing a List or array produces a field pair per item.
For a variable set of fields, use @FieldMap:
@FormUrlEncoded
@POST("/things")
Call<ResponseBody> things(@FieldMap Map<String, String> fields);
Both @Field and @FieldMap accept encoded = true if the names/values are already URL-encoded. A null map, key, or value is not allowed for @FieldMap.
4.6 Multipart requests and file upload
Add @Multipart to the method and declare each part with @Part.
@Multipart
@PUT("user/photo")
Call<User> updateUser(
@Part("photo") RequestBody photo,
@Part("description") RequestBody description);
A @Part parameter is processed one of three ways depending on its type:
| Parameter type | Behavior | Name in annotation |
|---|---|---|
okhttp3.MultipartBody.Part |
Used directly | Omit the name: @Part MultipartBody.Part part |
okhttp3.RequestBody |
Used directly with its content type | Supply the name: @Part("foo") RequestBody foo |
| Any other type | Serialized by a converter | Supply the name: @Part("foo") Image photo |
@Multipart
@POST("/")
Call<ResponseBody> example(
@Part("description") String description,
@Part(value = "image", encoding = "8-bit") RequestBody image);
The encoding element controls the part's Content-Transfer-Encoding (default "binary"). For a dynamic set of parts use @PartMap Map<String, RequestBody>. @Part values may be null (the part is omitted); a @PartMap map, key, or value may not be null.
4.7 Other parameter annotations
@Streaming — stream large response bodies
By default Retrofit buffers the entire response body into memory before handing it to you. For large downloads, add @Streaming so the ResponseBody is delivered as-is and you read it incrementally.
@Streaming
@GET("download/{id}")
Call<ResponseBody> download(@Path("id") String id);
Caution
With
@Streamingyou are responsible for closing theResponseBody(e.g. with try-with-resources). The body must be consumed and closed on the calling thread.
@Tag — attach metadata for interceptors
@Tag attaches an object to the underlying OkHttp request using its type as the key. Interceptors can read it back to make per-request decisions (logging, metrics, auth scoping).
@GET("/")
Call<ResponseBody> foo(@Tag String tag);
null tags are omitted. A parameterized type uses its raw type as the key (List<String> → List.class); primitives are boxed (long → Long.class). Duplicate tag types on one method are not allowed.
4.8 Annotation cheat sheet
| Annotation | Target | Purpose |
|---|---|---|
@GET @POST @PUT @PATCH @DELETE @HEAD @OPTIONS |
method | Standard HTTP verbs |
@HTTP |
method | Custom verb / DELETE with body |
@Path |
parameter | Fill a {block} in the URL |
@Query |
parameter | Append ?name=value |
@QueryMap |
parameter | Append many query parameters |
@QueryName |
parameter | Append a value-less query parameter |
@Url |
parameter | Provide the whole URL at runtime |
@Header |
parameter | Set one header from an argument |
@HeaderMap |
parameter | Set many headers from a map |
@Headers |
method | Static headers |
@Body |
parameter | Object → request body via converter |
@FormUrlEncoded |
method | Send application/x-www-form-urlencoded |
@Field / @FieldMap |
parameter | Form fields |
@Multipart |
method | Send a multipart body |
@Part / @PartMap |
parameter | Multipart parts |
@Streaming |
method | Don't buffer the response body |
@Tag |
parameter | Attach a typed tag for interceptors |
5. Converting Bodies (JSON, XML, and more)
By default Retrofit can only read OkHttp ResponseBody and write RequestBody. Converters teach it to map your own types onto JSON, XML, protocol buffers, and so on. Add them with addConverterFactory(...):
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://api.github.com/")
.addConverterFactory(GsonConverterFactory.create())
.build();
5.1 Built-in converters
| Library | Maven artifact | Notes |
|---|---|---|
| Gson | converter-gson |
JSON |
| Jackson | converter-jackson |
JSON, XML, CBOR, YAML, etc. by supplying a different mapper + media type |
| Moshi | converter-moshi |
JSON |
| Protobuf | converter-protobuf |
Protocol buffers |
| Wire | converter-wire |
Protocol buffers (Wire runtime) |
| Simple XML | converter-simplexml |
XML |
| JAXB | converter-jaxb |
XML |
| JAXB 3 | converter-jaxb3 |
XML (Jakarta jakarta.xml.bind) |
| Kotlin serialization | converter-kotlinx-serialization |
Multiformat |
| Scalars | converter-scalars |
String + primitives and their boxed types |
Tip
The scalars converter (
converter-scalars) handles plainString,int,boolean, and other primitives as request/response bodies. Add it when an endpoint returns or accepts a bare value rather than a JSON document.
5.2 Delegating (Optional) converters
These do not parse bytes themselves — they delegate to another converter and wrap a potentially-null result in an Optional:
| Wrapper type | Maven artifact |
|---|---|
Guava Optional<T> |
converter-guava |
Java 8 Optional<T> |
converter-java8 |
Install the delegating converter before the real one so it wraps that converter's output.
5.3 Mixing converters
You can add more than one converter. Retrofit tries each in the order added until one handles the type. A common pattern is JSON for most endpoints plus the scalars converter for raw text:
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(API_URL)
.addConverterFactory(ScalarsConverterFactory.create())
.addConverterFactory(GsonConverterFactory.create())
.build();
5.4 Custom converters
To support a format Retrofit doesn't ship (YAML, a bespoke text format, or a different library for an existing format), extend Converter.Factory and install an instance. The factory implements any of three methods — responseBodyConverter, requestBodyConverter, stringConverter — returning null for types it doesn't handle. See the Admin Guide for the full SPI contract.
6. Configuring Retrofit
The Retrofit.Builder is where you assemble an instance. baseUrl(...) is the only required call.
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://api.example.com/") // required
.client(myOkHttpClient) // optional: custom HTTP client
.addConverterFactory(MoshiConverterFactory.create())
.addCallAdapterFactory(RxJava3CallAdapterFactory.create())
.validateEagerly(BuildConfig.DEBUG) // optional: fail fast on bad declarations
.build();
| Builder method | Purpose |
|---|---|
baseUrl(String / HttpUrl / URL) |
The API base URL (required) |
client(OkHttpClient) |
Use a configured OkHttp client (convenience for callFactory) |
callFactory(okhttp3.Call.Factory) |
Supply a custom call factory; client(...) sets this automatically |
addConverterFactory(Converter.Factory) |
Add a body/string converter |
addCallAdapterFactory(CallAdapter.Factory) |
Support return types other than Call |
callbackExecutor(Executor) |
Thread on which Callback methods run (for Call returns) |
validateEagerly(boolean) |
Validate every method at create(...) time instead of on first call |
build() |
Produce the Retrofit instance |
Tip
Already have a
Retrofitand want a variation? Callretrofit.newBuilder()to get a builder pre-populated with the existing configuration, change what you need, andbuild()a new instance that shares the same connection pool.
6.1 Base URL rules
The endpoint values on your annotations are resolved against the base URL exactly the way an <a href=""> link resolves on a web page.
Important
Base URLs should always end in
/. Retrofit enforces this —baseUrl(...)throwsIllegalArgumentException: baseUrl must end in /otherwise. A trailing/ensures relative endpoint paths append correctly.
| Base URL | Endpoint | Result |
|---|---|---|
http://example.com/api/ |
foo/bar/ |
http://example.com/api/foo/bar/ |
http://example.com/api/ (no /: …/api) |
foo/bar/ |
http://example.com/foo/bar/ ⚠️ |
http://example.com/api/ |
/foo/bar/ (leading /) |
http://example.com/foo/bar/ (absolute) |
http://example.com/ |
https://github.com/square/retrofit/ |
https://github.com/square/retrofit/ (full URL wins) |
In short: a leading / on the endpoint makes it absolute (host kept, path replaced); a full URL on the endpoint replaces the host (and scheme, if present).
6.2 Customizing the HTTP client
Retrofit delegates all networking to OkHttp. To add logging, authentication, timeouts, caching, or any header that belongs on every request, configure an OkHttpClient and pass it with .client(...):
OkHttpClient client = new OkHttpClient.Builder()
.addInterceptor(new HttpLoggingInterceptor())
.addInterceptor(chain -> {
Request request = chain.request().newBuilder()
.header("Authorization", "Bearer " + token)
.build();
return chain.proceed(request);
})
.build();
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(API_URL)
.client(client)
.addConverterFactory(GsonConverterFactory.create())
.build();
Note
If you don't call
client(...)orcallFactory(...), Retrofit creates a defaultOkHttpClientfor you. For production apps you almost always want to supply your own so you control timeouts, interceptors, and the connection pool.
7. Changing the Return Type (Call Adapters)
By default a service method returns Call<T>. Call adapters let it return something else — an RxJava Observable, a CompletableFuture, a Guava ListenableFuture, or (with no extra dependency) the value of a Kotlin suspend function. Add them with addCallAdapterFactory(...).
7.1 Built-in call adapters
| Return types | Maven artifact |
|---|---|
RxJava Observable, Single |
adapter-rxjava |
RxJava 2 Observable, Flowable, Single, Completable, Maybe |
adapter-rxjava2 |
RxJava 3 Observable, Flowable, Single, Completable, Maybe |
adapter-rxjava3 |
Guava ListenableFuture |
adapter-guava |
Java 8 CompletableFuture |
adapter-java8 |
Kotlin suspend functions |
none needed — built in |
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(API_URL)
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJava3CallAdapterFactory.create())
.build();
public interface Service {
@GET("users/{user}")
Single<User> getUser(@Path("user") String user);
}
Note
On the JVM, the Java 8
CompletableFutureadapter is available without any extra dependency on Java 8+ runtimes. Theadapter-java8artifact exists for completeness;suspendandCompletableFuturesupport are built into the platform defaults.
7.2 Custom call adapters
To integrate a concurrency or task library Retrofit doesn't ship, extend CallAdapter.Factory and return an adapter that wraps the built-in Call. The Admin Guide documents the SPI in full.
8. Making Calls: Synchronous, Asynchronous, and Cancellation
A Call<T> represents a single request/response pair. Each Call can be used only once. To repeat a request (polling, retries), call clone() to get a fresh one.
8.1 Synchronous
Call<List<Contributor>> call = service.contributors("square", "retrofit");
Response<List<Contributor>> response = call.execute(); // blocks
if (response.isSuccessful()) {
List<Contributor> body = response.body();
}
Warning
execute()blocks the calling thread and throwsIOExceptionon a network problem. Never call it on Android's main thread — do it on a background thread or use the asynchronous API.
8.2 Asynchronous
call.enqueue(new Callback<List<Contributor>>() {
@Override public void onResponse(Call<List<Contributor>> call, Response<List<Contributor>> response) {
if (response.isSuccessful()) {
// response.body()
} else {
// response.errorBody() — server returned 4xx/5xx
}
}
@Override public void onFailure(Call<List<Contributor>> call, Throwable t) {
// network failure, or an error building the request / decoding the response
}
});
Important
onResponsefires for any HTTP response, including 404 and 500. Always checkresponse.isSuccessful()(true when the status is in[200, 300)) before readingbody().onFailureonly fires for network errors or request/response processing errors — not for non-2xx statuses.
On Android, callbacks run on the main thread by default. On the plain JVM, callbacks run on the same background thread that performed the request.
8.3 Cancellation, state, and the request
call.cancel(); // attempt to cancel an in-flight or not-yet-started call
boolean canceled = call.isCanceled();
boolean executed = call.isExecuted(); // already executed or enqueued?
Request original = call.request(); // the OkHttp request this call will send
Call<T> retry = call.clone(); // a fresh, re-usable copy
8.4 Working with Response<T>
| Method | Returns |
|---|---|
isSuccessful() |
true if the status code is in [200, 300) |
code() |
HTTP status code |
message() |
HTTP status message |
headers() |
Response headers |
body() |
Deserialized body of a successful response (may be null) |
errorBody() |
Raw ResponseBody of an unsuccessful response |
raw() |
The underlying okhttp3.Response |
Tip
When the server returns an error status, the parsed
body()isnulland the unparsed error payload is inerrorBody(). To map that error JSON onto your own error type, pull a converter from theRetrofitinstance withretrofit.responseBodyConverter(...)and apply it toerrorBody().
9. Kotlin: Coroutines and suspend Functions
Retrofit understands Kotlin suspend functions natively — no adapter dependency required. A suspend method creates the call, executes it asynchronously, and suspends the coroutine until it completes.
Return a Response<T> to inspect status and headers yourself:
@GET("users")
suspend fun getUser(): Response<User>
Or return the body type directly. On a non-2xx response, an HttpException is thrown:
@GET("users")
suspend fun getUser(): User
val service = retrofit.create(GitHubService::class.java)
try {
val user: User = service.getUser()
// use user
} catch (e: HttpException) {
// e.code(), e.message(), e.response()
} catch (e: IOException) {
// network failure
}
Important
A
suspendfunction must not declare a return type ofCall<T>— suspend functions already execute asynchronously, and Retrofit rejects this with a clear error telling you to return the body type directly.
Note
Use
Unitas the body type (suspend fun ping(): Unit) when you don't care about the response payload — the body is discarded without deserialization. A@HEADmethod must useVoid(Java) orUnit(Kotlin) as its response type.
10. Testing Your API with retrofit-mock
The retrofit-mock module lets you stand in for the network so you can unit-test code that depends on your API interface, with realistic delays and a configurable failure rate.
implementation("com.squareup.retrofit2:retrofit-mock:3.0.0")
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://api.github.com/")
.addConverterFactory(GsonConverterFactory.create())
.build();
// Wrap it in a MockRetrofit with simulated network behavior.
NetworkBehavior behavior = NetworkBehavior.create();
MockRetrofit mockRetrofit = new MockRetrofit.Builder(retrofit)
.networkBehavior(behavior)
.build();
BehaviorDelegate<GitHubService> delegate = mockRetrofit.create(GitHubService.class);
// Provide a hand-written mock implementation that returns canned responses.
GitHubService mockService = new MockGitHubService(delegate);
Inside your mock implementation, hand canned data back through the delegate:
public Call<List<Repo>> listRepos(String user) {
List<Repo> repos = Arrays.asList(new Repo("octocat", "Hello-World"));
return delegate.returningResponse(repos).listRepos(user);
}
NetworkBehavior lets you tune simulated latency, variance, and the probability of failures so tests exercise your error-handling paths. Calls (in the same module) provides helpers like Calls.response(...) and Calls.failure(...) to build standalone Call instances for tests.
Tip
For integration-style tests that exercise real serialization and the HTTP stack, point Retrofit at OkHttp's
MockWebServerinstead.retrofit-mockis for unit tests where you want to bypass the network entirely.
11. ProGuard / R8
If you use R8 (the default Android shrinker), Retrofit's shrinking and obfuscation rules are included automatically — there is nothing to do.
If you use ProGuard, add the rules from retrofit2.pro manually. You will likely also need rules for OkHttp and Okio, which are dependencies of Retrofit.
Caution
Aggressive shrinking can strip the generic type information Retrofit reads at runtime, causing failures like "Unable to create converter" or "Method return type must not include a type variable or wildcard" in release builds only. If static keep rules aren't enough for your interfaces, the
retrofit-response-type-keeperannotation processor generates the extra rules for every type referenced by your service interfaces.
12. Troubleshooting
Error: API declarations must be interfaces.
Cause: You passed a class (or abstract class) to retrofit.create(...).
Solution: Make your API declaration a Java/Kotlin interface.
Prevention: Retrofit only generates implementations for interfaces.
Error: baseUrl must end in /: <url>
Cause: The base URL you supplied has no trailing slash.
Solution: Add a trailing / — e.g. https://api.example.com/, not https://api.example.com.
Prevention: Always end base URLs in /; see §6.1.
Error: Could not locate ResponseBody converter for <type>
Cause: No installed converter can deserialize the method's return type — most often you forgot to add a converter, or added it after a converter that consumes all types.
Solution: Add the matching addConverterFactory(...) (e.g. GsonConverterFactory.create()). If using an Optional converter, install it before the real one.
Prevention: Add a converter whenever a method returns anything other than ResponseBody.
Error: Could not locate call adapter for <type>
Cause: The method returns a type (such as Single or Observable) with no matching call adapter installed.
Solution: Add the adapter factory — e.g. addCallAdapterFactory(RxJava3CallAdapterFactory.create()).
Prevention: Each non-Call return type needs its adapter registered on the builder.
Error: IllegalStateException: Base URL required.
Cause: build() was called without baseUrl(...).
Solution: Call .baseUrl(...) before .build(). It is the only required builder method.
Error: Suspend functions should not return Call
Cause: A Kotlin suspend method declared a Call<T> return type.
Solution: Change the return type to the body type (e.g. User) or Response<User>.
Prevention: suspend functions execute asynchronously already; never wrap them in Call.
Error: 'okhttp3.Response' is not a valid response body type. Did you mean ResponseBody?
Cause: A method used okhttp3.Response (the raw HTTP response) as its body type.
Solution: Return Call<ResponseBody> for the raw body, or Call<okhttp3.Response> is not allowed — use Retrofit's Response<T> wrapper or ResponseBody.
Error: HEAD method must use Void or Unit as response type.
Cause: A @HEAD method declared a body type other than Void/Unit.
Solution: Use Call<Void> (Java) or suspend fun … : Unit (Kotlin) — HEAD responses have no body.
Calls succeed but body() is null
Cause: The response was not successful (status outside [200, 300)), so the parsed body is null and the payload is in errorBody().
Solution: Check response.isSuccessful() first; read response.errorBody() for error responses.
@Path/@Body/@Part throws on null
Cause: @Path, @Body, and named @Part/@Field-map values may not be null.
Solution: Use @Query/@Header (which silently omit null), or guarantee a non-null argument.
Works in debug, fails in release (Android)
Cause: R8/ProGuard stripped generic type information or kept rules your interfaces need.
Solution: Ensure R8 is used (rules are automatic) or add retrofit2.pro for ProGuard; consider the retrofit-response-type-keeper processor. See §11.
@XmlJavaTypeAdapter ignored with the JAXB converter
Cause: A known open issue (#3349): JaxbConverterFactory does not recognize @XmlJavaTypeAdapter.
Solution: Apply the type adapter at the field/package level your JAXB implementation honors, or pre/post-process the affected types until the issue is resolved.
13. FAQ
Q: Do I need to add OkHttp as a dependency? No. Retrofit pulls OkHttp in transitively. Declare it explicitly only to pin a particular OkHttp version.
Q: Should I create a new Retrofit instance per request?
No. Create one Retrofit (and one OkHttpClient) and reuse them everywhere. They hold the connection pool and thread pools.
Q: How do I add an Authorization header to every request?
Use an OkHttp interceptor on the OkHttpClient you pass to .client(...), not a per-method @Header. See §6.2.
Q: Can one method's URL be completely dynamic?
Yes — omit the path on the verb annotation and annotate the first parameter with @Url. See §4.2.
Q: How do I send a DELETE with a body?
@DELETE has no body. Use @HTTP(method = "DELETE", path = "...", hasBody = true) with a @Body parameter.
Q: How do I read the error body on a 4xx/5xx response?
For Call, check response.isSuccessful() and read response.errorBody(). For suspend functions returning the body directly, catch HttpException and call e.response().
Q: What's the difference between @Query and @Field?
@Query appends to the URL (?name=value). @Field builds a form-encoded request body and requires @FormUrlEncoded on the method.
Q: How do I upload a file?
Use a @Multipart method with a @Part MultipartBody.Part parameter (build the part from an OkHttp RequestBody). See §4.6.
Q: Do suspend functions need the coroutines adapter?
No. Coroutine support is built into Retrofit core — just declare a suspend fun.
Q: My non-ASCII header value is rejected. What now?
Set allowUnsafeNonAsciiValues = true on @Header/@Headers/@HeaderMap. Note this isn't strictly HTTP-compliant.
Q: How do I test code that uses my API interface without hitting the network?
Use retrofit-mock (MockRetrofit + BehaviorDelegate + NetworkBehavior) for unit tests, or OkHttp's MockWebServer for integration tests. See §10.
Q: Is Retrofit 3 a breaking change from 2? No. Retrofit 3.x maintains forward binary compatibility with 2.x. The main change in 3.0 is the upgrade to the Kotlin-based OkHttp 4.12.
Q: How do I retry or poll a request?
A Call is single-use. Call call.clone() to get a fresh, executable copy for each attempt.
Q: Can I use multiple converters at once? Yes. Add them in priority order; Retrofit tries each until one handles the type. A common combo is scalars + a JSON converter.