Amazonで商品を見るセール会場へ

Googleスプレッドシート│POST通信で書き込む方法

GASのdoPostでJSONを受け取り、指定したシートに記録します。

この例は練習用の記録先を明示し、入力検査とリクエストIDによる再追記防止を行います。

練習コード・入力JSON・文字変換ブックをダウンロード(ZIP)できます。

記事ごとに別のApps Scriptプロジェクトを使い、新規の練習データを対象にしてください。

2026年10月2日の検証記録をもとに、2026年10月23日に行数が不足する場合の追加処理と本文を修正しました。

過去のGAS51項目は関連例を含むローカル模擬検証の合計で、この記事単独の件数ではありません。curlの3項目はローカルHTTP環境での検証です。今回の行数追加と再送・上限の確認もローカルの模擬環境によるもので、Google実環境での成功を示しません。

Googleへの認可・実シートの操作・配備後のHTTP通信は未検証です。

2026年10月28日は、エラー時を含むflushとロック解除の順序を修正し、再送・上限・入力拒否と併せてローカル模擬試験を行いました。

目次

練習シートとスクリプトプロパティ

新規の練習ブックに「practice-log」シートを作り、A1:D1にrequest_id、script、name、dateの順で見出しを入れます。

シートの共有範囲は広げず、実行するアカウントから編集できることを確認します。

プロパティ設定値
SPREADSHEET_ID練習ブックのID
SHEET_NAMEpractice-log
POST_TOKEN自分で用意した練習専用の長いランダムな文字列。公開記事や共有ファイルには載せない

「拡張機能」→「Apps Script」を開き、プロジェクトの設定からスクリプトプロパティを指定します。

以下のコードを保存します。

doGetは動作確認用の応答だけを返し、シートの行やトークンを公開しません。

// この例は、新規の練習シートだけで使います。
function fail_(code) {
  const error = new Error(code);
  error.publicCode = code;
  throw error;
}
function property_(key) {
  const value = PropertiesService.getScriptProperties().getProperty(key);
  if (!value) fail_('missing_configuration');
  return value;
}
function json_(value) {
  return ContentService.createTextOutput(JSON.stringify(value))
    .setMimeType(ContentService.MimeType.JSON);
}
function payload_(event) {
  if (!event || !event.postData || typeof event.postData.contents !== 'string') fail_('missing_body');
  if (!/^application\/json(?:\s*;|$)/i.test(event.postData.type || '')) fail_('content_type');
  const text = event.postData.contents;
  if (!text || text.length > 4096) fail_('body_size');
  let data;
  try { data = JSON.parse(text); } catch (_) { fail_('invalid_json'); }
  const keys = ['token', 'request_id', 'script', 'name', 'date'];
  if (!data || Array.isArray(data) || typeof data !== 'object' ||
      Object.keys(data).length !== keys.length || !keys.every(k => typeof data[k] === 'string')) fail_('schema');
  if (data.token !== property_('POST_TOKEN')) fail_('unauthorized');
  if (!/^[a-zA-Z0-9_-]{8,80}$/.test(data.request_id)) fail_('request_id');
  for (const key of ['script', 'name']) {
    data[key] = data[key].trim();
    if (!data[key] || data[key].length > 120 || /^[=+@-]/.test(data[key]) || /[\r\n\t]/.test(data[key])) fail_('unsafe_text');
  }
  if (!/^\d{4}-\d{2}-\d{2}$/.test(data.date)) fail_('date');
  const date = new Date(data.date + 'T00:00:00Z');
  if (!Number.isFinite(date.getTime()) || date.toISOString().slice(0, 10) !== data.date) fail_('date');
  return {request_id: data.request_id, script: data.script, name: data.name, date: data.date};
}
function logSheet_() {
  const sheet = SpreadsheetApp.openById(property_('SPREADSHEET_ID')).getSheetByName(property_('SHEET_NAME'));
  if (!sheet) fail_('missing_sheet');
  const expected = ['request_id', 'script', 'name', 'date'];
  const headers = sheet.getRange(1, 1, 1, 4).getValues()[0];
  if (!expected.every((x, i) => headers[i] === x)) fail_('headers');
  return sheet;
}
function appendOnce_(data) {
  const lock = LockService.getScriptLock();
  if (!lock.tryLock(5000)) fail_('busy');
  try {
    const sheet = logSheet_();
    const last = sheet.getLastRow();
    if (last > 1001) fail_('practice_limit');
    const rows = last > 1 ? sheet.getRange(2, 1, last - 1, 4).getValues() : [];
    const record = [data.request_id, data.script, data.name, data.date];
    const matches = rows.filter(row => row[0] === data.request_id);
    if (matches.length) {
      if (matches.length !== 1 || !record.every((x, i) => matches[0][i] === x)) fail_('request_id_conflict');
      return {ok: true, status: 'already_recorded', request_id: data.request_id};
    }
    if (last >= 1001) fail_('practice_limit');
    const maxRows = sheet.getMaxRows();
    if (last + 1 > maxRows) sheet.insertRowsAfter(maxRows, last + 1 - maxRows);
    sheet.getRange(last + 1, 1, 1, 4).setNumberFormat('@').setValues([record]);
    return {ok: true, status: 'recorded', request_id: data.request_id};
  } finally {
    try { SpreadsheetApp.flush(); }
    finally { lock.releaseLock(); }
  }
}
function doPost(event) {
  try { return json_(appendOnce_(payload_(event))); }
  catch (error) { return json_({ok: false, error: error.publicCode || 'service_error'}); }
}
function doGet() {
  // トークンやシートの行はGETで返しません。
  return json_({ok: true, service: 'practice-log', read_enabled: false});
}
function demoPost() {
  const data = {token: property_('POST_TOKEN'), request_id: 'practice-20261002-01',
    script: 'demo.jsx', name: 'practice-user', date: '2026-10-02'};
  const response = doPost({postData: {type: 'application/json', contents: JSON.stringify(data)}});
  console.log(response.getContent());
}
スポンサーリンク

エディタで先に1件を記録する

demoPostを選んで実行します。

練習ブックへのアクセス権限を確認して認可すると、request_idがpractice-20261002-01の行を記録します。

再実行時はalready_recordedを返し、同じ内容を増やしません。

この後のrequest.jsonもdemoPostと同じID・内容なので、続けてcurlで送った場合はalready_recordedが正常な結果です。

別の記録を追加する練習では、新しいrequest_idに変更します。通信失敗後の再確認では、IDを変えないでください。

失敗した場合はログのerrorと見出し・設定値を照合します。

curlから送る入力とリダイレクト

HTTPで試す場合は、「デプロイ」→「新しいデプロイ」でウェブアプリを選び、実行するアカウントとアクセスできるユーザーを確認します。

通常のcurlはブラウザーのGoogleログインを引き継ぎません。

サインイン必須の配備ではログイン画面になる場合があり、このコマンドだけでは認証できません。

匿名アクセスが必要な練習でも、共有トークンは必須です。

本番データを含むブックを匿名公開するための設計ではありません。

アクセス条件が合わない場合は設定を広げず、先のエディタ実行までで確認してください。

HTTPによる練習が終わったら、「デプロイを管理」で対象のデプロイをアーカイブします。

request.jsonはUTF-8で保存します。

tokenは自分のPOST_TOKENへ置き換え、実際のトークンを公開したり、URLの引数に入れたりしないでください。

日付はYYYY-MM-DD形式です。

{
  "token": "REPLACE_WITH_YOUR_PRACTICE_TOKEN",
  "request_id": "practice-20261002-01",
  "script": "demo.jsx",
  "name": "practice-user",
  "date": "2026-10-02"
}

Windowsではcurl.exe --versionで利用できるか確認し、request.jsonと同じフォルダで次のコマンドを実行します。

YOUR_PRACTICE_EXEC_URLを、自分の練習用配備の /exec URLへ置き換えます。

macOS・Linuxではcurl.exeをcurlに読み替えます。

curl.exe --location --header "Content-Type: application/json" --data-binary "@request.json" "YOUR_PRACTICE_EXEC_URL"

Content Serviceの応答は別のURLへリダイレクトされるため、--locationを指定します。

--data-binaryで最初のPOSTを指定し、--request POSTは併用しません。

ローカルの302・303応答では、最初のPOSTの後にGETで応答を受け取ることを確認しました。

Googleへの実通信は未検証です。

確認結果と切り分け

条件結果
最初の正しい入力ok:true、status:recorded。データが1行増える
同じIDと同じ内容already_recorded。行は増えない
同じIDで異なる内容request_id_conflict。書き込まない
本文なし・不正JSON・項目違いmissing_body、invalid_json、schema
トークン違いunauthorized。書き込まない
数式のような文字・改行・長すぎる値unsafe_text。練習例の入力条件外として拒否
GETの応答接続確認のみ。シート設定や書き込みの成功は示さない
HTTP200でもok:false失敗。応答本文のerrorを確認する
1000件を超える練習データpractice_limit。部分的な走査で続行しない

LockServiceで同じプロジェクトの同時実行を制御します。

シートへの変更は、ロックを解除する前にSpreadsheetApp.flushで確定させます。

書込みの途中でエラーが出た場合もflushを試み、その処理が失敗してもロックを解除します。

これはロールバックではありません。service_errorが返っても記録済みの可能性があるため、シートのrequest_idと内容を照合してください。

この順序はLock.releaseLockの公式資料に基づきます。

別プロジェクトや手動編集までは防げないため、練習中は他の処理から同じシートを変更しないでください。

通信が途中で切れた場合は、シートのrequest_idを確認してから同じIDで再確認します。

IDを変えると別件として記録されます。

停止と戻し方

この例は定期トリガーを作りません。

HTTPの受付を止めるときは「デプロイを管理」で対象のデプロイをアーカイブし、不要になったPOST_TOKENとローカルのrequest.jsonも削除します。

データを戻す必要がある場合は、変更前のコピーと記録したrequest_idを照合します。

本番の行を一括削除する操作は含めていません。

公式資料

ウェブアプリのイベントと配備、Content Serviceのリダイレクト、curlのリダイレクトとリクエスト方式を参照してください。

関連記事・旧実装の参考資料

旧GitHubコードや外部の参考例は、現在の記事のZIPと異なる場合があります。

今回の練習は本文のコード・セル番地・プロパティを使用してください。配布ZIPは旧版で、行数拡張とエラー時のflush修正が含まれないため、本文のコードを優先します。

スポンサーリンク

よくある質問:GASのdoPostでシートに書き込むときのチェックリスト

doPostは、何をしますか?

GASのdoPostでJSONを受け取り、指定したシートに記録します。記事の例は、練習用の記録先を明示し、入力検査と、リクエストIDによる再追記防止を行います。

同じリクエストを2回送ると、行は増えますか?

増えません。記事の確認結果では、同じIDと同じ内容は「already_recorded」を返し、行は増えません。同じIDで異なる内容は「request_id_conflict」になり、書き込みません。

トークンは、どう扱いますか?

POST_TOKENは、自分で用意した、練習専用の長いランダムな文字列で、公開記事や共有ファイルには載せません。実際のトークンを公開したり、URLの引数に入れたりしないでください。トークンが違う場合は「unauthorized」になり、書き込みません。

curlで送るときの注意点は?

通常のcurlは、ブラウザーのGoogleログインを引き継ぎません。サインイン必須の配備では、ログイン画面になる場合があり、このコマンドだけでは認証できません。Content Serviceの応答は別のURLへリダイレクトされるため、--locationを指定します。

匿名で公開してもよいですか?

本番データを含むブックを、匿名公開するための設計ではありません。匿名アクセスが必要な練習でも、共有トークンは必須です。アクセス条件が合わない場合は、設定を広げず、先のエディタ実行までで確認してください。

練習が終わったあと、配備はどうしますか?

「デプロイを管理」で対象のデプロイをアーカイブします。不要になった練習トークンとrequest.jsonも削除してください。

この記事のコードは、動作確認済みですか?

過去のGAS51項目は関連例を含むローカル模擬検証の合計で、この記事単独の件数ではありません。curlの3項目はローカルHTTP環境での検証です。今回の行数追加と再送・上限の確認もローカルの模擬環境によるもので、Google実環境での成功を示しません。Googleへの認可・実シートの操作・配備後のHTTP通信は、未検証です。

次に学ぶ・作業環境を選ぶ

学習を続けたい方や、作業環境を整えたい方は、目的に合うガイドをご覧ください。

スプレッドシート・GASのおすすめ書籍

作業環境の作り方

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次