Google Apps Script (GAS) でWebアプリを作成する際、サーバー側(.gs)のデータをオブジェクトや配列のままクライアント側(.html)に渡したい場面がよくあります。
しかし、安直に JSON.stringify して渡そうとすると、データの中に 日本語、改行コード、ダブルクォーテーション(") などが含まれている場合に、ブラウザ側で SyntaxError や文字化けが発生してハマることがあります。
この記事では、あらゆる文字が含まれていても絶対にエラーにならない、堅牢なデータの受け渡し方法をメモとして残します。
❌ よくある失敗例(アンチパターン)
サーバー側で JSON.stringify した文字列を、そのまま Scriptlet (<?= ... ?>) で HTML に埋め込む方法です。
GAS側 (.gs)
function doGet() {
const data = {
message: 'これは "引用符" です', // ← これが鬼門
note: '改行も\nあります'
};
const template = HtmlService.createTemplateFromFile('index');
template.jsonData = JSON.stringify(data);
return template.evaluate();
}
HTML側 (.html)
<script>
// エラーになる!
const data = JSON.parse('<?!= jsonData ?>');
</script>
発生するエラー
ブラウザのコンソールに以下のようなエラーが出ます。
Uncaught SyntaxError: Bad escaped character in JSON
または
Uncaught SyntaxError: Unexpected token
❌ なぜ普通に埋め込むと失敗するのか?
「日本語や特殊記号が含まれているからエラーになる」と思われがちですが、実は { msg: "Hello" } のような最もシンプルなデータであっても、以下の方法ではエラーになります。
JSON.stringify はデータを {"key":"value"} のような文字列に変換しますが、この中には必ず ダブルクォーテーション(") が含まれます。 これをHTMLやJSのコード内に埋め込むと、HTMLの属性を閉じる " や、JSの文字列を閉じる ' と衝突してしまい、構文が破壊されるためです。
失敗するメカニズム
- データのJSON化
JSON.stringifyは、仕様上必ず キーと値をダブルクォーテーション(") で囲みます。JavaScript// 元データ: { msg: "Hello" } // 変換後: {"msg":"Hello"} - HTMLへの埋め込み(衝突)
これを HTML の属性(valueなど)に埋め込むと、HTMLの属性を囲む"と JSONデータ内の"が衝突します。HTML<input type="hidden" value="<?!= jsonData ?>"> <input type="hidden" value="{"msg":"Hello"}"> - ブラウザの解釈
ブラウザはvalue="{"までを属性値とみなし、そこで属性を閉じてしまいます。 その後に続くmsg":"Hello"}"が「タグ内の不正な文字(ゴミ)」として扱われ、SyntaxErrorやMalformed HTML(形式が正しくないHTML) エラーが発生します。
結論
つまり、データの中身に関わらず、「JSON文字列を生のままHTML属性(valueなど)に埋め込むこと」自体が、構造的に不可能です。
これに加えて、GASのテンプレートエンジン特有の挙動により、改行コードやエスケープ文字(\)が含まれると Bad escaped character エラーも併発するため、問題はより深刻化します。
だからこそ、「引用符も記号も含まない形式(Base64)」 に変換して受け渡す方法が、唯一の安全な解決策となります。
✅ 解決策:Base64エンコード(UTF-8対応)× 隠しフィールド
この問題を解決する鉄板の方法は以下の通りです。
-
サーバー側: データを Base64 にエンコードして渡す。(
"や改行を英数字に変換して無害化する) -
受け渡し:
<script>タグ内ではなく、<input type="hidden">に埋め込む。(JSの構文エラーリスクをゼロにする) -
クライアント側: 受け取ったBase64を、日本語(UTF-8)に対応した方法でデコードする。
1. サーバー側の実装 (.gs)
GAS標準の Utilities を使ってエンコードします。
ポイントは、そのままエンコードするのではなく、一度 UTF-8 のバイト配列 (getBytes()) に変換してから WebSafe な Base64 にすることです。これをしないと日本語が文字化けします。
// ✅ 解決策のサーバー側コード (.gs)
function doGet() {
// 引用符、改行、絵文字を含んだ「壊れやすい」データ
const data = {
id: 101,
name: "山田 太郎",
memo: "ダブルクォート(\")も、\n改行も、絵文字(🎉)も、\n全て文字化けせずに渡せます。"
};
const template = HtmlService.createTemplateFromFile('index');
// 【重要】JSON化 -> UTF-8バイト配列 -> Base64(WebSafe)エンコード
const jsonString = JSON.stringify(data);
const blob = Utilities.newBlob(jsonString);
template.encodedData = Utilities.base64EncodeWebSafe(blob.getBytes());
return template.evaluate()
.addMetaTag('viewport', 'width=device-width, initial-scale=1')
.setSandboxMode(HtmlService.SandboxMode.IFRAME);
}
2. クライアント側の実装 (.html)
HTML側では、隠しフィールドで受け取り、JSでデコードします。 window.atob() は日本語(マルチバイト文字)を直接扱うと文字化けするため、Uint8Array と TextDecoder を組み合わせて復号します。
<!DOCTYPE html>
<html>
<body>
<input type="hidden" id="server-data" value="<?!= encodedData ?>">
<h1>データ受け渡しテスト</h1>
<div id="output"></div>
<script>
document.addEventListener('DOMContentLoaded', () => {
// 1. 隠しフィールドからBase64文字列を取得
const base64String = document.getElementById('server-data').value;
// 2. デコードしてオブジェクトに戻す
const data = decodeBase64Json(base64String);
console.log(data);
document.getElementById('output').innerText = data.memo;
});
/**
* 日本語対応 Base64デコード関数
* @param {string} base64String
* @return {Object|null}
*/
function decodeBase64Json(base64String) {
if (!base64String) return null;
try {
// WebSafe Base64 ('-' -> '+', '_' -> '/') に置換
const base64 = base64String.replace(/-/g, '+').replace(/_/g, '/');
// ブラウザ標準機能でデコード (この時点ではLatin1)
const binaryString = window.atob(base64);
// バイナリ文字列を Uint8Array (バイト配列) に変換
const len = binaryString.length;
const bytes = new Uint8Array(len);
for (let i = 0; i < len; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
// TextDecoder で UTF-8 として文字列化 (これで文字化けしない!)
const decoder = new TextDecoder('utf-8');
const jsonString = decoder.decode(bytes);
return JSON.parse(jsonString);
} catch (e) {
console.error("Base64 Decode Error", e);
return null;
}
}
</script>
</body>
</html>
まとめ
GASからHTMLへ複雑なデータを渡すときは、以下の手順を踏むのが最も確実です。
-
GAS側:
JSON.stringify→Utilities.newBlob().getBytes()→Utilities.base64EncodeWebSafe -
HTML側:
<input type="hidden">で受け取る -
JS側:
atob→Uint8Array→TextDecoder('utf-8')→JSON.parse
この構成にしてからは、Bad escaped character や Unexpected token といったエラーには一切悩まされなくなりました。 日本語や特殊記号を含むデータを扱う際は、ぜひこのパターンを使ってみてください。