lysine-dev/retrofit · error · IllegalStateException

Future return type must be parameterized as Future<Foo> or F

Error message

Future return type must be parameterized as Future<Foo> or Future<? extends Foo>

What it means

Thrown by ScalaCallAdapterFactory.get() when a Retrofit service method's return type is a raw scala.concurrent.Future with no type argument. The factory first checks getRawType(returnType) == Future.class, then requires returnType to be a ParameterizedType so it can extract the inner body or Response type. A raw Future leaves the adapter unable to determine what to deserialize.

Source

Thrown at retrofit-adapters/scala/src/main/java/retrofit2/adapter/scala/ScalaCallAdapterFactory.java:65

 *   <li>Response wrapped body (e.g., {@code Future<Response<User>>}) returns a {@link Response}
 *       object for all HTTP responses and sets {@link IOException} for network errors
 * </ul>
 */
public final class ScalaCallAdapterFactory extends CallAdapter.Factory {
  public static ScalaCallAdapterFactory create() {
    return new ScalaCallAdapterFactory();
  }

  private ScalaCallAdapterFactory() {}

  @Override
  public @Nullable CallAdapter<?, ?> get(
      Type returnType, Annotation[] annotations, Retrofit retrofit) {
    if (getRawType(returnType) != Future.class) {
      return null;
    }
    if (!(returnType instanceof ParameterizedType)) {
      throw new IllegalStateException(
          "Future return type must be parameterized as Future<Foo> or Future<? extends Foo>");
    }
    Type innerType = getParameterUpperBound(0, (ParameterizedType) returnType);

    if (getRawType(innerType) != Response.class) {
      // Generic type is not Response<T>. Use it for body-only adapter.
      return new BodyCallAdapter<>(innerType);
    }

    if (!(innerType instanceof ParameterizedType)) {
      throw new IllegalStateException(
          "Response must be parameterized as Response<Foo> or Response<? extends Foo>");
    }

    Type responseType = getParameterUpperBound(0, (ParameterizedType) innerType);
    return new ResponseCallAdapter<>(responseType);
  }
}

View on GitHub (pinned to d0b112dad0)

Solutions

  1. Parameterize the Future: change Future getUser(...) to Future<User> getUser(...).
  2. If you want the full retrofit2.Response, use Future<Response<User>>.
  3. Verify you imported scala.concurrent.Future and not java.util.concurrent.Future, which belongs to a different adapter.
  4. Recompile so Retrofit sees the corrected signature when it eagerly validates the service interface.

Example fix

// before
@GET("users/{id}")
Future getUser(@Path("id") String id);

// after
@GET("users/{id}")
Future<User> getUser(@Path("id") String id);
Defensive patterns

Strategy: validation

Validate before calling

// No per-call runtime pre-check exists; this is a signature discipline error.
// Catch it in a unit test that builds the Retrofit instance eagerly:
// Retrofit.Builder().baseUrl(...).addCallAdapterFactory(new ScalaCallAdapterFactory()).build()
//   .create(ApiService.class);
//
// Static check that Future is parameterized:
Method m = ApiService.class.getMethod("getUser", String.class);
Type rt = m.getGenericReturnType();
if (!(rt instanceof ParameterizedType)
    && m.getReturnType() == scala.concurrent.Future.class) {
  throw new AssertionError("getUser() returns raw Future; parameterize it.");
}

Type guard

// In Scala the type system already enforces this; in Java interop, declare a typed helper:
// Prefer the Scala-side trait with an explicit type parameter rather than a raw Java Future.

Try / catch

// Fires at Retrofit.create() time. Isolate if you cannot fix the signature immediately:
try {
  ApiService api = retrofit.create(ApiService.class);
} catch (IllegalStateException e) {
  if (e.getMessage() != null && e.getMessage().contains("Future return type must be parameterized")) {
    throw new ConfigurationException("Service interface has a raw Future method", e);
  }
  throw e;
}

Prevention

When it happens

Trigger: A Retrofit interface method declared as Future getUser(...) (raw Future return). The factory's `returnType instanceof ParameterizedType` check fails immediately and throws IllegalStateException before any network call.

Common situations: Porting a synchronous Call-based method to the Scala adapter and omitting the type argument, or IDE raw-type warnings being ignored because the code still compiles under Java interoperability with Scala.

Related errors


AI-assisted analysis of lysine-dev/retrofit@d0b112dad0 (2026-08-13). Data as JSON: /api/errors/f0362cbd85930eb8. Report an issue: GitHub.