Gemini API を本番アプリに組み込んだ翌日、ユーザーから「画面が あい のような英数字記号に化けて読めません」という連絡が入る — そんな経験は、API を一度でも実装したことがある方なら覚えがあるのではないでしょうか。
私自身も Gemini API を使ったチャットアプリを最初にデプロイしたとき、ローカルでは綺麗に表示されていた日本語が、本番のログには \xe3\x81\x82\xe3\x81\x84 のような16進文字列で並んでいて、原因の特定に半日を費やしました。
実は Gemini API そのものは UTF-8 で正しい日本語を返しています。文字化けが起きる場所はほとんどの場合、API レスポンス側ではなく呼び出し側のクライアントコード・ストリーミング処理・出力先にあります。ここではGemini API で日本語レスポンスが文字化けしたときに、最初にチェックすべき4つのポイントを順番に解説します。
チェックポイント1: HTTP レイヤーの Content-Type と charset を確認する
最初に疑うべきは、HTTP レスポンスのデコードを担うクライアントライブラリの挙動です。
Gemini API は application/json; charset=UTF-8 を返しますが、フレームワークによっては charset を見落として ISO-8859-1 でデコードしてしまうケースがあります。Python の requests で発生しやすいパターンです。
import requests
# NG: 化けるパターン
response = requests.post(url, json=payload)
print(response.text) # 'ã\x81\x82ã\x81\x84' のように化けることがある
# OK: 明示的に encoding を指定する
response = requests.post(url, json=payload)
response.encoding = "utf-8"
print(response.text) # 正しく日本語表示されるresponse.text は内部的にエンコーディング判定を行いますが、ヘッダーの優先順位や apparent_encoding の挙動でブレることがあります。確実なのは response.json() を使うか、response.content.decode("utf-8") で明示的にバイト列をデコードする方法です。
JavaScript / Node.js の fetch でも同様の問題が起きます。
// OK: JSON で受け取れば fetch が UTF-8 デコードを担当する
const res = await fetch(url, {
method: "POST",
body: JSON.stringify(payload),
});
const data = await res.json();
console.log(data.candidates[0].content.parts[0].text);
// NG → OK: 自前でバッファ操作する場合は TextDecoder で明示
const buf = await res.arrayBuffer();
const decoded = new TextDecoder("utf-8").decode(buf);ここまで確認しても化けるなら、原因は次のレイヤーにあります。
チェックポイント2: ストリーミングのチャンク境界でマルチバイト文字が切れている
Gemini API のストリーミングレスポンス(generateContentStream)でだけ文字化けが起きる場合、その犯人はほぼ間違いなくチャンク境界です。
UTF-8 では日本語1文字が3バイトで表現されます。SSE(Server-Sent Events)のチャンクは任意のバイト数で区切られるため、「あ」(E3 81 82)の3バイトが2つのチャンクに分かれることがあります。各チャンクを個別に decode("utf-8") すると、不完全なシーケンスでデコードエラーや化け文字 ? が混入します。
# NG: 各チャンクを個別にデコードする実装はバグります
async for chunk in stream:
text = chunk.decode("utf-8") # マルチバイト境界で壊れる
# OK: IncrementalDecoder で状態を保持する
import codecs
decoder = codecs.getincrementaldecoder("utf-8")()
async for chunk in stream:
text = decoder.decode(chunk) # 不完全なバイトはバッファされる
if text:
yield text
# ストリーム終了後に残りをフラッシュする
final = decoder.decode(b"", final=True)公式 SDK(google-generativeai)を使っている場合は SDK 内部で IncrementalDecoder を使っているため、自前でストリームを読んでいる時だけこの問題が起きます。Edge Runtime や Cloudflare Workers で Response.body を直接 getReader() している場合は、特に注意が必要です。
ストリーミング全般のチャンク制御については Gemini API ストリーミングレスポンスのチャンクエラーとUX設計 で詳しく扱っています。
チェックポイント3: 出力先(コンソール・ファイル・DB)の encoding 設定
API から正しく日本語が取れていても、それを書き出す側で化けるパターンも非常に多いです。
特に Windows 環境のコンソール出力は要注意です。Python のデフォルト標準出力は cp932(Shift_JIS)が選ばれることがあり、print(response_text) で UnicodeEncodeError が発生するか、化け文字に置き換えられます。
import sys
import io
# Windows でコンソールに日本語を出すための定石
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8")
print(gemini_response) # 正しく出力される
# ファイル書き出しも同じく encoding を明示する
with open("output.txt", "w", encoding="utf-8") as f:
f.write(gemini_response)データベースに保存する場合は、テーブルの文字コードが utf8mb4 になっているかも必ず確認してください。MySQL の古いデフォルト utf8(実体は3バイトUTF-8)では、絵文字や一部の漢字(𠮷野家の「𠮷」など)を含むレスポンスが切り捨てられて文字化けします。
チェックポイント4: 絵文字とサロゲートペアの取り扱い
Gemini はカジュアルな会話文を生成する際に、絵文字(🎉 や 😊)を自然に挿入することがあります。これらは UTF-16 サロゲートペアで2要素として表現されるため、文字数カウントや切り詰め処理で破綻しやすい部分です。
const text = "おめでとう🎉";
console.log(text.length); // 7 — 絵文字が2文字としてカウントされる
// NG: 後ろから切ると絵文字が割れて化ける
text.slice(0, 6); // "おめでとう\uD83C" のような不正な孤立サロゲート
// OK: Array.from でコードポイント単位に分解してから処理する
const points = Array.from(text);
points.slice(0, 5).join(""); // "おめでとう"私の経験では、Gemini のレスポンスを Web Push 通知のタイトルに使う場面で、200文字制限のために slice(0, 200) をしていたら、ちょうど絵文字の途中で切れて配信エラーになったことがあります。ユーザー向けに表示する文字列を切り詰める時は、必ずコードポイント単位で扱うのが鉄則です。
日本語と英語が混在するレスポンスのコントロールについては、Gemini API の言語制御 — 出力が混在する問題の修正 で別途まとめています。出力品質そのものを底上げしたい方は、Gemini API 日本語タスク実践ガイド も合わせてご覧ください。
まずはこの順番でログに息を吹き込んでみてください
文字化けは原因の切り分けが命です。まずはログ出力を print(repr(response_text)) のように repr で囲んで、実際にどの段階のバイト列・文字列が壊れているかを可視化しましょう。'\xe3\x81\x82' のように生バイトが見えていればデコード前、'あ' のように Unicode コードポイントなら正しくデコードできています。
Gemini API はサーバー側ではきちんと UTF-8 を返してくれます。原因はかならずクライアント側の4つのレイヤー(HTTP デコード・ストリームチャンク・出力 encoding・サロゲートペア)のどこかにあるはずです。今日のうちに repr で1行ログを足しておくと、次に同じ事故が起きた時に5分で犯人を捕まえられるようになります。