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言語で別物です。

くらべる

制約の書き場所と検証の場所

Pydantic

Python

制約はField(ge=1, le=50)のように型定義へ書きます。送信するスキーマからは外れて説明文に移り、応答は元の制約で検証されます。

独自DSL

Ruby

min_items:のような配列の制約は、ArrayOfではなくrequired/optionalの引数に渡します。

Jackson + Swagger

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. 1

    ローカル検証(SDK内)

    outputConfig(Class<T>)が、クラスから導いたスキーマに未対応の制約キーワードがないかを調べます。見るのはキーワードの有無で、制約の値までは検証しません。

  2. 2

    リモート検証(Claude側)

    同じスキーマがClaude側でも検証されます。未対応のformat値のように、ローカルは通ってもここでエラーになるものがあります。

  3. 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方向に分かれているのが見えます。

観点PythonRubyJava
型の宣言方法PythonPydantic BaseModelRubyAnthropic::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段構成で扱っています。

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