Menu

C#のJSON:System.Text.Jsonによるシリアル化と逆シリアル化

C#でSystem.Text.Jsonを使ってJSONを扱う方法を解説します。JsonSerializer.SerializeとDeserialize、camelCaseとインデント付きの出力、JsonPropertyNameやJsonIgnoreなどの属性、文字列としてのenum、JsonDocumentとJsonNodeによるクラスなしのJSONの読み取り、エラー、そしてNewtonsoft.Jsonとの比較を扱います。

JSONは、ほとんどのC#のプログラムがWeb APIとやり取りし、設定を保存し、データを交換する手段です。現代の.NETは System.Text.Json でJSONを読み書きし、これは.NET Core 3.0以降のランタイムの一部なので、NuGetパッケージは不要です。主な入口は静的クラスの JsonSerializer で、オブジェクトをJSONの文字列に変換し、また元に戻します。

System.Text.Json は.NET Core 3.0とそれ以降のすべてのバージョン(.NET 5から.NET 10まで)に組み込まれています。.NET Framework 4.6.2以降や.NET Standard 2.0のプロジェクトでも、NuGetパッケージ System.Text.Json をインストールすれば使えます。このページの例は、出力をコメントに書いた普通のコードとして示しています。.NETのプロジェクトで実行するには、次の using ディレクティブを加えます。

using System.Text.Json;
using System.Text.Json.Serialization;

シリアル化:オブジェクトからJSONへ

JsonSerializer.Serialize は、オブジェクトのすべての公開プロパティを、プロパティの名前をそのまま使って書き出します。

public class Product
{
    public string Name { get; set; }
    public decimal Price { get; set; }
    public List<string> Tags { get; set; } = new List<string>();
    public bool InStock { get; set; }
}

var lamp = new Product { Name = "Desk lamp", Price = 34.90m, Tags = { "home", "light" }, InStock = true };

string json = JsonSerializer.Serialize(lamp);
Console.WriteLine(json);
// {"Name":"Desk lamp","Price":34.90,"Tags":["home","light"],"InStock":true}

コレクションは配列に、文字列のキーを持つ辞書はオブジェクト({"apples":3,"pears":5})になり、null は null のままで、DateTime はISO 8601の文字列("2026-03-01T14:30:00")になります。多くの人がつまずく既定が2つあります。

  • フィールドは飛ばされます。 シリアル化されるのはプロパティだけです。public int X; を持つクラスは、オプションで IncludeFields = true を設定するか、フィールドをプロパティに変えない限り、{} としてシリアル化されます。
  • enumは数値になります。 OrderStatus.Shipped は 1(enumの基になる値)として書き出されます。"Shipped" と書き出すには、JsonStringEnumConverter(後述)を追加します。

逆シリアル化:JSONからオブジェクトへ

JsonSerializer.Deserialize<T> は T を作り、JSONからそのプロパティを設定します。

string json = "{\"Name\":\"Desk lamp\",\"Price\":34.90,\"Tags\":[\"home\",\"light\"]}";
Product p = JsonSerializer.Deserialize<Product>(json);
Console.WriteLine($"{p.Name} {p.Price} {p.Tags.Count}");   // Desk lamp 34.90 2

var many = JsonSerializer.Deserialize<List<Product>>("[{\"Name\":\"A\",\"Price\":1},{\"Name\":\"B\",\"Price\":2.5}]");
Console.WriteLine(many.Count);                               // 2

一致するC#のプロパティのないJSONのプロパティは無視され、一致するJSONのないC#のプロパティは既定値のままです。既定ではどちらもエラーではありません。

ほぼ全員がつまずくルールがあります。名前の照合は大文字と小文字を区別します。ほとんどのWeb APIはcamelCaseを送り、camelCaseはPascalCaseのプロパティに一致しません。

string fromApi = "{\"name\":\"Mug\",\"price\":8.5}";

var a = JsonSerializer.Deserialize<Product>(fromApi);
Console.WriteLine($"[{a.Name}] {a.Price}");    // [] 0: nothing matched, no error

var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
var b = JsonSerializer.Deserialize<Product>(fromApi, options);
Console.WriteLine($"[{b.Name}] {b.Price}");    // [Mug] 8.5

new JsonSerializerOptions(JsonSerializerDefaults.Web) は、ASP.NET Coreが使う設定を与えます。大文字と小文字を区別しない読み取り、camelCaseの書き出し、引用符で囲まれた文字列としての数値の受け入れです。

逆シリアル化には、各値を設定する手段が必要です。公開のsetter、init アクセサー、またはプロパティと引数名が一致するコンストラクターです。この最後のルールのおかげで、レコードはそのまま動きます。

public record Point(int X, int Y);

Point pt = JsonSerializer.Deserialize<Point>("{\"X\":1,\"Y\":2}");
Console.WriteLine(pt);   // Point { X = 1, Y = 2 }

オプション:camelCaseとインデント付きの出力

JsonSerializerOptions は、名前付け、書式などを制御します。1つのインスタンスを作って再利用します。シリアライザーはオプションのインスタンスごとにメタデータをキャッシュするので、呼び出しのたびに新しいオプションを作ると目に見えて遅くなります。

private static readonly JsonSerializerOptions Options = new JsonSerializerOptions
{
    PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
    WriteIndented = true,
};

Console.WriteLine(JsonSerializer.Serialize(lamp, Options));
// {
//   "name": "Desk lamp",
//   "price": 34.90,
//   "tags": [
//     "home",
//     "light"
//   ],
//   "inStock": true
// }

知っておく価値のある他のオプション:nullのプロパティを省く DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull、IncludeFields = true、文字列として書かれた数値を受け付ける NumberHandling、そして手で編集する設定ファイルのための ReadCommentHandling = JsonCommentHandling.Skip と AllowTrailingCommas = true です。.NET 8では、snake_case を使うAPIのための JsonNamingPolicy.SnakeCaseLower が追加されました。

属性:名前の変更、無視、文字列としてのenum

クラスに付ける属性は、プロパティを1つずつ制御し、オプションより優先されます。

public enum OrderStatus { Pending, Shipped }

public class Order
{
    [JsonPropertyName("order_id")]
    public int Id { get; set; }

    public string Customer { get; set; }

    [JsonIgnore]
    public string InternalNote { get; set; }                  // never written or read

    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
    public string Coupon { get; set; }                        // left out when null

    [JsonConverter(typeof(JsonStringEnumConverter))]
    public OrderStatus Status { get; set; }

    public DateTime PlacedAt { get; set; }
}

var order = new Order
{
    Id = 1042, Customer = "Ana", InternalNote = "vip",
    Status = OrderStatus.Shipped, PlacedAt = new DateTime(2026, 3, 1, 14, 30, 0),
};
Console.WriteLine(JsonSerializer.Serialize(order));
// {"order_id":1042,"Customer":"Ana","Status":"Shipped","PlacedAt":"2026-03-01T14:30:00"}

各プロパティに付ける代わりにすべてのenumを文字列として書き出すには、オプションにコンバーターを追加します:options.Converters.Add(new JsonStringEnumConverter());。

クラスなしでJSONを読む:JsonDocumentとJsonNode

大きなレスポンスからいくつかの値だけが必要な場合や、形が変わる場合は、クラスを省きます。JsonDocument は、JsonElement の値からなる読み取り専用のツリーに解析します。

string weather = "{\"city\":\"Lisbon\",\"current\":{\"temp\":21.5,\"conditions\":[\"sunny\",\"windy\"]},\"alerts\":null}";

using (JsonDocument doc = JsonDocument.Parse(weather))
{
    JsonElement root = doc.RootElement;
    Console.WriteLine(root.GetProperty("city").GetString());                        // Lisbon
    Console.WriteLine(root.GetProperty("current").GetProperty("temp").GetDecimal()); // 21.5

    foreach (JsonElement c in root.GetProperty("current").GetProperty("conditions").EnumerateArray())
        Console.WriteLine(c.GetString());                                            // sunny, windy

    Console.WriteLine(root.TryGetProperty("humidity", out _));                       // False
    Console.WriteLine(root.GetProperty("alerts").ValueKind);                         // Null
}

GetProperty は存在しない名前に対して KeyNotFoundException を投げるので、省略可能なフィールドには TryGetProperty を使います。JsonDocument はプールされたメモリを借りるので、using で破棄します。

JSONを変更するには、System.Text.Json.Nodes の JsonNode(.NET 6以降)を使います。インデクサーを持つ変更可能なツリーが得られます。

JsonNode node = JsonNode.Parse(weather);
Console.WriteLine((string)node["city"]);   // Lisbon

node["current"]["temp"] = 23;
node["updated"] = true;
Console.WriteLine(node.ToJsonString());
// {"city":"Lisbon","current":{"temp":23,"conditions":["sunny","windy"]},"alerts":null,"updated":true}

ファイルとストリーム

ディスク上のJSONはファイルの中の文字列なので、ファイルのメソッドはシリアライザーとそのまま組み合わせられます。

File.WriteAllText("settings.json", JsonSerializer.Serialize(settings, Options));
var loaded = JsonSerializer.Deserialize<Settings>(File.ReadAllText("settings.json"), Options);
// Same options both ways: camelCase names written with Options would not match on a default read.

大きなファイルやHTTPの本文には、文字列全体をメモリで組み立てずに済む非同期のストリームのオーバーロードを使います。

await using FileStream stream = File.OpenRead("orders.json");
List<Order> orders = await JsonSerializer.DeserializeAsync<List<Order>>(stream);

ASP.NET Coreや HttpClient では、自分でシリアライザーを呼ぶことはまれです。コントローラーはJSONの本文を自動でバインドし、httpClient.GetFromJsonAsync<Order>(url)(System.Net.Http.Json)はリクエストと逆シリアル化を1回の呼び出しで行います。

エラー

不正なJSONや、プロパティの型に変換できない値は JsonException を投げます。そのメッセージはJSONのパスと位置を示し、たいていそれで問題を見つけられます。

try
{
    JsonSerializer.Deserialize<Product>("{\"Name\": \"Lamp\", \"Price\": \"cheap\"}");
}
catch (JsonException e)
{
    Console.WriteLine(e.Message);
    // The JSON value could not be converted to System.Decimal. Path: $.Price | LineNumber: 0 | BytePositionInLine: 33.
}

JSONのテキストがリテラルの null の場合、Deserialize は(例外ではなく)null を返すので、入力が外部から来るなら結果を確認します。

出力のエスケープされた文字

既定では、シリアライザーはASCII以外の文字と、HTMLの中で安全でない文字をエスケープします。

Console.WriteLine(JsonSerializer.Serialize(new { city = "São Paulo", note = "5 > 3" }));
// {"city":"S\u00E3o Paulo","note":"5 \u003E 3"}

これは有効なJSONで、元のテキストとして読み戻されます。出力をHTMLのページに安全に入れられるようにエスケープしているのです。人が読むファイルには、オプションで Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping(System.Text.Encodings.Web)を設定します。「unsafe」が指すのは、結果をHTMLに埋め込む場合のことだけです。

System.Text.JsonとNewtonsoft.Json

Newtonsoft.Json(Json.NET、JsonConvert クラス)は10年間標準であり、今もどこにでもあります。主な違い:

System.Text.JsonNewtonsoft.Json
利用できる環境.NET Core 3.0以降に組み込み、.NET Framework 4.6.2以降はNuGetパッケージ.NET Frameworkと.NETのNuGetパッケージ
シリアル化 / 逆シリアル化JsonSerializer.Serialize(obj) / Deserialize<T>(json)JsonConvert.SerializeObject(obj) / DeserializeObject<T>(json)
名前の照合既定で大文字と小文字を区別区別しない
型なしの読み取りJsonDocument、JsonNodeJObject、JToken、JSONPathのクエリ
プロパティの名前の変更[JsonPropertyName("x")][JsonProperty("x")]
寛容さ厳格:有効にしない限りコメント、末尾のカンマ、引用符付きの数値は不可既定で寛容
性能高速で割り当てが少なく、トリミングとAOTのためのソース生成がある遅く、リフレクションに基づく

属性は名前も名前空間も異なるので、コードベースの切り替えは、検索と置換の作業に加えて、より厳格な解析のためのテストが必要になります。新しい.NETのコードでは System.Text.Json から始めます。

ソース生成(.NET 6以降)

JsonSerializer は通常、実行時にリフレクションで型を調べます。トリミングされたり事前にコンパイルされたりするアプリ(Native AOT、Blazor WebAssembly)では、代わりにソースジェネレーターがそのコードをコンパイル時に書きます。

[JsonSerializable(typeof(Product))]
internal partial class AppJsonContext : JsonSerializerContext { }

string json = JsonSerializer.Serialize(lamp, AppJsonContext.Default.Product);

よくある間違い

  • 既定のオプションでcamelCaseのJSONをPascalCaseのプロパティに読み込む。 すべてのプロパティが何も言わずに空のままになります。PropertyNameCaseInsensitive か JsonSerializerDefaults.Web を使います。
  • プロパティの代わりに公開フィールドを使う。 IncludeFields を設定しない限りシリアル化されません。
  • setterのないプロパティ。 一致するコンストラクターの引数のない読み取り専用のプロパティは、逆シリアル化で埋められません。
  • 呼び出しごとの新しい JsonSerializerOptions。 1つの静的なインスタンスを再利用します。
  • 浮動小数点のお金。 0.1 + 0.2 の double は 0.30000000000000004 としてシリアル化されます。金額には decimal を使います。

よくある質問

C#でオブジェクトをJSONに変換するには?

System.Text.Json の JsonSerializer.Serialize(obj) を呼びます。.NET Core 3.0以降に組み込まれていて、インストールするパッケージはありません。すべての公開プロパティを書き出します:{"Name":"Desk lamp","Price":34.90}。読みやすい出力には new JsonSerializerOptions { WriteIndented = true } を、camelCaseの名前には PropertyNamingPolicy = JsonNamingPolicy.CamelCase を渡します。

C#でJSONをオブジェクトに変換するには?

var product = JsonSerializer.Deserialize<Product>(json); は Product を作り、一致するJSONの名前から公開の設定可能なプロパティを埋めます。照合は既定で大文字と小文字を区別するので、PropertyNameCaseInsensitive = true か new JsonSerializerOptions(JsonSerializerDefaults.Web) を渡さない限り、camelCaseのJSONではPascalCaseのプロパティは空のままです。不正な形式のJSONや型の違う値は JsonException を投げます。

C#でクラスを作らずにJSONを読むには?

JsonDocument.Parse(json) を使い、RootElement を GetProperty("name")、GetString()、GetInt32()、EnumerateArray() でたどります。読み取り専用で高速で、破棄する必要があります。変更したいJSONには、JsonNode.Parse(json)(.NET 6以降)が変更可能なツリーを与えます。node["city"]、代入、ToJsonString() が使えます。

System.Text.JsonとNewtonsoft.Jsonのどちらを使うべきですか?

.NET Core 3.0以降の新しいコードならSystem.Text.Jsonです。組み込みで、高速で、割り当てが少なく、ASP.NET Coreも既定で使っています。Newtonsoft.Json(Json.NET)は、.NET Frameworkのプロジェクト(System.Text.JsonはNuGetパッケージとしてしか使えない)、その追加機能(JSONPathのクエリ、非常に寛容な解析、TypeNameHandling)に依存するコード、すでにそれの上に作られた大きなコードベースで今もよく使われています。

System.Text.Jsonがéや<のような文字をエスケープするのはなぜですか?

既定のエンコーダーは、ASCII以外の文字とHTMLで問題になる文字(<、>、&、')を \uXXXX としてエスケープし、出力をHTMLに安全に埋め込めるようにしています。それでも有効なJSONで、逆シリアル化すれば同じテキストに戻ります。読みやすい出力にするには、オプションで Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping を設定しますが、JSONをHTMLに書き込まない場合に限ります。

Coddy programming languages illustration

Coddyでコードを学ぼう

始める