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の制約(minimumやmaxLengthなど)を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を継承したクラスに、requiredとoptionalというクラスメソッドでフィールドを宣言します。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_itemsやmax_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方向に分かれているのが見えます。
| 観点 | Python | Ruby | Java |
|---|---|---|---|
| 型の宣言方法 | PythonPydantic BaseModel | RubyAnthropic::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の構造化出力入門で扱っています。