Claude Media
Claude Java SDKでアノテーションからツールを定義する — 非同期ストリーミングとメッセージバッチの実装

Claude Java SDKでアノテーションからツールを定義する — 非同期ストリーミングとメッセージバッチの実装

Claude Java SDKはJavaクラスのアノテーションからツールのJSONスキーマを自動生成します。非同期ストリーミング・ファイルアップロード・バッチ処理まで実装例で解説します。

Java SDKを導入してクライアントを設定する

Claude API用のJava SDKは、GradleかMavenで導入します。

// Gradle
implementation("com.anthropic:anthropic-java:2.67.0")
<!-- Maven -->
<dependency>
    <groupId>com.anthropic</groupId>
    <artifactId>anthropic-java</artifactId>
    <version>2.67.0</version>
</dependency>

必要なJavaのバージョンは8以降です。ドキュメントのコード例はJDK 25のコンパクトソースファイル形式(void main() と IO.println())で書かれていますが、API呼び出しの内容はどのJDKでも同じです。古いJDKでは IO.println を System.out.println に、main をクラス内の public static void main(String[] args) に置き換えます。

クライアントは AnthropicOkHttpClient.fromEnv() で環境変数(ANTHROPIC_API_KEY / ANTHROPIC_AUTH_TOKEN / ANTHROPIC_BASE_URL、対応するシステムプロパティは anthropic.apiKey 等)から自動設定するか、.builder().apiKey(...) で明示的に設定します。fromEnv() のあとに .apiKey(...) で上書きでき、システムプロパティは環境変数より優先されます。

AnthropicClient client = AnthropicOkHttpClient.fromEnv();
 
MessageCreateParams params = MessageCreateParams.builder()
  .maxTokens(1024L)
  .addUserMessage("Hello, Claude")
  .model(Model.CLAUDE_OPUS_5_5)
  .build();
 
Message message = client.messages().create(params);

クライアントごとにコネクションプールとスレッドプールを持つので、アプリケーション内で複数生成せず使い回します。設定を一時的に変えたいときは、新しいクライアントを作らず client.withOptions(...) で設定だけ変えたコピーを作ります。コピーは元のクライアントの接続・スレッドプールを共有します。

同期と非同期を切り替える

デフォルトのクライアントは同期です。client.async() で非同期実行に切り替わり、CompletableFuture を返すメソッド群が使えます。最初から非同期専用なら AnthropicOkHttpClientAsync.fromEnv() を使います。

くらべる

ストリーミングの戻り値と実行スレッド

StreamResponse

同期クライアント

createStreaming(params) が StreamResponse を返します。呼び出したスレッドで stream().forEach(...) を回すため、try-with-resources で閉じます。

AsyncStreamResponse

非同期クライアント

AsyncStreamResponse に subscribe() でコールバックを渡します。クライアントごとに専用のキャッシュドスレッドプール Executor で動き、現在のスレッドはブロックされません。

try (StreamResponse<RawMessageStreamEvent> streamResponse = client.messages().createStreaming(params)) {
    streamResponse.stream().forEach(chunk -> IO.println(chunk));
}

非同期側の Executor は、subscribe() の第2引数に別の Executor を渡すか、クライアント作成時に streamHandlerExecutor(...) を指定すると差し替えられます。

チャンク単位のイベントを、非ストリーミングAPIが返すのと同じ Message に組み立て直したいときは MessageAccumulator を使います。同期では Stream.peek(messageAccumulator::accumulate) をパイプラインに挟み、非同期では subscribe() のコールバック内で accumulate() を呼びます。ベータ版のメッセージには対になる BetaMessageAccumulator があります。

Javaクラスのアノテーションだけでツールを定義する

Java SDKは、任意のJavaクラスの構造からツール定義を導出します。PythonのSDKは関数のシグネチャ、TypeScriptのSDKはZodスキーマから導出するヘルパーを持ちますが、Javaではクラスそのものが定義になります。クラス名(スネークケースに変換)がツール名、フィールドがツールのパラメータです。

@JsonClassDescription("Get the weather in a given location")
static class GetWeather {
 
  @JsonPropertyDescription("The city and state, e.g. San Francisco, CA")
  public String location;
 
  @JsonPropertyDescription("The unit of temperature")
  public Unit unit;
 
  public Weather execute() {
    // 実処理
  }
}

ここで注意したいのは、addTool(Class) を持つのが通常の com.anthropic.models.messages.MessageCreateParams ではなく、ベータ側の com.anthropic.models.beta.messages.MessageCreateParams だという点です。公式のコード例は com.anthropic.models.beta.messages.* をインポートしており、v2.67.0で通常側のビルダーに addTool(GetWeather.class) を書くとコンパイルエラーになりました。ツール呼び出しのループも client.beta().messages() と Beta 接頭辞付きの型で組みます。

手順

ツール呼び出しの1往復

  1. 1

    クラスを渡す

    addTool(GetWeather.class) でスキーマを導出して追加します。

  2. 2

    tool_use を受け取る

    応答の content から toolUse() で取り出したブロックが、Claudeからの実行要求です。

  3. 3

    引数をクラスに戻して実行する

    BetaToolUseBlock.input(GetWeather.class) でJSONの引数をインスタンスへ復元し、execute() を呼びます。input(Class) はnullを返しうるので、nullの場合の分岐(公式サンプルでは new Weather("unknown") を返しています)を入れておきます。

  4. 4

    結果を返す

    BetaToolResultBlockParam.Builder.contentAsJson(Object) に戻り値を渡せば、JSON化してモデルへ返ります。直前の tool_use ブロックをアシスタントメッセージとして履歴に足してから、結果をユーザーメッセージとして足します。

クラスに付けられる主なアノテーションは5つです。

アノテーション役割
@JsonClassDescription役割ツールの説明(いつ・どう使うか)
@JsonPropertyDescription役割パラメータごとの詳細な説明
@JsonTypeName役割ツール名をスネークケース変換以外の名前に上書き
@JsonIgnore役割公開フィールド・getterをスキーマから除外
@JsonProperty役割非公開フィールド・getterをスキーマに含める

Claudeへ渡すJSONスキーマがAnthropicの制約を満たしているかは、既定で自動ローカル検証されます。無効にするなら addTool(GetWeather.class, JsonSchemaLocalValidation.NO) のように第2引数を渡します。

ツール利用の機能にはさらに「strict」モードがあり、モデルの出力が渡したJSONスキーマに必ず一致することを保証します。アノテーションから自動生成したスキーマにもstrictモードは適用でき、保証される範囲と保証の外にあるものはClaudeのstrict tool useが保証すること・しないことで扱っています。

導出されるスキーマをJavaで実際に出力する

アノテーションがどんなJSONスキーマになるかは、API呼び出しなしで見られます。ビルドしたパラメータの _tools() をJacksonでシリアライズするだけです。次のプログラムをJava 17とSDK v2.67.0で実行しました。クラス定義は先ほどの GetWeather に、@JsonIgnore を付けた公開フィールドと @JsonProperty を付けた非公開フィールドを加えています。

MessageCreateParams p = MessageCreateParams.builder()
    .model(Model.CLAUDE_OPUS_5_5).maxTokens(256L)
    .addUserMessage("hi").addTool(GetWeather.class).build();
System.out.println(ObjectMappers.jsonMapper().writeValueAsString(p._tools()));

出力は次のとおりです(読みやすいよう整形)。

{
  "input_schema": {
    "$schema": "https://json-schema.org/draft/2020-12/schema",
    "type": "object",
    "properties": {
      "hidden": { "type": "integer" },
      "location": { "type": "string", "description": "The city and state, e.g. San Francisco, CA" },
      "unit": { "type": "string", "enum": ["CELSIUS", "FAHRENHEIT"], "description": "The unit of temperature" }
    },
    "required": ["hidden", "location", "unit"],
    "additionalProperties": false
  },
  "name": "get_weather",
  "description": "Get the weather in a given location",
  "strict": true
}

読み取れるのは次の4点です。

  • 全フィールドが required になる: オプション扱いはなく、additionalProperties は false です
  • strict: true が自動で付く: addTool(Class) だけで入ります
  • @JsonProperty を付けた非公開の hidden は載り、@JsonIgnore の公開フィールドは載らない
  • 列挙型はコンスタント名で出る: toString() を "C" / "F" に上書きした Unit でも、スキーマの値は CELSIUS / FAHRENHEIT のままです

record も同じ形で使えます。Optional<String> のフィールドは {"type": ["string", "null"]} になり、これも required に入ります。List<Integer> は {"type": "array", "items": {"type": "integer"}} です。

@JsonTypeName("lookup_weather") を付けたクラスは、クラス名と無関係にツール名が lookup_weather になりました。

失敗例

クラスから導出できない2つの型

  • 非staticの内部クラス

    ツールクラスとして渡すと IllegalArgumentException が出ます。メッセージは class <クラス名> cannot be instantiated from JSON. It must be a top-level class, a static nested class, or a record. でした。ツール入力をデシリアライズするJackson Databindが、非staticの内部クラスをインスタンス化できないためです。

  • プロパティのない Map フィールド

    Map<String, String> のフィールドは、ローカル検証で 'properties' field is missing, empty or not an object. At least one named property must be defined. と弾かれました。検証を JsonSchemaLocalValidation.NO にすると通りますが、スキーマは {"type": "object", "additionalProperties": false} になり、キーを1つも持てません。

手書きのJSON Schemaでなら通る自由形式のオブジェクトも、クラスから導出する経路では名前付きプロパティを持つクラスに置き換える必要があります。

ツール名はキャメルケースからどう変換されるか

クラス名からツール名への変換は、単なる小文字化ではありません。単語の境界は「現在の文字が先頭でなく、大文字で、かつ直前が小文字か直後が小文字」という条件で決まります。GetWeather は get_weather、MyJSONParser は my_json_parser、ParseJSON は parse_json です。

連続する大文字の塊は、直後に小文字が続く箇所だけで区切られます。上のプログラムでも、この3つの変換結果は同じでした。

名前を明示したいときは @JsonTypeName で上書きします。連携先やログの規約でツール名の書式が決まっているなら、変換結果を毎回目視確認するより、最初から固定したほうが安全です。

ここで導出されるのは自作ツールの名前です。Anthropicが提供する公式ツール(code_execution 等)に付く _20250522 のようなバージョン接尾辞とは別の仕組みなので、公式ツールのアップグレードで自作ツール名を変える必要はありません。切り分けはClaude APIツールのバージョン管理にあります。

ファイルアップロードとメッセージバッチを扱う

ファイルは MultipartField 経由で送ります。Path から読み込んだ InputStream、URLから開いたストリーム、メモリー上のバイト列のいずれも同じ形で FileUploadParams に渡せます。

FileUploadParams params = FileUploadParams.builder()
  .file(
    MultipartField.<InputStream>builder()
      .value(Files.newInputStream(Paths.get("/path/to/file.pdf")))
      .contentType("application/pdf")
      .build()
  )
  .build();
 
FileMetadata fileMetadata = client.files().upload(params);

ダウンロード側は client.files().download(...) が HttpResponse を返し、response.body() を Files.copy(...) や任意の OutputStream へ転送します。接続を保持したままのストリームなので、try-with-resources で閉じます。

大量のリクエストをまとめて処理したい場合は、client.messages().batches() 名前空間でメッセージバッチを扱います。一覧・ページングは他のリスト系エンドポイントと共通で、autoPager() を使えば全ページを自動でたどりながら1件ずつ処理できます。手動なら page.hasNextPage() と page.nextPage() を使います。

ベータ機能とモデル名の新しい値を先取りする

正式リリース前の機能は client.beta() の名前空間から使います。個別のベータ機能には anthropic-beta HTTPヘッダーが必要で、メッセージパラメータ構築時に .addBeta(...) を呼ぶと自動で送信されます。ヘッダーの値は機能ごとに決まった文字列(例: コンテキスト管理機能なら context-management-2025-06-27)で、AnthropicBeta の対応する定数を渡します。

BetaMessage message = client.beta().messages().create(
    MessageCreateParams.builder()
        .model(Model.CLAUDE_OPUS_5_5)
        .maxTokens(1024L)
        .addBeta(AnthropicBeta.CONTEXT_MANAGEMENT_2025_06_27)
        .addUserMessage("Hello, Claude")
        .build());

Model や AnthropicBeta はJavaの閉じた enum ではなく、of(String) ファクトリメソッドを持つ拡張可能なクラスです。新しいモデルやベータヘッダーがSDKのリリースより先にAPI側で使えるようになっても、Model.of("some-new-model") のように文字列を直接渡せば動きます。.model("some-new-model") という文字列オーバーロードもあります。

補完や非推奨警告が効くのは Model.CLAUDE_OPUS_5_5 のような型付き定数だけです。SDKが対応済みならそちらを優先します。

リクエストIDで失敗を追跡する

リクエストが失敗してAnthropicへ調査を依頼するときは、request-id レスポンスヘッダーが手がかりになります。withRawResponse() を任意のHTTPメソッド呼び出しの前に付けるとステータスコード・ヘッダー・生のレスポンスボディへアクセスでき、requestId() でこのヘッダーだけを取り出せます。

HttpResponseFor<Message> message = client.messages().withRawResponse().create(params);
Optional<String> requestId = message.requestId();

失敗時にこのIDをログへ残しておくと、問い合わせで再現条件を伝える手間が減ります。message.parse() を呼べば、同じレスポンスを通常の Message として取り出せます。ログを出したいだけなら、環境変数 ANTHROPIC_LOG に info か debug を設定します。

リトライ・タイムアウト・エラー型を理解する

数字

既定値の目安

  • 自動リトライ

    2回

    間隔は指数バックオフ

  • 通常のタイムアウト

    10分

    リクエスト単位かクライアント単位で上書き可

  • ストリーミング時の上限

    60分

    maxTokens から算出

  • 非ストリーミング時の下限

    30秒

    上限は10分

自動リトライの対象は、接続エラー・408・409・429・5xx系に限られます。回数は AnthropicOkHttpClient.builder().fromEnv().maxRetries(4).build() のように変更します。

ストリーミングで maxTokens を指定した場合のタイムアウトは 60 * 60 * maxTokens / 128_000 秒が基準です。非ストリーミングで10分を超えると見込まれるリクエストは、SDKがエラーを送出して止めます。ストリーミングに切り替えるか、クライアントかリクエスト単位でタイムアウトを明示的に上書きすれば、このエラーは出ません。

例外は用途別に分かれています。

例外用途
AnthropicServiceException用途HTTPエラー全般の基底
AnthropicIoException用途ネットワークI/Oエラー
AnthropicRetryableException用途リトライ可能な失敗全般
AnthropicInvalidDataException用途パース済みデータを解釈できない失敗(必須のはずのプロパティがAPI側で省かれた場合など)
AnthropicException用途すべての例外の基底

ステータスコードは個別の例外型にもマッピングされます。

ステータス例外
400例外BadRequestException
401例外UnauthorizedException
403例外PermissionDeniedException
404例外NotFoundException
422例外UnprocessableEntityException
429例外RateLimitException
5xx例外InternalServerException
上記以外例外UnexpectedStatusCodeException

最初のレスポンスが成功したあとにSSEストリーミング中で起きたエラーは、SseException で通知されます。

実装でつまずきやすい点

ツールクラスの要件は前の節で扱ったので、ここでは別の落とし穴です。

  • Jacksonのバージョン競合: SDKはJackson 2.13.4以降と互換ですが、既定では2.19.4に依存します。MavenやGradleの設定で別バージョンに上書きされていて非互換を検知すると、例外を投げます。互換だと確信できる場合のみ checkJacksonVersionCompatibility で検知を無効化でき、無効化中はSDKが正しく動く保証がありません
  • レスポンスの型不一致が遅れて表に出る: 既定ではAPIの応答が期待する型と合わなくても例外になりません。該当プロパティに触れた時点で AnthropicInvalidDataException が出ます。受け取った直後にまとめて検査するなら create(params).validate() を呼ぶか、クライアントに responseValidation(true) を設定します
  • ProGuard / R8: SDKはリフレクションを使いますが、anthropic-java-core にkeepルールが同梱されているため、自動検出されます
  • 大きな maxTokens を非ストリーミングで指定する: ネットワークによってはアイドル接続が切られ、応答が返る前にタイムアウトします

まとめ

アノテーション駆動の定義は、スキーマをクラスの外で管理しなくて済む分だけ手数が少なく済みます。入力の形が固定されたツールならこれで足り、自由形式のキーを受けたいツールだけ別の設計になります。ほかの言語のSDKでの扱いはC# SDK実装ガイドやPHP SDK実装ガイドにあります。

この記事を共有:XはてブLinkedIn