JSON.stringify の基本をまず押さえる
JSON.stringify は、「JavaScript の値(オブジェクトや配列など)を JSON 文字列に変換する関数」です。
const obj = { id: 1, name: "山田" };
const json = JSON.stringify(obj);
console.log(json); // {"id":1,"name":"山田"}
JavaScriptこの文字列は、そのまま API で送ったり、ログに出したり、localStorage に保存したりできます。
一見とてもシンプルですが、実務で使うときには「落とし穴」がいくつかあります。
JSON.stringify が「危険」になりやすいポイント
一番分かりやすい危険ポイントは「例外が投げられる」ことです。
特にやばいのが「循環参照」です。
const a = {};
const b = { a };
a.b = b; // a → b → a とぐるぐる参照
JSON.stringify(a); // ここで例外: TypeError: Converting circular structure to JSON
JavaScriptこのようなオブジェクトをそのまま JSON.stringify すると、例外が投げられて処理が止まります。
ログ出力や API 送信の直前でこれが起きると、画面が真っ白になったり、処理全体が落ちたりします。
もう一つのポイントは、「関数や Symbol、undefined などは JSON にできない(あるいは無視される)」ことです。
const obj = {
id: 1,
fn: () => {},
value: undefined,
};
console.log(JSON.stringify(obj)); // {"id":1}
JavaScript「思っていたより情報が落ちている」ことに気づかず、そのまま保存してしまうと、
「復元したときに必要な情報がない」という事態になります。
だからこそ、「安全な JSON stringify」をユーティリティとして用意しておく価値があります。
基本形:例外を飲み込まずに返り値で表現する safeJsonStringify
まずは、「例外で落ちない」ことを最優先にしたラッパーを作ります。
function safeJsonStringify(value) {
try {
return {
ok: true,
value: JSON.stringify(value),
};
} catch (error) {
return {
ok: false,
error,
};
}
}
JavaScript使い方のイメージです。
const a = {};
const b = { a };
a.b = b;
const result = safeJsonStringify(a);
if (result.ok) {
console.log("JSON:", result.value);
} else {
console.error("JSON 変換に失敗しました:", result.error.message);
}
JavaScript重要なのは、「例外を外に飛ばさず、“成功か失敗か”を返している」ことです。
呼び出し側は try/catch を書かずに、「ok を見て分岐する」だけで済みます。
デフォルト文字列を返すバージョン
「失敗したときはエラー情報はいらないから、代わりに決め打ちの文字列を返してほしい」という場面も多いです。
その場合は、次のような関数が便利です。
function safeJsonStringifyOr(value, fallback = 'null') {
try {
return JSON.stringify(value);
} catch {
return fallback;
}
}
JavaScript使い方の例です。
const a = {};
const b = { a };
a.b = b;
const json = safeJsonStringifyOr(a, '"[unserializable]"');
console.log(json); // 失敗しても "[unserializable]" が返る
JavaScriptこれなら、「ログには最低限この文字列を出す」「保存にはこの値を使う」といったフォールバック戦略を簡単に書けます。
循環参照に強い safeJsonStringify(少し踏み込んだ版)
「循環参照があっても、とりあえず落ちずに“それっぽく”文字列化したい」というニーズもよくあります。
その場合は、JSON.stringify の第 2 引数(replacer)を使って、自前で循環検出を入れます。
function safeJsonStringifyWithCircular(value) {
const seen = new WeakSet();
try {
const json = JSON.stringify(
value,
(key, val) => {
if (typeof val === "object" && val !== null) {
if (seen.has(val)) {
return "[Circular]"; // ここで循環を文字列に置き換える
}
seen.add(val);
}
return val;
}
);
return { ok: true, value: json };
} catch (error) {
return { ok: false, error };
}
}
JavaScript使い方です。
const a = {};
const b = { a };
a.b = b;
const result = safeJsonStringifyWithCircular(a);
console.log(result.ok); // true
console.log(result.value); // {"b":{"a":"[Circular]"}}
JavaScriptここでの重要ポイントは二つです。
一つ目は、「WeakSet を使って“すでに見たオブジェクト”を記録している」ことです。
同じオブジェクトが再び出てきたら、「循環している」と判断して "[Circular]" に置き換えています。
二つ目は、「落とさないことを最優先にしている」ことです。
循環部分の情報は完全ではありませんが、「例外で処理が止まる」よりはずっとマシ、という判断です。
ログやデバッグ用途では、このくらいの情報で十分なことが多いです。
機密情報をマスクする「安全さ」も考える
実務で「安全な stringify」と言うとき、もう一つ大事なのが「情報漏えいの観点での安全」です。
例えば、ユーザーのパスワードやトークン、クレジットカード番号などを、そのままログに出してはいけません。
そこで、「特定のキーをマスクする stringify」を用意することもあります。
function safeJsonStringifyWithMask(value, maskKeys = ["password", "token"]) {
const maskSet = new Set(maskKeys);
return safeJsonStringifyWithCircular(
value,
maskSet
);
}
JavaScriptと書きたいところですが、さっきの循環対応版を少し拡張します。
function safeJsonStringifyWithCircularAndMask(value, maskKeys = ["password", "token"]) {
const seen = new WeakSet();
const maskSet = new Set(maskKeys);
try {
const json = JSON.stringify(
value,
(key, val) => {
if (maskSet.has(key)) {
return "***"; // マスク
}
if (typeof val === "object" && val !== null) {
if (seen.has(val)) {
return "[Circular]";
}
seen.add(val);
}
return val;
}
);
return { ok: true, value: json };
} catch (error) {
return { ok: false, error };
}
}
JavaScript使い方の例です。
const user = {
id: 1,
name: "山田",
password: "secret",
token: "abcd-efgh",
};
const result = safeJsonStringifyWithCircularAndMask(user);
console.log(result.value); // {"id":1,"name":"山田","password":"***","token":"***"}
JavaScriptこれで、「落ちない」「循環にも強い」「機密情報もマスクする」という、かなり実務寄りの stringify ができます。
実務での具体的な利用パターン
ログ出力のときに使うパターンが典型的です。
function logDebug(label, payload) {
const result = safeJsonStringifyWithCircularAndMask(payload);
if (result.ok) {
console.debug(label, result.value);
} else {
console.debug(label, "[unserializable]", result.error.message);
}
}
JavaScriptこれを使えば、「どんなオブジェクトが来ても、とりあえずログは出る」「機密情報もマスクされる」「循環参照でも落ちない」という状態を作れます。
また、localStorage や sessionStorage に保存するときにも、安全版を挟むと安心です。
function saveState(key, state) {
const json = safeJsonStringifyOr(state, "null");
localStorage.setItem(key, json);
}
JavaScriptここでは、「どうしても stringify できない状態なら "null" として保存する」という割り切りをしています。
少なくとも、「例外で画面が落ちる」ことはなくなります。
小さな練習で感覚をつかむ
次のような値を用意して、自分で safeJsonStringify と safeJsonStringifyWithCircularAndMask を実装して試してみてください。
const a = {};
const b = { a };
a.b = b;
const samples = [
{ id: 1, name: "山田" },
["a", "b", "c"],
"ただの文字列",
a, // 循環参照あり
{ user: { id: 1, password: "secret" } },
];
JavaScriptそれぞれを安全版 stringify に通して、「落ちないか」「どういう文字列になるか」「マスクは効いているか」を確認してみると、
「素の JSON.stringify」と「安全な JSON stringify」の違いが、かなりクリアに見えてきます。
