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言語で別物です。
制約の書き場所と検証の場所
Python
制約はField(ge=1, le=50)のように型定義へ書きます。送信するスキーマからは外れて説明文に移り、応答は元の制約で検証されます。
Ruby
min_items:のような配列の制約は、ArrayOfではなくrequired/optionalの引数に渡します。
Java
説明はJacksonのアノテーション、制約はSwagger Coreの@Schemaで付けます。ローカル検証はキーワードの対応可否だけを見て、値の妥当性はClaude側に委ねます。
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-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"}制約の扱いは、変換の前後を並べるとはっきりします。Python SDK v1.11.0で、数値制約・Optional・配列の下限を持つモデルをtransform_schema()に通しました。
from typing import Optional
from pydantic import BaseModel, Field, TypeAdapter
from anthropic import transform_schema
class Item(BaseModel):
sku: str
qty: int = Field(ge=1, le=50)
class Order(BaseModel):
order_id: str
note: Optional[str] = None
items: list[Item] = Field(min_length=1)
print(transform_schema(TypeAdapter(Order).json_schema()))出力の要点だけを抜き出すと、次のようになります。
{
"properties": {
"order_id": {"type": "string", "title": "Order Id"},
"note": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"description": "{default: None}"
},
"items": {"type": "array", "items": {"$ref": "#/$defs/Item"}, "minItems": 1}
},
"additionalProperties": false,
"required": ["order_id", "items"]
}Item.qtyのge=1, le=50は送信側から消え、"description": "{maximum: 50, minimum: 1}"という説明文に変わりました。Optional[str]はanyOfのまま残り、noteはrequiredに入りません。一方でmin_length=1はminItems: 1として送信側に残り、全オブジェクトにadditionalProperties: falseが付きます。Python側で意識するのはFieldに制約を書くところまでで、送信形への変換はSDKの仕事という分業です。変換の規則そのものは前掲のJSON Schema制限記事にまとまっています。
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-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の呼び出し側にキーワードとして渡す設計です。ArrayOf自体に渡すのは公式の書き方ではありません。
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_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出力の形が変わる点は、Javaの公式解説が明記している癖です。あるクラスを別クラスのフィールドとして持たせる合成はネストしたJSONを生み、extendsで継承した場合は同じフィールドがすべてフラットな1階層のJSONになります。同じ情報を持たせるつもりでも、合成か継承かでクライアント側の受け取り方が変わるので、既存のドメインモデルをそのままスキーマに流用するときは注意が必要です。
バリデーションは2段階で、エラーの出どころによって打ち手が変わります。
Javaでスキーマが検証される順序
- 1
ローカル検証(SDK内)
outputConfig(Class<T>)が、クラスから導いたスキーマに未対応の制約キーワードがないかを調べます。見るのはキーワードの有無で、制約の値までは検証しません。 - 2
リモート検証(Claude側)
同じスキーマがClaude側でも検証されます。未対応の
format値のように、ローカルは通ってもここでエラーになるものがあります。 - 3
ローカルだけが落ちるとき
SDKのバージョンが古いと、リモートでは通るスキーマがローカルで落ちることがあります。
outputConfig(BookList.class, JsonSchemaLocalValidation.NO)でローカル検証だけを無効にできます。
スキーマ自体が作れなくなる形も決まっています。プロパティが1つもないクラスはバリデーションエラーになり、次の4つがその典型です。
- クラスにフィールドもgetterもない
- publicなメンバーがすべて
@JsonIgnore付き - publicでないメンバーに
@JsonPropertyがない - フィールドが
Map型(空のpropertiesが生成される)
任意フィールドの扱いも他言語と違います。クラスから導いたスキーマは常に全プロパティを必須にし、@JsonProperty(required = false)のfalseは無視されます。省略可能にしたい項目をJavaで表現する方法は、このクラス経由のルートにはありません。
JSONキー名の決まり方にも癖があります。Pythonはフィールド名がそのままキーになり、上の実行例でもorder_idが送信側に出ています。Rubyも公式例ではシンボル名がそのままキーに出ています。Javaは公式の例がフィールド名自体をplan_interestやdemo_requestedとsnake_caseで宣言しており、出力JSONのキーも同じ綴りになります。
camelCaseで書く場合はキーもcamelCaseになります。publicでないフィールドにpublicなgetterがあるとき、プロパティ名はgetterから決まります。
static class Settings {
private String myValue;
public String getMyValue() { return myValue; }
}このSettingsから導かれるプロパティは"myValue"です。getter名が慣習から外れるときは、そのメソッドに@JsonPropertyを付けて取り込みます。他言語のスキーマやsnake_case前提のAPIと突き合わせる場合は、Javaだけキーがずれないよう、フィールド名の綴りから先に決めておくと手戻りが出ません。
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] / Optional[T] | RubyArrayOf[T] / UnionOf[T1, T2] | JavaList<T>(共用体を導くクラスの例は公式にない) |
| 説明文の付け方 | PythonField(description=...) | Rubydoc: キーワード | Java@JsonPropertyDescription |
| 任意フィールド | PythonOptional[T]はrequiredから外れる | Rubyoptionalで宣言 | Java常に全プロパティが必須 |
| JSONキー名の決まり方 | Pythonフィールド名がそのまま | Rubyシンボル名がそのまま(公式例) | Javaフィールド名かgetter名がそのまま(camelCaseならcamelCase) |
配列と共用体、それに任意フィールドの行は実装コストに直結します。PythonはOptionalがanyOfになり、RubyはUnionOfで同じものを書けます。Javaのクラス経由のルートでは共用体も任意フィールドも表現しにくく、必要ならJsonOutputFormat.Schemaを手で組んでOutputConfigに包む経路に移ります。ネストした構造では、Javaは合成と継承のどちらを使うかで出力の形を選べます。
JacksonアノテーションはPydanticの表現力にどこまで迫れるか
PydanticのField(ge=1, le=50)に相当する役割を、JavaではSwagger Coreのアノテーションが担います。差は表現力よりも、何に依存して書くかにあります。
この違いは設計思想の差というより、依存しているエコシステムの差です。JacksonとSwagger Coreは既存のJavaクラスに付けられるアノテーションなので、Structured outputs専用の書き方を覚えなくても、既存のドメインクラスへアノテーションを足せます。一方でRubyのArrayOf/EnumOf/UnionOfはAnthropic SDKが自前で持つ型システムです。配列・列挙・共用体を型定義の中で書ける代わりに、SDK固有の書き方を新たに覚える必要があります。
まとめ
3言語とも「型定義がそのままスキーマになる」設計は共通ですが、実装の癖ははっきり分かれます。既にPydanticモデルやJacksonアノテーション付きのクラスがあるなら、そのSDKの型定義パターンにそのまま乗れます。省略可能な項目が多いスキーマなら、OptionalのPythonかoptionalのRubyのほうが書きやすくなります。
各言語のSDK全体の使い方はClaudeのRuby SDKでツール実行ループとページネーションを実装するとClaude Java SDKでアノテーションからツールを定義する、JSONモード自体の基本はClaude JSONモードの使い方、Agent SDK経由での構造化出力はAgent SDKの構造化出力入門で扱っています。レビューの合否のような固定フィールドをプロンプトチェーンの中で扱う実装は、プロンプトチェーンでClaudeに自己修正させる3段構成で扱っています。