C#でJSONのバックスラッシュが消える問題を完全解説【\が勝手にエスケープされる理由と対処法】

C#でJSONを扱っていると「なぜかバックスラッシュ \ が消える」「意図した文字列と違う値になってしまう」というハマりポイントがとても多いです。特に先頭が \dddd のような値を、そのままプロパティ(例:Student.Id)に保持したいときは、JSON側とC#側の二重のエスケープを正しく理解していないと、延々とハマり続けてしまいます。この記事では、なぜ「\ が消えたように見える」のか、その正体と正しい書き方、そして現場で使える回避テクニックまでまとめて解説します。

目次

現象:JSONのバックスラッシュが「消えた」ように見える

まず、よくある質問のシナリオを整理しておきます。

  • JSON上では "id": "\dddd" のように書いている。
  • C#でデシリアライズして Student.Id に代入したい。
  • しかし、Id の値を確認すると、期待した文字列になっていない(エラーになったり、想定外の文字になったりする)。

ここで重要なのは次の3つの「レイヤー」を混同してしまっていることです。

  1. 実際に保持したい値(.NETオブジェクト内の文字列)
  2. JSON上での表現({ "id": "\\dddd" } など)
  3. C#のソースコード内での文字列リテラル表現(ダブルクォーテーションやバックスラッシュのエスケープ)

この3レイヤーをちゃんと分解して考えると、「\ が消えているように見える」問題はだいたい解決します。

なぜ「\」が消えるのか:二重のエスケープを理解する

JSON側のルール:バックスラッシュは制御用文字

JSONでは、バックスラッシュ \ はエスケープシーケンスの開始文字です。代表的なものは次の通りです。

  • \n:改行
  • \t:タブ
  • \":ダブルクォーテーション
  • \\:バックスラッシュそのもの

つまり、JSONの文字列中で「バックスラッシュ1個」という実体を表現したい場合、必ず \\ と書く必要があります。 実体の \ 1個 → JSONでは \\ というルールです。

C#の通常文字列リテラルのルール:こちらもバックスラッシュはエスケープ文字

さらに、C#の通常の文字列リテラル("...";)でも、バックスラッシュはエスケープ文字です。

  • "\n" は「改行1文字」
  • "\\" は「バックスラッシュ1文字」

ということは、JSONの中に \\ を書きたい(=実体のバックスラッシュ1個を意味する)場合、C#の通常文字列リテラルの中では、さらにバックスラッシュをエスケープしなければなりません。

具体的には次のような対応になります。

レイヤー意味したい実体書き方
.NET文字列の中身\dddd@"\dddd" など
JSON中の表記.NETで \dddd にしたい"\\dddd"
C#通常文字列リテラルJSON中に \\dddd を書きたい"\\\\dddd"

このようにJSONとC#の両方でエスケープが必要なので、「二重のエスケープ」を意識しないと、すぐにどこかが足りない/多い状態になってしまいます。

正しい書き方:Newtonsoft.Json の例

実際のコードで確認してみましょう。まずは Newtonsoft.Json(Json.NET)を使うパターンです。


using Newtonsoft.Json;

public class Student
{
    public string Name { get; set; }
    public string Id { get; set; }
}

// 通常の文字列リテラルを使う場合
string jsonString = "{\"name\": \"user\", \"id\": \"\\\\dddd\"}";
Student stu = JsonConvert.DeserializeObject<Student>(jsonString);

// stu.Id の実体は @"\dddd"

\" は C# の文字列リテラルの中で " を表し、\\\\ が JSON中の \\ になります。最終的に、JSONとしては "id": "\\dddd" となり、デシリアライズ後の stu.Id が \dddd になります。

逐語文字列(@文字列)ならかなり見やすくなる

同じことを C# の逐語文字列(@)で書くと、ぐっと見通しが良くなります。


// 逐語文字列を使う場合
string jsonString = @"{""name"": ""user"", ""id"": ""\\dddd""}";
Student stu = JsonConvert.DeserializeObject<Student>(jsonString);

// どちらの書き方でも stu.Id は @"\dddd"

逐語文字列では バックスラッシュはエスケープ不要なので、JSON側のルールだけ考えて \\ と書けばOKです(その代わり、ダブルクォーテーションは "" と二重にする必要があります)。

System.Text.Json でも考え方はまったく同じ

.NET 標準の System.Text.Json を使う場合も、基本ルールは一切変わりません。


using System.Text.Json;

public class Student
{
    public string Name { get; set; }
    public string Id { get; set; }
}

string jsonString = @"{""name"": ""user"", ""id"": ""\\dddd""}";
Student stu = JsonSerializer.Deserialize<Student>(jsonString);

// stu.Id == @"\dddd"

ライブラリが違っても、JSONの仕様とC#文字列の仕様は同じなので、「実体の \ 1個 → JSONでは \\ → 逐語文字列ではそのまま "\\」 という流れは共通です。

間違った書き方とエラーになるケース

よくある間違いの一つが、次のようなJSONです。


string jsonString = "{ \"id\": \"\dddd\" }"; // × 不正なJSON

ここで JSON 側から見た "\dddd" は、"\d" という存在しないエスケープから始まるため、JSONとして不正になります。多くのJSONパーサはこの時点で例外を投げます。

JSONの仕様上、\ の直後には特定の文字(", \, /, b, f, n, r, t, u)しか許されません。d はその中に含まれていないため、そもそも JSON として成立していないのです。

正しくは次のように、JSON中でバックスラッシュを二重にする必要があります。


// 正しいJSON表現
string jsonString = "{ \"id\": \"\\\\dddd\" }";  // 通常文字列
// または
string jsonString = @"{""id"": ""\\dddd""}";     // 逐語文字列

外部サービスなどから返ってくるJSONがすでに "\dddd" になっている場合は、受け取る側で無理に置換して直すのではなく、可能な限り送信側のロジックを修正してもらうのが安全です。受け取り側で機械的に "\d" を "\\d" に置換するなどの力技を使うと、他の正しいエスケープまで壊してしまうおそれがあります。

C#文字列リテラルごとの早見表

ここまでの内容をまとめて、よく使うパターンの早見表にしておきます。

最終的に欲しい実体(.NETの値)JSON内の表記C#通常文字列でJSONを書く場合C#逐語文字列(@)でJSONを書く場合
\dddd"\\dddd""{\"id\":\"\\\\dddd\"}"@"{""id"": ""\\dddd""}"
C:\temp"C:\\temp""{\"path\":\"C:\\\\temp\"}"@"{""path"": ""C:\\temp""}"
\\server\share(UNCパス)"\\\\server\\share""{\"path\":\"\\\\\\\\server\\\\share\"}"@"{""path"": ""\\\\server\\share""}"

表を見て分かる通り、通常文字列はバックスラッシュ地獄になりやすいので、可能であれば逐語文字列(@)を積極的に使うとよいです。

C# 11 以降の生文字列リテラル(raw string)を使う方法

C# 11 以降では、生文字列リテラル(raw string literal)が使えるようになり、JSONをソースコードにベタ書きするときのストレスがかなり減りました。


// C# 11 の生文字列リテラルの例
string jsonString = """
{
  "name": "user",
  "id": "\\dddd"
}
""";

Student stu = JsonSerializer.Deserialize<Student>(jsonString);

生文字列リテラルでは、バックスラッシュもダブルクォーテーションもそのまま書けるため、「JSONとしてどう書くか」だけ考えればよく、C#側のエスケープをほとんど意識しなくて済みます。 この例では、JSONとして "\\dddd" と書いているので、結果として Id の中身は \dddd になります。

シリアライズ時は「これを覚える」よりライブラリに任せる

ここまでは「JSON文字列を手書きする」前提で説明してきましたが、実務ではなるべくJSON文字列を手書きしないのがベストプラクティスです。

C#のオブジェクトからJSONを作るときは、次のようにライブラリに任せてしまうのが安全です。


var stu = new Student
{
    Name = "user",
    Id = @"\dddd" // 実体の値だけ意識すればよい
};

// Newtonsoft.Json
string jsonNewtonsoft = JsonConvert.SerializeObject(stu);

// System.Text.Json
string jsonSystemText = JsonSerializer.Serialize(stu);

このとき、jsonNewtonsoft や jsonSystemText の中身は自動的に


{"Name":"user","Id":"\\dddd"}

のように正しくエスケープされます。 「実体の値 → JSONの表現」を考えるのはライブラリの仕事なので、開発者は Id = @"\dddd" のように「最終的に欲しい値」だけ設定すればOKです。

外部から来るJSONが不正な場合の対処戦略

現実的には、自分のコードではなく外部サービスや他システムからのJSONが原因で、"\dddd" のような不正な文字列が飛んでくることもあります。その場合の基本戦略は次の通りです。

  1. 送信側に修正を依頼できるなら、まずそれを優先する。
  2. どうしても修正できない場合のみ、受信側で対処する。

受信側で対処せざるを得ない場合でも、機械的な置換は極力避けるべきです。

  • Replace("\\", "\\\\") のような処理は、正しい \n や \\ まで壊す危険がある。
  • 特定のフィールドだけ、必要なパターンだけに限定した修正ロジックを組む方がまだ安全。
  • 本来は「JSON文字列」として壊れているので、事前にバリデーション用のレイヤを用意するのも有効。

一時しのぎの置換がそのまま本番に残ってしまうと、後からバグの温床になります。ログなどで「本来どうあるべきか」を明示しておき、できるだけ早いタイミングで正式な修正に置き換えるようにしましょう。

ログや設定ファイルにバックスラッシュを含めたいときの注意点

バックスラッシュを含む文字列は、ログ出力や設定ファイル(JSON)でもよく使われます。例えば:

  • Windowsパス:C:\log\app
  • UNCパス:\\server\share
  • 正規表現パターン:\d{4} など

これらを JSON 設定ファイルとして管理する場合は、「設定ファイル自体がJSONとして正しいか」 を必ず確認しましょう。


// 設定ファイル (config.json) の一部例
{
  "LogPath": "C:\\log\\app",
  "Regex": "\\d{4}"
}

このファイルを C# から読み込むときは、すでに JSON として正しい形になっているので、通常通りデシリアライズするだけでOKです。


public class AppConfig
{
    public string LogPath { get; set; }
    public string Regex { get; set; }
}

string json = File.ReadAllText("config.json");
var config = JsonSerializer.Deserialize<AppConfig>(json);

// config.LogPath == @"C:\log\app"
// config.Regex   == @"\d{4}"

ここでは C# 側では一切バックスラッシュの数を意識していません。あくまで「JSONの中を正しく書いておく」ことが大事です。

実践テクニック:JSON文字列を手書きしなければならないときのコツ

APIテスト用のサンプルコードなど、どうしても JSON を手書きで埋め込まなければならない場面もあります。そのときのコツをいくつか紹介します。

逐語文字列か生文字列を優先して使う

通常文字列リテラルよりも、逐語文字列(@)か C# 11 の生文字列(raw string)を優先的に使うと、エスケープの混乱をかなり減らせます。


// 逐語文字列
string json1 = @"{""id"": ""\\dddd""}";

// 生文字列 (C# 11+)
string json2 = """
{
  "id": "\\dddd"
}
""";

どちらを選ぶかはプロジェクトのC#バージョンやコーディング規約次第ですが、「JSONとしてどう書くか」さえ守ればよく、C#側でのバックスラッシュ地獄を避けられるのが大きなメリットです。

匿名オブジェクトをシリアライズしてデバッグする

「この JSON、何個バックスラッシュを書けばよかったっけ?」と混乱したときは、次のように匿名オブジェクトをライブラリでシリアライズして答え合わせをするのが簡単です。


var temp = new
{
    id = @"\dddd"
};

string json = JsonSerializer.Serialize(temp);
Console.WriteLine(json); // {"id":"\\dddd"}

これをそのままサンプルJSONとして利用すれば、エスケープの数を自力で数えなくても、ライブラリが出した「正解」をそのまま使うことができます。

トラブルシューティングチェックリスト

最後に、「\ が消えた」「意図した値にならない」と感じたときに確認すべきポイントをチェックリスト形式でまとめておきます。

  • 1. 実際に受け取っているJSONはどうなっているか?
    ログに JSON 生文字列を出力し、\\ の数を確認する。
  • 2. JSONは仕様として正しいか?
    "\d" のような不正なエスケープが含まれていないかチェックする。
  • 3. C#側で余計な置換をしていないか?
    Replace("\\", "\\\\") などを使っていないか確認する。
  • 4. C#の文字列リテラルの種類は適切か?
    通常文字列でバックスラッシュ地獄になっていないか。逐語文字列や生文字列にできないか検討する。
  • 5. シリアライズ/デシリアライズに同じライブラリを使っているか?
    Newtonsoft.Json と System.Text.Json が混在すると、細かい挙動の違いでハマることもある。
  • 6. 最終的に欲しい値を紙に書いて整理したか?
    まず \dddd を書いて、その値をJSONでどう表現するか → C#でどう書くか、という順に整理すると混乱が減る。

まとめ:実体・JSON・C#の3レイヤーを分けて考える

C#でJSONを扱うときに「バックスラッシュが消えた」「エスケープがおかしい」と感じる原因の多くは、次の3レイヤーが頭の中でごちゃ混ぜになっていることにあります。

  • .NETオブジェクトの中身(最終的に欲しい文字列)
  • JSONとしての表現(\\ など)
  • C#ソースコードとしての文字列リテラル表現("\\\\ など)

ポイントをもう一度まとめると、次のようになります。

  • 実体の \ 1個 → JSONでは \\
  • その JSON を C# の通常文字列に書くなら \\\\
  • 逐語文字列(@)や生文字列を使うと、C#側のエスケープをだいぶ意識しなくて済む
  • シリアライズは基本ライブラリに任せる(オブジェクトに実体の値を入れておき、Serialize してしまう)
  • 外部JSONが不正な場合は、送信側修正が本命、受信側の置換は最小限に

一度「実体 → JSON → C#」の変換を頭と手で追いかけてみると、バックスラッシュ問題への苦手意識はかなり薄れるはずです。この記事のサンプルコードや早見表を手元に置きながら試してみて、自分のプロジェクトに合った書き方パターンを固めておくと、今後のデバッグ時間がぐっと減るはずです。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次