Menu

JSON trong C#: serialize và deserialize với System.Text.Json

Cách làm việc với JSON trong C# bằng System.Text.Json: JsonSerializer.Serialize và Deserialize, đầu ra camelCase và thụt lề, các attribute như JsonPropertyName và JsonIgnore, enum dạng chuỗi, đọc JSON không cần lớp qua JsonDocument và JsonNode, xử lý lỗi, và so sánh với Newtonsoft.Json.

JSON là cách hầu hết chương trình C# giao tiếp với web API, lưu cài đặt và trao đổi dữ liệu. .NET hiện đại đọc và ghi JSON bằng System.Text.Json, một phần của runtime từ .NET Core 3.0: không cần package NuGet nào. Điểm vào chính là lớp tĩnh JsonSerializer, chuyển đối tượng thành chuỗi JSON và ngược lại.

System.Text.Json có sẵn trong .NET Core 3.0 và mọi phiên bản sau đó (.NET 5 đến .NET 10). Dự án trên .NET Framework 4.6.2 trở lên, hoặc trên .NET Standard 2.0, cũng dùng được bằng cách cài package NuGet System.Text.Json. Các ví dụ trên trang này là code thường, với đầu ra ghi trong chú thích. Thêm các chỉ thị using sau để chạy chúng trong một dự án .NET:

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

Serialize: từ đối tượng sang JSON

JsonSerializer.Serialize ghi mọi thuộc tính public của một đối tượng, giữ nguyên tên thuộc tính:

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}

Collection trở thành mảng, dictionary có khóa kiểu chuỗi trở thành đối tượng ({"apples":3,"pears":5}), null vẫn là null, còn DateTime trở thành chuỗi ISO 8601 ("2026-03-01T14:30:00"). Có hai giá trị mặc định hay khiến người ta bất ngờ:

  • Field bị bỏ qua. Chỉ thuộc tính được serialize. Một lớp có public int X; sẽ serialize thành {}, trừ khi bạn đặt IncludeFields = true trong options hoặc đổi field đó thành thuộc tính.
  • Enum là số. OrderStatus.Shipped được ghi thành 1 (giá trị gốc của enum). Thêm JsonStringEnumConverter (bên dưới) để ghi "Shipped".

Deserialize: từ JSON sang đối tượng

JsonSerializer.Deserialize<T> tạo một T và gán các thuộc tính của nó 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

Thuộc tính JSON không có thuộc tính C# tương ứng sẽ bị bỏ qua, và thuộc tính C# không có giá trị JSON tương ứng giữ giá trị mặc định. Mặc định cả hai trường hợp đều không phải lỗi.

Quy tắc khiến gần như ai cũng vấp: so khớp tên có phân biệt chữ hoa chữ thường. Phần lớn web API gửi camelCase, và camelCase không khớp với thuộc tính 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) cho bộ cài đặt mà ASP.NET Core dùng: đọc không phân biệt hoa thường, ghi camelCase, và chấp nhận số được viết trong dấu ngoặc kép.

Deserialize cần một cách để gán từng giá trị: setter public, accessor init, hoặc hàm khởi tạo có tên tham số khớp với tên thuộc tính. Quy tắc cuối cùng là lý do record dùng được ngay:

public record Point(int X, int Y);

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

Options: camelCase và đầu ra thụt lề

JsonSerializerOptions điều khiển cách đặt tên, định dạng và nhiều thứ khác. Hãy tạo một instance rồi dùng lại: serializer lưu cache metadata theo từng instance options, nên tạo options mới cho mỗi lần gọi sẽ chậm hơn thấy rõ.

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
// }

Vài option khác nên biết: DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull để bỏ các thuộc tính null, IncludeFields = true, NumberHandling để chấp nhận số viết dạng chuỗi, và ReadCommentHandling = JsonCommentHandling.Skip cùng AllowTrailingCommas = true cho các tệp cấu hình sửa bằng tay. .NET 8 bổ sung JsonNamingPolicy.SnakeCaseLower cho các API dùng snake_case.

Attribute: đổi tên, bỏ qua, enum dạng chuỗi

Attribute đặt trên lớp điều khiển từng thuộc tính một và được ưu tiên hơn options:

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"}

Để ghi mọi enum thành chuỗi thay vì đánh dấu từng thuộc tính, hãy thêm converter vào options: options.Converters.Add(new JsonStringEnumConverter());.

Đọc JSON không cần lớp: JsonDocument và JsonNode

Khi bạn chỉ cần vài giá trị từ một response lớn, hoặc cấu trúc của nó thay đổi, hãy bỏ qua việc tạo lớp. JsonDocument phân tích JSON thành một cây chỉ đọc gồm các giá trị 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 ném KeyNotFoundException khi thiếu tên, nên hãy dùng TryGetProperty cho các trường không bắt buộc. JsonDocument mượn bộ nhớ từ pool, đó là lý do nó được dispose bằng using.

Để sửa JSON, hãy dùng JsonNode trong System.Text.Json.Nodes (.NET 6 trở lên), một cây có thể thay đổi với indexer:

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}

Tệp và stream

JSON trên đĩa là một chuỗi trong tệp, nên các phương thức đọc ghi tệp kết hợp trực tiếp với serializer:

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.

Với tệp lớn và body HTTP, các overload bất đồng bộ dùng stream giúp tránh dựng cả chuỗi trong bộ nhớ:

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

Trong ASP.NET Core và với HttpClient, bạn hiếm khi tự gọi serializer: controller tự bind body JSON, và httpClient.GetFromJsonAsync<Order>(url) (trong System.Net.Http.Json) gửi request và deserialize chỉ trong một lần gọi.

Lỗi

JSON không hợp lệ, hoặc một giá trị không chuyển được sang kiểu của thuộc tính, sẽ ném JsonException. Thông báo lỗi nêu đường dẫn JSON và vị trí, thường là đủ để tìm ra vấn đề:

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.
}

Deserialize trả về null (không ném ngoại lệ) khi văn bản JSON chính là literal null, nên hãy kiểm tra kết quả khi đầu vào đến từ bên ngoài.

Ký tự bị escape trong đầu ra

Mặc định, serializer escape các ký tự không phải ASCII và những ký tự không an toàn bên trong HTML:

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

Đây vẫn là JSON hợp lệ và đọc lại đúng văn bản gốc; nó được escape để đầu ra có thể chèn vào trang HTML một cách an toàn. Với tệp cho người đọc, đặt Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping (trong System.Text.Encodings.Web) trong options. Chữ "unsafe" chỉ nói về việc nhúng kết quả vào HTML.

System.Text.Json và Newtonsoft.Json

Newtonsoft.Json (Json.NET, lớp JsonConvert) là chuẩn mực suốt một thập kỷ và vẫn có mặt ở khắp nơi. Những khác biệt chính:

System.Text.JsonNewtonsoft.Json
Có sẵntích hợp trong .NET Core 3.0+; package NuGet cho .NET Framework 4.6.2+package NuGet cho .NET Framework và .NET
Serialize / deserializeJsonSerializer.Serialize(obj) / Deserialize<T>(json)JsonConvert.SerializeObject(obj) / DeserializeObject<T>(json)
So khớp tênmặc định phân biệt hoa thườngkhông phân biệt hoa thường
Đọc không cần kiểuJsonDocument, JsonNodeJObject, JToken, có truy vấn JSONPath
Đổi tên thuộc tính[JsonPropertyName("x")][JsonProperty("x")]
Độ dễ dãichặt chẽ: không chú thích, không dấu phẩy thừa, không số trong ngoặc kép trừ khi bậtmặc định dễ dãi
Hiệu năngnhanh hơn, ít cấp phát hơn, có source generation cho trimming và AOTchậm hơn, dựa trên reflection

Các attribute có tên và namespace khác nhau, nên chuyển một codebase là việc tìm và thay thế, cộng thêm test cho phần phân tích chặt chẽ hơn. Với code .NET mới, hãy bắt đầu bằng System.Text.Json.

Source generation (.NET 6+)

JsonSerializer thường dùng reflection để xem xét các kiểu của bạn lúc chạy. Với ứng dụng được trim hoặc biên dịch trước (Native AOT, Blazor WebAssembly), một source generator sẽ viết phần code đó ngay lúc biên dịch:

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

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

Lỗi thường gặp

  • JSON camelCase vào thuộc tính PascalCase với options mặc định. Mọi thuộc tính đều trống mà không báo gì. Dùng PropertyNameCaseInsensitive hoặc JsonSerializerDefaults.Web.
  • Field public thay vì thuộc tính. Chúng không được serialize trừ khi đặt IncludeFields.
  • Thuộc tính không có setter. Thuộc tính chỉ có get mà không có tham số hàm khởi tạo tương ứng sẽ không được điền khi deserialize.
  • Tạo JsonSerializerOptions mới cho mỗi lần gọi. Hãy dùng lại một instance tĩnh.
  • Dùng số thực cho tiền. Một double bằng 0.1 + 0.2 được serialize thành 0.30000000000000004. Dùng decimal cho số tiền.

Câu hỏi thường gặp

Làm sao chuyển một đối tượng sang JSON trong C#?

Gọi JsonSerializer.Serialize(obj) trong System.Text.Json, thư viện có sẵn từ .NET Core 3.0 trở đi, không cần cài package nào. Phương thức này ghi mọi thuộc tính public: {"Name":"Desk lamp","Price":34.90}. Truyền new JsonSerializerOptions { WriteIndented = true } để có đầu ra dễ đọc và PropertyNamingPolicy = JsonNamingPolicy.CamelCase để dùng tên camelCase.

Làm sao chuyển JSON thành đối tượng trong C#?

var product = JsonSerializer.Deserialize<Product>(json); tạo một Product và điền các thuộc tính public có setter từ những tên JSON khớp với chúng. Mặc định việc so khớp phân biệt chữ hoa chữ thường, nên JSON camelCase sẽ để trống các thuộc tính PascalCase, trừ khi bạn truyền PropertyNameCaseInsensitive = true hoặc new JsonSerializerOptions(JsonSerializerDefaults.Web). JSON sai định dạng hoặc giá trị sai kiểu sẽ ném JsonException.

Làm sao đọc JSON mà không cần tạo lớp trong C#?

Dùng JsonDocument.Parse(json) rồi duyệt RootElement bằng GetProperty("name"), GetString(), GetInt32() và EnumerateArray(); cách này chỉ đọc, nhanh, và phải được dispose. Với JSON bạn muốn sửa, JsonNode.Parse(json) (.NET 6+) cho một cây có thể thay đổi: node["city"], phép gán và ToJsonString().

Nên dùng System.Text.Json hay Newtonsoft.Json?

Với code mới trên .NET Core 3.0 trở lên, hãy chọn System.Text.Json: thư viện có sẵn, nhanh hơn, cấp phát ít bộ nhớ hơn, và ASP.NET Core dùng nó mặc định. Newtonsoft.Json (Json.NET) vẫn phổ biến trong các dự án .NET Framework (nơi System.Text.Json chỉ có dưới dạng package NuGet), trong code phụ thuộc vào các tính năng riêng của nó (truy vấn JSONPath, phân tích rất dễ dãi, TypeNameHandling), và trong những codebase lớn đã xây trên nó.

Tại sao System.Text.Json escape các ký tự như é và <?

Bộ mã hóa mặc định escape ký tự không phải ASCII và các ký tự nhạy cảm với HTML (<, >, &, ') thành \uXXXX, để đầu ra an toàn khi nhúng vào HTML. Kết quả vẫn là JSON hợp lệ và deserialize lại đúng văn bản ban đầu. Để có đầu ra dễ đọc, đặt Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping trong options, nhưng chỉ khi JSON không được ghi vào HTML.

Hình minh họa các ngôn ngữ lập trình của Coddy

Học lập trình cùng Coddy

Bắt đầu