◉GEMINI LABEN
●TTS — TTS プレビュー3種の停止日は 12/18 です(deprecations 本文)。11/17 ではありません●CLI 0.68 — Gemini CLI は安定版 v0.68.0、プレビュー v0.69.0-preview.0(11/10)が最新です●GEMINI 2.5 — gemini-2.5-pro と 2.5-flash は 12/1 に API で停止、残り16日。移行先は 3.8 Flash と 3.5 Flash-Lite●NODE 26 — Node 26 で Gemini CLI のタイムアウト設定が効かない、という報告が出ています●OMNI 1.1 — gemini-omni-1.1-flash への要求は、12/3 から 1.2 Flash へ自動で振り分けられます●NEW — OpenAI 互換で JSON の形が揃わないときの3段階●TTS — TTS プレビュー3種の停止日は 12/18 です(deprecations 本文)。11/17 ではありません●CLI 0.68 — Gemini CLI は安定版 v0.68.0、プレビュー v0.69.0-preview.0(11/10)が最新です●GEMINI 2.5 — gemini-2.5-pro と 2.5-flash は 12/1 に API で停止、残り16日。移行先は 3.8 Flash と 3.5 Flash-Lite●NODE 26 — Node 26 で Gemini CLI のタイムアウト設定が効かない、という報告が出ています●OMNI 1.1 — gemini-omni-1.1-flash への要求は、12/3 から 1.2 Flash へ自動で振り分けられます●NEW — OpenAI 互換で JSON の形が揃わないときの3段階
記事一覧/API / SDK
◈ API / SDK/2026-11-14中級

OpenAI 互換レイヤーで JSON の形が揃わないとき、既存コードを残したまま response_json_schema へ寄せる3段階

OpenAI SDK の送り先だけを Gemini に向けたコードで、構造化出力が効いていないように見えるときの確かめ方と、関数の境界を変えずに Gemini SDK の response_json_schema へ移す手順を Python で整理します。

Gemini API255OpenAI 互換構造化出力19response_json_schema移行11

個人開発で運用している壁紙アプリの投稿画像に付いた説明文から、タグを1つだけ選ばせる小さな処理を書き直していた夕方のことです。もとは OpenAI の SDK で書いたコードで、base_url とモデル名だけを Gemini に向けて動かしておりました。返ってくるのは JSON に見える文字列なのに、ときどき前後に説明文が付き、json.loads がそこで止まります。

最初に私がしたのは、プロンプトに「JSON だけを返してください」と三度書き足すことでした。結果は芳しくありませんでした。原因はプロンプトではなく、スキーマがそもそも送られていなかったことにあったのです。

同じように「OpenAI 互換のまま試したら構造化出力が効かない」と感じている方に向けて、効いているかの確かめ方と、既存コードを捨てずに Gemini SDK の response_json_schema へ寄せる手順を書き残します。

最初に確かめること——スキーマは本当に送られているか

Google の公式ドキュメント「OpenAI compatibility」には、互換レイヤーで構造化出力を使う例が載っています。一方で、Google AI Developers Forum には「互換レイヤー経由だと structured output が効かない」「propertyOrdering が守られない」という報告がいくつも上がっています。

私のコードを見直すと、response_format={"type": "json_object"} だけを渡していました。これは「JSON で返してほしい」という指定で、どの形の JSON かまでは伝えていません。キー名や許可値はプロンプトの文章頼みで、モデルが少し気を利かせると形が崩れます。

確かめ方は単純です。互換レイヤーに渡している response_format を、次の3つのどれかに当てはめてみてください。

渡しているものモデルに伝わること形が揃うか
何も渡していない文章で返すプロンプト次第
type に json_objectJSON で返す(形は未指定)キー名・値はぶれうる
type に json_schema と schema 本体この形の JSON で返す形は揃いやすい(値の意味は別途検証)

二行目に当てはまるなら、まずは三行目に直すだけで症状の多くは消えます。それでも揃わないときに、経路そのものを見直すのが順番として無理がありません。

段階1——互換レイヤーのまま、スキーマを渡して検証する

最初の段階では SDK を替えません。Pydantic でモデルを定義し、その JSON Schema を json_schema として渡したうえで、返ってきた文字列を同じモデルで検証します。

import logging
import os
from typing import Literal, Optional
 
from openai import OpenAI
from pydantic import BaseModel, ValidationError
 
# モデル名は使う前に ai.google.dev の Models ページで提供状況を確かめてください
MODEL = "gemini-4-hydrogen"
SYSTEM = "壁紙の説明文から、最も近いタグを1つだけ選んでください。"
 
 
class WallpaperTag(BaseModel):
    label: Literal["landscape", "portrait", "abstract", "other"]
    reason: str
 
 
compat = OpenAI(
    api_key=os.environ["GEMINI_API_KEY"],
    base_url="https://generativelanguage.googleapis.com/v1beta/openai/",
)
 
 
def classify_compat(caption: str) -> Optional[WallpaperTag]:
    completion = compat.chat.completions.create(
        model=MODEL,
        messages=[
            {"role": "system", "content": SYSTEM},
            {"role": "user", "content": caption},
        ],
        response_format={
            "type": "json_schema",
            "json_schema": {
                "name": "wallpaper_tag",
                "schema": WallpaperTag.model_json_schema(),
            },
        },
    )
    text = completion.choices[0].message.content or ""
    try:
        return WallpaperTag.model_validate_json(text)
    except ValidationError as e:
        # 形が崩れた応答は捨てずに残す。どの経路で崩れたかを後で数えるため
        logging.warning("compat schema mismatch: %s | raw=%r", e.errors()[0]["type"], text[:200])
        return None

ここで大事なのは、検証に失敗した応答を握りつぶさずにログへ残すことです。失敗の件数が数えられれば、次の段階へ進むかどうかを感覚ではなく記録で決められます。

段階2——関数の境界はそのまま、中身を Gemini SDK に替える

段階1で失敗が残るとき、あるいは optional やフィールド順の扱いを Gemini 側の仕様どおりに揃えたいときは、呼び出し経路を Gemini SDK(google-genai)に移します。呼び出し側のコードを触らずに済むよう、関数の名前・引数・戻り値の型は段階1と同じにしておきます。

from typing import Optional
 
from google import genai
from google.genai import types
from pydantic import ValidationError
 
# 先ほどの MODEL・SYSTEM・WallpaperTag をそのまま使う
native = genai.Client(api_key=os.environ["GEMINI_API_KEY"])
 
 
def classify_native(caption: str) -> Optional[WallpaperTag]:
    res = native.models.generate_content(
        model=MODEL,
        contents=caption,
        config=types.GenerateContentConfig(
            system_instruction=SYSTEM,
            response_mime_type="application/json",
            # Pydantic の定義から作った JSON Schema を、互換レイヤーと同じものとして渡す
            response_json_schema=WallpaperTag.model_json_schema(),
        ),
    )
    try:
        return WallpaperTag.model_validate_json(res.text or "")
    except ValidationError as e:
        logging.warning("native schema mismatch: %s | raw=%r", e.errors()[0]["type"], (res.text or "")[:200])
        return None

スキーマを WallpaperTag.model_json_schema() の1か所から作っているのは、送る形と検証する形を二重に書かないためです。経路を替えても、正とみなす基準は Pydantic の定義ひとつに置いておきます。

なお 2026年11月時点の公式ドキュメントでは、generateContent 側の Structured outputs ページに「Legacy」の表記が付き、Interactions API 側にも構造化出力のページが用意されています。今回は既存コードとの差分を小さくするため generateContent で書きましたが、まったく新しく書く処理なら Interactions API のページも先に読んでおくと迷いが減ると思います。

段階3——環境変数で切り替え、しばらく両方を並べる

いきなり全部を切り替えると、戻したくなったときに胃が痛くなります。私自身は環境変数ひとつで経路を選べるようにし、手元の説明文で両方を並べてから既定値を変えました。

import os
from typing import Optional
 
 
def classify(caption: str) -> Optional[WallpaperTag]:
    backend = os.environ.get("TAG_BACKEND", "compat")
    if backend == "native":
        return classify_native(caption)
    return classify_compat(caption)
 
 
def shadow_compare(captions: list[str]) -> None:
    """同じ入力を両経路に送り、ラベルの食い違いと検証失敗を数える"""
    diff = fail_compat = fail_native = 0
    for c in captions:
        a, b = classify_compat(c), classify_native(c)
        fail_compat += a is None
        fail_native += b is None
        if a and b and a.label != b.label:
            diff += 1
            print(f"DIFF {a.label} / {b.label} : {c[:60]}")
    print(f"件数={len(captions)} 食い違い={diff} 互換の失敗={fail_compat} SDKの失敗={fail_native}")

shadow_compare は本番の経路には入れず、手元で一度流すだけの道具です。私の場合、食い違いが出たのは「夜景の街並み」のように landscape と abstract のどちらとも取れる説明文で、経路の差というより、私の許可値の定義が曖昧だったことのほうが目立ちました。互換レイヤーを疑う前に、自分の定義を疑うべきだったのかもしれません。

経路を替える前に、正とみなす基準をひとつに決めておく。 今回の書き直しで、いちばん効いたのはこの線引きでした。

つまずきやすい点

  • json_object のままスキーマを渡したつもりになる——型の指定だけでは形は伝わりません。json_schema と schema 本体の組で渡してください。
  • Pydantic の Optional を「キーが無くてもよい」と読み違える——Optional は null を許すだけで、キーの欠落とは別です。欠落と null の扱いはスキーマを変えても壊れない構造化出力の設計で詳しく扱っています。
  • enum を書けば許可値以外は来ないと思い込む——形が揃っても値の意味までは保証されません。responseSchema の enum 指定なのに違う文字列が返るときの対処が、そのまま互換経路にも当てはまります。
  • res.text が空のまま検証して例外になる——安全性のフィルタなどで本文が無い応答もあり得ます。上のコードのように空文字を渡して検証に落とし、ログへ回すかたちにしておくと原因を追えます。

JSON の解析そのもので止まる場合は、構造化出力の JSON エラーを直す手順も合わせてご覧ください。

結び

互換レイヤーは、既存のコードを大きく変えずに Gemini を試せる、ありがたい入口です。ただ、入口のまま本番を任せるかどうかは、検証の記録を見てから決めても遅くはありません。

まずは段階1のコードで、互換レイヤーに json_schema を渡したうえで検証の失敗を1日分だけログに残してみていただければと思います。その件数が、SDK を替えるかどうかを決めるいちばん確かな材料になるはずです。

シェア

お読みいただきありがとうございます

Gemini Lab は広告なしで運営しており、サーバー費用などの運営コストはメンバーシップのご支援で賄っています。実装コード・ベンチマーク・本番設計パターンなど、実務でお役立ていただける記事を毎日更新しています。もし読んでよかったと感じていただけましたら、ぜひご覧ください。

  • ✦コピー&ペーストで使える実装コード付き
  • ✦毎日新しい上級ガイドを追加
  • ✦¥580/月 または ¥1,480 の永久アクセス
メンバーシップを見る →

もしこの記事がお役に立ちましたら、チップ(¥150)で応援いただけると大変励みになります。広告なしでの運営を続けるため、皆さまのご支援が大きな力になっています。

関連記事

◈ API / SDK2026-09-02
9月で止まる Gemini の標準 API キーを、CI とサーバーとローカルの三層で棚卸しする
標準 API キーは制限付きのものも含めて9月中に拒否されるようになります。差し替えではなく出どころの移行になるため、コード・デプロイ設定・手元の三層に分けた棚卸しと、両対応クライアントを挟んでから切り替える手順をまとめました。
◈ API / SDK2026-09-01
Gemini の API キーを差し替えても旧キーが勝つ。決め手は読み込み順ではありませんでした
同じ環境変数名を複数の経路が供給していると、キーを差し替えても古い値が残ります。読み込み順を四通り入れ替えた実測と、供給と読み取りを台帳に残す監査シムの実装をまとめました。
◈ API / SDK2026-08-27
Gemini に渡す前に表が壊れる場所は結合セルと2行ヘッダ
スプレッドシートの表をそのまま Gemini に渡すと、結合セルと2行ヘッダで行の所属が失われます。抽出時点で何が消えるかを実測し、平坦化と集計行の検出までを Python で組みます。