GoogleフォームやCRMの問い合わせをスプレッドシートへ転記していると、入力者が書いた文字列が別の値へ解釈されることがあります。
-
=1+1が数式として評価される -
00123が数値123になる -
2026/10/09が日付として扱われる
受付原文を後から確認する用途では、この変換が「入力を保存できた」という成功に見えてしまうのが問題です。
この記事では、Google Apps Script(GAS)から Sheets API の valueInputOption: 'RAW' を使って、書き込み前の文字列・書き込み結果・読み戻した値と数式の有無を検査する実装を作ります。実顧客のフォームには接続せず、架空のテスト用シートで一行を確認するところまでを扱います。
この記事で作るもの
対象は、フォームやCRMの回答をスプレッドシートへ保存するGASを保守している担当者です。次の状態を完成とします。
-
case_idとraw_textの二列へテスト入力を追加できる。 -
RAWと文字列型を指定し、値を解釈させない。 - APIが返した
updatedRangeを使って、同じセルを読み戻す。 - 値の一致、文字列型、数式なしを確認してから後続処理へ渡す。
- 結果不明時に同じ行を盲目的に再追加しない。
受付返信、CRM登録、AIへの送信はこの実装の完了条件に含めません。
RAWだけでは十分でない
Sheets APIの ValueInputOption では、RAWは入力値を解析せず保存し、USER_ENTEREDは画面入力と同じように解釈すると説明されています。
| 指定 | 入力値 | 保存時の扱い | 受付原文での判断 |
|---|---|---|---|
USER_ENTERED |
=1+1 |
数式として解釈され得る | 原文保管には使わない |
RAW |
文字列 =1+1
|
解析せず保存する | 今回の方式 |
RAW |
数値 123
|
数値のまま保存する |
00123には戻せない |
つまり、RAWを付けるだけでなく、書き込み直前に値が文字列であることを検査します。すでに Number() や日付変換を通った値は、RAWでも元には戻りません。
テスト用シートの準備
本番の回答シートとは分けた新しいスプレッドシートを用意します。
- シート名を
受付テストにする。 - A1に
case_id、B1にraw_textと入力する。 - A:Bに集計式や手動の運用列を置かない。
- Apps ScriptをV8ランタイムで開く。
- 「サービス」からGoogle Sheets API v4(識別子
Sheets)を追加する。 - スクリプトプロパティ
TEST_SPREADSHEET_IDにテスト用シートのIDを保存する。
シートIDはURLの /d/ と /edit の間の値です。記事や公開リポジトリへ実環境のID、回答本文、認証情報を貼りません。高度なSheetsサービスと、関連付けたGoogle CloudプロジェクトのSheets APIが有効であることも確認します。
入力を文字列のまま作る
まず外部サービスを呼ばない関数を作ります。trim、Number、日付変換をここへ入れないのがポイントです。
const RAW_TEXT_LIMIT = 1000;
function buildRawRow(caseId, text) {
if (typeof caseId !== 'string' || !/^TEST-\d{3}$/.test(caseId)) {
throw new Error('CASE_ID_INVALID');
}
if (typeof text !== 'string' || text.length === 0 ||
text.length > RAW_TEXT_LIMIT) {
throw new Error('TEXT_INVALID');
}
// 原文を保存する段階では、前後の空白や改行も変更しない。
return [caseId, text];
}
この上限は記事内のサンプル用です。業務の文字数制限やSheetsの最大値を表しません。空白だけの入力を許すかどうかは、原文保存とは別の受付ルールで決めます。
RAWで追加し、返却範囲を読み戻す
次に、APIの返却値から書き込み範囲を取得します。最終行を推測して読み直すと、同時実行や空行の影響を受けます。
function appendRawTest(caseId, text) {
const expected = buildRawRow(caseId, text);
const spreadsheetId = PropertiesService.getScriptProperties()
.getProperty('TEST_SPREADSHEET_ID');
if (!spreadsheetId) throw new Error('TEST_SHEET_ID_REQUIRED');
const result = Sheets.Spreadsheets.Values.append(
{ majorDimension: 'ROWS', values: [expected] },
spreadsheetId,
"'受付テスト'!A:B",
{ valueInputOption: 'RAW', insertDataOption: 'INSERT_ROWS' }
);
const updates = result && result.updates;
if (!updates || !updates.updatedRange ||
updates.updatedRows !== 1 || updates.updatedColumns !== 2) {
throw new Error('WRITE_RESULT_UNCONFIRMED');
}
const written = SpreadsheetApp.openById(spreadsheetId)
.getRange(updates.updatedRange);
const actual = written.getValues();
const formulas = written.getFormulas();
if (formulas.flat().some((formula) => formula !== '')) {
throw new Error('RAW_FORMULA_DETECTED');
}
if (actual.length !== 1 || actual[0].length !== 2 ||
expected.some((value, index) =>
typeof actual[0][index] !== 'string' || actual[0][index] !== value)) {
throw new Error('RAW_READBACK_MISMATCH');
}
return { status: 'RAW_WRITE_VERIFIED', range: updates.updatedRange };
}
INSERT_ROWSは新しい行へ追加する指定で、数式化を防いでいるのは RAW です。updatedRange、updatedRows、updatedColumnsの意味は Values.appendの公式リファレンスで確認できます。
getValues()だけでなくgetFormulas()も確認します。値が一致していても、後続処理が別の式へ差し替えられていれば、原文の保管確認として不十分だからです。
書き込み前のローカルテスト
Googleサービスへ接続しないテストを先に実行します。
function testBuildRawRow() {
const examples = [
'=1+1', '00123', '2026/10/09', '+10', '-10', '@sample',
"'quoted", ' 空白\n改行 ', 'x'.repeat(RAW_TEXT_LIMIT),
];
examples.forEach((text) => {
const row = buildRawRow('TEST-001', text);
if (row.length !== 2 || row[0] !== 'TEST-001' ||
typeof row[1] !== 'string' || row[1] !== text) {
throw new Error('TEST_RAW_TEXT_CHANGED');
}
});
const invalid = [null, 123, false, {}, [], '',
'x'.repeat(RAW_TEXT_LIMIT + 1)];
invalid.forEach((text) => {
let actual = '';
try { buildRawRow('TEST-001', text); }
catch (error) { actual = error.message; }
if (actual !== 'TEXT_INVALID') throw new Error('TEST_REJECTION_FAILED');
});
try { buildRawRow('bad-id', '=1+1'); }
catch (error) {
if (error.message !== 'CASE_ID_INVALID') throw error;
console.log('RAW_ROW_TESTS_PASS');
return;
}
throw new Error('CASE_ID_REJECTION_FAILED');
}
実行ボタンから appendRawTest や onFormSubmit を直接呼んでも、実際のフォーム送信イベントの検証にはなりません。まず testBuildRawRow で入力境界を確認し、シートへ書き込むテストは専用の関数から一回だけ行います。
テスト用シートへ一行書く
次の関数を一度だけ実行します。
function demoRawWrite() {
const result = appendRawTest('TEST-001', '=1+1');
// 本文やシートIDはログへ出さない。
console.log(result.status);
}
RAW_WRITE_VERIFIED が出たら、A列が TEST-001、B列が文字列 =1+1 のままであることをシートでも確認します。数値 2になっていないことに加えて、コードの getFormulas() が空文字を返すことまでが検証結果です。
| 入力 | 期待する保存結果 | 失敗時の扱い |
|---|---|---|
TEST-001 / =1+1
|
文字列のまま、数式なし | 後続処理を保留 |
TEST-002 / 00123
|
先頭のゼロを保持 | 原本と照合 |
TEST-003 / 2026/10/09
|
日付へ変換されない | 転記経路を確認 |
数値 123
|
書き込み前に TEXT_INVALID
|
入力処理を修正 |
| IDプロパティなし | TEST_SHEET_ID_REQUIRED |
設定を確認 |
| 読み戻しの値・型が違う | RAW_READBACK_MISMATCH |
AI入力を止める |
失敗時に再実行しない理由
このサンプルの append は冪等ではありません。同じ caseId で二回実行すると二行追加されます。
特に注意するのは、書き込み自体は成功した後に、読み戻しやネットワークが失敗する場合です。その時点で「保存されていない」と決めつけて再実行すると、重複受付になります。
- 書き込み前の入力エラーなら、入力または設定を修正する。
- APIエラーや
WRITE_RESULT_UNCONFIRMEDなら、テストシートのcase_idと実行履歴を照合する。 - 数式検出や値の不一致なら、その行をAI入力・返信・CRM登録へ進めない。
- 読み取り権限のエラーなら、保存済みの可能性を残して正規の権限を確認する。
- 照合が終わるまで、自動リトライや行の削除はしない。
本番で重複を防ぐには、受付IDの一意制約、処理状態、結果不明時の回収手順を別に設計します。この記事の TEST-001 はその代替ではありません。
RAWで解決しないこと
RAWは保存時の解釈を抑えるための指定です。次の問題は別の工程です。
- すでに数値や日付へ変換された原文の復元
- CSVを別ソフトで開いた時の再解釈
- 実顧客の氏名、連絡先、自由記述をAIへ送ってよいかの判断
- AIが作った返信の正確性、宛先、料金や納期の約束
- 書き込み成功後に誰が承認し、どこで外部送信を止めるか
保管の成功と、AIへの入力許可・顧客への返信許可は分けて記録します。テスト生成のために実顧客の問い合わせや認証情報を外部AIへ渡す必要もありません。
まとめ:受付処理へ組み込む前のチェックリスト
- 本番から分離した二列のテスト表を用意した
- 書き込み直前の値が文字列であることを検査した
-
RAWを指定し、updatedRangeから同じセルを読み戻した - 値の一致、文字列型、数式なしを確認した
- 結果不明時に自動再実行しない回収手順を決めた
- 保管成功とAI入力・外部送信の承認を別の状態にした
まずは =1+1、00123、日付に見える文字列の三つを、架空のテストIDで確認してください。転記処理へ追加する時は、「どこで原文が数値や日付へ変わったか」を一か所だけ追える状態にしてから、次の工程へ進めます。
Miraigentの無料診断を業務整理に使う場合も、「受付原文が転記先で変わった時、誰が原本と照合するか」を確認項目として残せます。
