Claude Media
Structured outputsの型定義パターンをPython・Ruby・Javaで実装比較

Structured outputsの型定義パターンをPython・Ruby・Javaで実装比較

Structured outputsの型定義はPythonがPydantic、RubyがAnthropic::BaseModelの独自DSL、Javaがpublicフィールドのクラス。3言語の書き方と検証の違いをコードで比較します。

Structured outputsの型定義パターンは言語ごとに何が違うのか

Structured outputsは、Claudeの応答を指定したJSON Schemaに強制するAPI機能です。output_config.formatにスキーマを渡すと、Claudeはそのスキーマに一致するJSONしか返しません。

このスキーマを手書きする代わりに、各言語SDKは「使い慣れた型定義がそのままスキーマになる」しくみを持っています。PythonはPydanticのBaseModel、RubyはAnthropic独自のBaseModel、Javaはpublicフィールドを持つ素のクラス。3言語とも「型定義 = スキーマ」という思想は共通ですが、書き方も検証が発生する場所も別物です。

JSON Schemaの制約(minimummaxLengthなど)をSDKがどう自動変換して送るかは、Structured outputsのJSON Schema制限をSDKが自動変換する仕組みで扱っています。本記事はその手前、3言語それぞれの型定義の書き方そのものに絞ります。

PythonはPydanticのBaseModelをclient.messages.parse()に渡す

Python SDKの型定義は、PydanticのBaseModelを書いてclient.messages.parse()output_format引数に渡すだけです。返ってくるresponse.parsed_outputは、渡したモデルのインスタンスそのものになります。

from pydantic import BaseModel
from anthropic import Anthropic
 
 
class ContactInfo(BaseModel):
    name: str
    email: str
    plan_interest: str
    demo_requested: bool
 
 
client = Anthropic()
 
response = client.messages.parse(
    model="claude-opus-5",
    max_tokens=1024,
    messages=[{"role": "user", "content": "..."}],
    output_format=ContactInfo,
)
 
print(response.parsed_output.name)

create()ではなくparse()を使う点が要です。create()に生のスキーマ辞書を渡すoutput_config={"format": {...}}という書き方もできますが、parse()はPydanticモデルを直接受け取り、バリデーションとparsed_outputへの詰め替えまで一括でやってくれます。

手書きのスキーマをさらに調整したい場合はtransform_schema()ヘルパーを使います。TypeAdapter(ContactInfo).json_schema()でPydanticモデルをJSON Schemaに変換し、transform_schema()でstructured outputs対応の形に整えたうえで、フィールドを追加するといった手を加えられます。

from anthropic import transform_schema
from pydantic import TypeAdapter
 
schema = TypeAdapter(ContactInfo).json_schema()
schema = transform_schema(schema)
schema["properties"]["custom_field"] = {"type": "string"}

Field(ge=1, le=50)のような数値制約は、送信されるスキーマからは取り除かれ、説明文に書き換えられます。この変換の中身は前掲のJSON Schema制限記事に譲りますが、Python側で意識するのはFieldに制約を書くところまでで、送信形への変換はSDKの仕事という分業だけ覚えておけば十分です。

RubyはAnthropic::BaseModelの独自DSLで配列・enumまで書ける

Ruby SDKはAnthropic::BaseModelを継承したクラスに、requiredoptionalというクラスメソッドでフィールドを宣言します。Pydanticのような外部ライブラリではなく、SDK自身が型システムを持っている点がPythonとの違いです。

class ContactInfo < Anthropic::BaseModel
  required :name, String
  required :email, String
  required :plan_interest, String
  required :demo_requested, Anthropic::Boolean
end
 
client = Anthropic::Client.new
 
message = client.messages.create(
  model: "claude-opus-5",
  max_tokens: 1024,
  messages: [{role: "user", content: "..."}],
  output_config: {format: ContactInfo}
)
 
contact = message.parsed_output
puts contact.name

配列や列挙型が絡むと、このDSLの独自性がはっきりします。Anthropic::ArrayOf[T]で要素の型を指定した配列、Anthropic::EnumOf[:a, :b]で列挙値、Anthropic::UnionOf[T1, T2]でJSON SchemaのanyOfに相当する共用体を書けます。

class FamousNumber < Anthropic::BaseModel
  required :value, Float
  optional :reason, String, doc: "why is this number mathematically significant?"
end
 
class Output < Anthropic::BaseModel
  required :numbers, Anthropic::ArrayOf[FamousNumber], min_items: 3, max_items: 5
end

説明文はdoc:キーワードで付けます。PythonのField(description=...)と役割は同じですが、min_itemsmax_itemsのような配列レベルの制約はArrayOf自体ではなく、required/optionalの呼び出し側にキーワードとして渡す設計です。この位置を間違えると制約が効かないので注意してください。

JavaはPOJOのpublicフィールドからJacksonでスキーマを自動導出する

Java SDKでは、publicフィールドを持つ素のJavaクラスをoutputConfig(Class<T>)に渡すだけでスキーマが自動生成されます。裏側で使われているのはJackson Databind(com.fasterxml.jackson.databind)で、このライブラリがクラスをJSONへ読み書きします。

static class ContactInfo {
    public String name;
    public String email;
    public String planInterest;
    public boolean demoRequested;
}
 
void main() {
    AnthropicClient client = AnthropicOkHttpClient.fromEnv();
 
    StructuredMessageCreateParams<ContactInfo> createParams = MessageCreateParams.builder()
        .model(Model.CLAUDE_OPUS_5)
        .maxTokens(1024)
        .outputConfig(ContactInfo.class)
        .addUserMessage("...")
        .build();
 
    StructuredMessage<ContactInfo> response = client.messages().create(createParams);
    ContactInfo contact = response.content().stream()
        .flatMap(block -> block.text().stream())
        .findFirst().orElseThrow().text();
}

Jacksonが相手である以上、スキーマ用のクラスはトップレベルかstaticなネストクラスとして書く必要があります。Jackson Databindは非staticな内部クラスをインスタンス化できないため、これを守らないと実行時エラーになります。

説明文を付けたいときはJacksonのアノテーションを使います。@JsonPropertyDescriptionはフィールド単位、@JsonClassDescriptionはクラス単位の説明で、@JsonIgnoreはスキーマから除外、@JsonPropertyは非publicなフィールドや取得メソッドを取り込みます。数値やリストの制約にはSwagger Core(OpenAPI 3)の@Schema@ArraySchemaも使え、両方を同じフィールドに付けた場合はJacksonのアノテーションが優先されます。

継承と合成でJSON出力の形が変わる点も、他の2言語にはない癖です。あるクラスを別クラスのフィールドとして持たせる合成はネストしたJSONを生み、extendsで継承した場合は同じフィールドがすべてフラットな1階層のJSONになります。同じ情報を持たせるつもりでも、合成か継承かでクライアント側の受け取り方が変わるので、既存のドメインモデルをそのままスキーマに流用するときは注意が必要です。

バリデーションも2段階です。outputConfig(Class<T>)はまずSDK内でローカルにスキーマを検証し、そのあとClaude側でも同じスキーマがリモートで検証されます。ローカル検証は使われているキーワードが対応範囲内かをチェックするだけで、制約の値そのものまでは見ていません。未対応のformat値のようにローカルは通ってもリモートでエラーになるケースがあるため、SDKのバージョンが古いことが原因でローカル検証だけが失敗するなら、JsonSchemaLocalValidation.NOを渡してローカル検証自体を無効化できます。

JSONキー名の扱いも言語ごとに癖があります。Pythonはフィールド名がそのままJSONキーになり、Ruby SDKも同様にシンボル名(:invoice_numberなど)がそのまま使われます。どちらも命名規約がsnake_caseで揃っているためです。一方Javaの慣習はcamelCaseなので、フィールド名invoiceNumberをそのまま使うとJSONキーもinvoiceNumberになってしまい、Claudeへ渡すスキーマのキー名が他言語と揃いません。スキーマのキーをAPI側の慣習(snake_case)に合わせたいときは、@JsonProperty("invoice_number")でフィールドごとに明示的にリネームします。

static class Invoice {
    @JsonProperty("invoice_number")
    public String invoiceNumber;
 
    @JsonProperty("total_amount")
    public double totalAmount;
 
    @JsonProperty("line_items")
    public List<LineItem> lineItems;
}

Java側のコードはcamelCaseのままで読み書きしつつ、Claudeとやり取りするJSONだけsnake_caseに揃えられるのがこの書き方の利点です。@JsonPropertyを付けなければJSONキーはフィールド名(camelCase)がそのまま使われるので動作自体は崩れませんが、他言語のSDKや既存のsnake_case前提のスキーマと突き合わせるときはキー名がずれる点に注意してください。

3言語の型定義パターンを実装コストと表現力で比較する

書き方の違いを一望すると、Pythonは外部ライブラリ主導、RubyはSDK内製DSL、Javaは既存のJavaエコシステム(Jackson・Swagger)への依存という3方向に分かれているのが見えます。

観点PythonRubyJava
型の宣言方法PythonPydantic BaseModelRubyAnthropic::BaseModel継承Javapublicフィールドを持つ素のクラス
呼び出しメソッドPythonclient.messages.parse()Rubycreate(output_config: {format: Model})JavaoutputConfig(Class<T>)
配列・共用体Pythonlist[T] / Union[T1, T2]RubyArrayOf[T] / UnionOf[T1, T2]JavaList<T>(共用体はクラス由来の導出に記載なし)
説明文の付け方PythonField(description=...)Rubydoc: キーワードJava@JsonPropertyDescription
バリデーションの発生場所PythonSDK側(Pydantic)RubySDK側Javaローカル(SDK)+リモート(Claude)の2段階
JSONキー名の決まり方Pythonフィールド名がそのまま(snake_case)Rubyシンボル名がそのまま(snake_case)JavacamelCaseがそのまま。snake_caseにするには@JsonPropertyが必要

配列と共用体の行だけは実装コストに直結します。RubyとPythonは言語の型システムの上に素直に共用体を表現できますが、Java SDKの公式ドキュメントは共用体の書き方に触れていません。JsonOutputFormat.Schemaを手で組んでOutputConfigに包む経路であれば任意のスキーマを渡せます。逆にネストした構造の表現力では、Jacksonの合成・継承の使い分けがPython・Rubyにはない柔軟さを持っています。

JacksonアノテーションはPydanticの表現力にどこまで迫れるか

3言語を並べると、JavaのアノテーションはPydanticの表現力に迫っているものの、性格が異なることが見えてきます。PydanticはField(ge=1, le=50)のような制約をPythonの型システムの一部として持ち、SDKが自動でスキーマから制約を除いて説明文に転記します。

Javaでは同じ役割をSwagger Coreのアノテーションが担いますが、ローカル検証はキーワードの対応可否しか見ていません。制約の値自体の妥当性はリモート、つまりClaude側に投げているというのが実態です。

この違いは設計思想の差というより、依存しているエコシステムの差です。JacksonとSwagger CoreはSpring Bootなどの既存Java資産で広く使われているライブラリなので、Structured outputs専用の学習コストをかけずに既存のドメインクラスへアノテーションを足すだけで済みます。一方でRubyのArrayOf/EnumOf/UnionOfはAnthropic SDKが自前で持つ型システムで、Pydanticに最も近い表現力を持つ代わりに、SDK固有の書き方を新たに覚える必要があります。既存のJavaコードベースに後付けするならJava、型定義そのものを新規に書くならRubyかPythonが手に馴染みやすい、という向き不向きです。

まとめ

3言語とも「型定義がそのままスキーマになる」設計は共通ですが、実装の癖ははっきり分かれます。PythonはFieldで制約を書いてSDKに変換を任せる分業、RubyはArrayOf/EnumOf/UnionOfという独自DSLでPydanticに近い表現力を確保、Javaは既存のJacksonアノテーションを流用できる代わりに合成と継承でJSON構造が変わる癖と付き合う必要があります。

自分のコードベースに既にPydanticモデルやJacksonアノテーション付きのクラスがあるなら、そのSDKの型定義パターンにそのまま乗るのが最短です。ゼロから設計するなら、RubyのArrayOf/UnionOfのように配列や共用体を明示的に書ける言語のほうが、複雑なスキーマを組むときの見通しは立てやすくなります。

各言語のSDK全体の使い方はClaudeのRuby SDKでツール実行ループとページネーションを実装するClaude Java SDKでアノテーションからツールを定義する、JSONモード自体の基本はClaude JSONモードの使い方、Agent SDK経由での構造化出力はAgent SDKの構造化出力入門で扱っています。

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