JavaScript Tips | 基本・共通ユーティリティ:安全処理 – 安全な JSON stringify

JavaScript JavaScript
スポンサーリンク

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

これを使えば、「どんなオブジェクトが来ても、とりあえずログは出る」「機密情報もマスクされる」「循環参照でも落ちない」という状態を作れます。

また、localStoragesessionStorage に保存するときにも、安全版を挟むと安心です。

function saveState(key, state) {
  const json = safeJsonStringifyOr(state, "null");
  localStorage.setItem(key, json);
}
JavaScript

ここでは、「どうしても stringify できない状態なら "null" として保存する」という割り切りをしています。
少なくとも、「例外で画面が落ちる」ことはなくなります。


小さな練習で感覚をつかむ

次のような値を用意して、自分で safeJsonStringifysafeJsonStringifyWithCircularAndMask を実装して試してみてください。

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」の違いが、かなりクリアに見えてきます。

タイトルとURLをコピーしました