はじめに (対象読者・この記事でわかること)

この記事は、Java・Android アプリ開発者、特に Retrofit を使って HTTP 通信を実装しようとしている方を対象としています。
Retrofit のインターフェイスにおいて URL が正しく定義できない、もしくはビルドエラーになるケースの原因を体系的に把握し、実際のコード例を通じて解決できるようになることを目的としています。

  • Retrofit のベース URL とエンドポイントの関係が分かる
  • アノテーションの書き方や文字列リテラルの扱いで起きやすい落とし穴を把握できる
  • 正しい実装パターンと、エラー時のデバッグ手順が身につく

この記事を書いた背景は、社内プロジェクトで「@GET の URL がコンパイルエラーになる」問題が頻発し、同様の悩みを抱える開発者が多いことが判明したためです。

前提知識

この記事を読み進める上で、以下の知識があるとスムーズです。

  • Java の基本文法(特にインターフェイスとアノテーション)
  • Android Studio の基本的な使い方と Gradle ビルドの概念
  • HTTP の基礎(GET/POST などのメソッド)

Retrofit の URL 定義ができない原因と基本概念

Retrofit は、ベース URLエンドポイント の組み合わせで最終的なリクエスト URL を生成します。

Java
Retrofit retrofit = new Retrofit.Builder() .baseUrl("https://api.example.com/") .addConverterFactory(GsonConverterFactory.create()) .build();

この baseUrl には必ず末尾にスラッシュ (/) が必要です。スラッシュが無いと IllegalArgumentException: Base URL must end in / が発生します。

インターフェイス側では、@GET@POST などの HTTP メソッドアノテーションに 相対パス を指定します。ここで間違いやすいポイントは次の通りです。

項目 よくあるミス例 正しい書き方
スラッシュの有無 @GET("users")(ベース URL に / が無い) @GET("users")(ベース URL が …/ で終わっていること)
プレフィックス @GET("/users")(先頭に / が付く) @GET("users")(先頭は付けない)
クエリ文字列 @GET("search?q={query}")(文字列リテラルで {} が評価されない) @GET("search") + @Query("q") String query
動的パス @GET("users/{id}")@Path が抜ける @GET("users/{id}") + @Path("id") int id

また、Kotlin と Java の文字列リテラルの違いに注意が必要です。Java ではエスケープが必要になるケースがありますが、Retrofit のアノテーションは 文字列リテラルそのもの を期待するため、余計なエスケープやダブルクオートの入れ子はエラー原因になります。

実装例とトラブルシューティング

以下では、典型的な「URL が定義できない」ケースを段階的に解決していく手順を示します。コードは Java で記述し、Android Studio のプロジェクト構成を想定しています。

ステップ1:プロジェクトの Gradle 設定

まずは build.gradle (app) に Retrofit と Gson の依存関係を追加します。

Gradle
dependencies { implementation 'com.squareup.retrofit2:retrofit:2.9.0' implementation 'com.squareup.retrofit2:converter-gson:2.9.0' // Optional: OkHttp のロギングインターセプター implementation 'com.squareup.okhttp3:logging-interceptor:4.9.3' }

ポイント
- バージョンは最新安定版を使用(本稿執筆時点は 2.9.0)。
- converter-gson が無いと JSON のシリアライズ/デシリアライズができません。

ステップ2:ベース URL の正しい設定

RetrofitClient.java を作成し、ベース URL に必ず / を付けます。

Java
public class RetrofitClient { private static final String BASE_URL = "https://api.example.com/"; // ← 必ず最後にスラッシュ private static Retrofit retrofit = null; public static Retrofit getInstance() { if (retrofit == null) { // デバッグ用に OkHttp のロガーを設定(任意) HttpLoggingInterceptor logging = new HttpLoggingInterceptor(); logging.setLevel(HttpLoggingInterceptor.Level.BODY); OkHttpClient client = new OkHttpClient.Builder() .addInterceptor(logging) .build(); retrofit = new Retrofit.Builder() .baseUrl(BASE_URL) .client(client) .addConverterFactory(GsonConverterFactory.create()) .build(); } return retrofit; } }

ハマりポイント
- BASE_URLhttps://api.example.com(末尾スラッシュなし)の場合、IllegalArgumentException が投げられます。
- プロトコル(http:// または https://)が欠如していると同様にエラーになります。

ステップ3:インターフェイスの定義

次に、エンドポイントを記述したインターフェイス ApiService.java を作成します。

Java
public interface ApiService { // 正しい相対パス:ベース URL の末尾スラッシュと結合される @GET("users") Call<List<User>> getUserList(); // 動的パスパラメータ @GET("users/{id}") Call<User> getUserDetail(@Path("id") int userId); // クエリパラメータ付き GET @GET("search") Call<List<User>> searchUser(@Query("q") String keyword); }

注意点
- @GET("users") のように先頭にスラッシュは付けません。
- 動的パスは {} で囲み、@Path アノテーションで変数をバインドします。
- クエリ文字列は @Query を使うのがベストプラクティスです。

ステップ4:呼び出し側コード(例:Repository)

Java
public class UserRepository { private final ApiService apiService; public UserRepository() { apiService = RetrofitClient.getInstance().create(ApiService.class); } public void fetchUsers(Callback<List<User>> callback) { Call<List<User>> call = apiService.getUserList(); call.enqueue(callback); } public void fetchUserDetail(int id, Callback<User> callback) { Call<User> call = apiService.getUserDetail(id); call.enqueue(callback); } public void search(String keyword, Callback<List<User>> callback) { Call<List<User>> call = apiService.searchUser(keyword); call.enqueue(callback); } }

ステップ5:ハマった点やエラー解決

エラー例1:IllegalArgumentException: Base URL must end in /

  • 原因BASE_URL の末尾にスラッシュが無い。
  • 解決策BASE_URL"https://api.example.com/" に修正。

エラー例2:java.lang.IllegalArgumentException: URL null@GET("/users") 使用時)

  • 原因@GET のパスが先頭スラッシュで始まっているため、Retrofit が絶対パスとみなしベース URL と結合できず null になる。
  • 解決策@GET("users") に書き換える。

エラー例3:java.lang.IllegalArgumentException: Expected a URL path butusers/{id}is not a valid path

  • 原因@Path パラメータ名がアノテーション側とメソッド引数で一致していない、または {} が正しく閉じていない。
  • 解決策@Path("id") とメソッド引数 int userId が一致しているか確認し、@GET("users/{id}"){} が正しく書かれているか検証。

エラー例4:java.lang.IllegalStateException: Expected a @GET/@POST annotated method but found ...

  • 原因@GET に文字列リテラルが抜けているか、誤って @GET の前に static 修飾子を付与した。
  • 解決策:メソッド宣言は @GET("endpoint") Call<...> methodName(...); の形に統一。

ステップ6:ユニットテストで URL 生成を検証

Retrofit は実行時に URL を生成するため、JUnit と MockWebServer を組み合わせるとテストが容易です。

Java
public class ApiServiceTest { private MockWebServer mockWebServer; private ApiService apiService; @Before public void setUp() throws IOException { mockWebServer = new MockWebServer(); mockWebServer.start(); Retrofit retrofit = new Retrofit.Builder() .baseUrl(mockWebServer.url("/")) // 動的にポートが割り当てられる .addConverterFactory(GsonConverterFactory.create()) .build(); apiService = retrofit.create(ApiService.class); } @Test public void testGetUserListUrl() throws Exception { // キューに空レスポンスを用意 mockWebServer.enqueue(new MockResponse().setBody("[]")); // 実際に呼び出す Call<List<User>> call = apiService.getUserList(); call.execute(); // 発行されたリクエストを取得 RecordedRequest request = mockWebServer.takeRequest(); assertEquals("/users", request.getPath()); // 期待通りのパスか検証 } @After public void tearDown() throws IOException { mockWebServer.shutdown(); } }

ポイント
- mockWebServer.url("/") でベース URL を自動生成し、テストごとにポート競合を防げます。
- request.getPath() が期待した相対パスになるかを assert するだけで、URL の構成ミスを簡単に検出可能です。

まとめ

本記事では、Retrofit において URL が定義できない 典型的な原因と、その解決策を実装例を交えて体系的に解説しました。

  • ベース URL の末尾スラッシュ忘れは必ずチェックポイントにする
  • @GET@POST のパスは先頭スラッシュを付けず、相対パスとして記述
  • 動的パスやクエリは必ず @Path@Query アノテーションでバインド
  • エラーはビルド時と実行時で分けて対処し、MockWebServer でテストすれば早期に問題を捕捉できる

これにより、Retrofit の URL 定義で躓くことなく、安定した API 呼び出し基盤を構築できるようになります。次回は、認証ヘッダーの自動付与と複数ベース URL の切替 について解説する予定です。

参考資料