Menu

JSON ב-C#: Serialize ו-Deserialize עם System.Text.Json

איך עובדים עם JSON ב-C# בעזרת System.Text.Json: JsonSerializer.Serialize ו-Deserialize, פלט ב-camelCase ועם הזחה, attributes כמו JsonPropertyName ו-JsonIgnore, enums כמחרוזות, קריאת JSON בלי מחלקות עם JsonDocument ו-JsonNode, שגיאות, והשוואה ל-Newtonsoft.Json.

JSON הוא הדרך שבה רוב התוכניות ב-C# מדברות עם web APIs, שומרות הגדרות ומחליפות נתונים. .NET מודרני קורא וכותב אותו עם System.Text.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. הדוגמאות בעמוד הזה מוצגות כקוד רגיל עם הפלט בהערות. הוסיפו את הנחיות ה-using האלה כדי להריץ אותן בפרויקט .NET:

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

Serialize: מאובייקט ל-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"). שתי ברירות מחדל מפילות אנשים:

  • שדות מדולגים. רק מאפיינים עוברים serialize. מחלקה עם public int X; הופכת ל-{} אלא אם מגדירים IncludeFields = true באפשרויות או הופכים את השדה למאפיין.
  • enums הם מספרים. OrderStatus.Shipped נכתב כ-1 (הערך הבסיסי של ה-enum). הוסיפו JsonStringEnumConverter (בהמשך) כדי לכתוב "Shipped".

Deserialize: מ-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

מאפייני JSON בלי מאפיין C# תואם נזנחים, ומאפייני C# בלי JSON תואם שומרים על ערכי ברירת המחדל שלהם. אף אחד מהם הוא לא שגיאה כברירת מחדל.

הכלל שמכשיל כמעט את כולם: התאמת השמות רגישה לאותיות גדולות וקטנות. רוב ה-web APIs שולחים 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, ומספרים שמתקבלים גם כמחרוזות במירכאות.

Deserialization צריך דרך להגדיר כל ערך: setter ציבורי, accessor מסוג init, או בנאי ששמות הפרמטרים שלו תואמים למאפיינים. הכלל האחרון הוא הסיבה ש-records עובדים מיד:

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 שולט בשמות, בעיצוב ועוד. צרו מופע אחד ועשו בו שימוש חוזר: ה-serializer שומר מטא-דאטה במטמון לכל מופע של אפשרויות, ולכן בנייה של אפשרויות חדשות בכל קריאה איטית יותר באופן מדיד.

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

אפשרויות נוספות ששווה להכיר: DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull כדי להשמיט מאפיינים שהם null, IncludeFields = true, NumberHandling כדי לקבל מספרים שנכתבו כמחרוזות, ו-ReadCommentHandling = JsonCommentHandling.Skip יחד עם AllowTrailingCommas = true לקבצי הגדרות שנערכים ביד. .NET 8 הוסיף את JsonNamingPolicy.SnakeCaseLower ל-APIs שמשתמשים ב-snake_case.

Attributes: שינוי שם, התעלמות, enums כמחרוזות

Attributes על המחלקה שולטים במאפיין אחד בכל פעם וגוברים על האפשרויות:

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 כמחרוזת במקום לסמן כל מאפיין, הוסיפו את ה-converter לאפשרויות: 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 שוכר זיכרון ממאגר (pool), ולכן משחררים אותו עם using.

כדי לשנות JSON, השתמשו ב-JsonNode מ-System.Text.Json.Nodes (.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}

קבצים ו-streams

JSON על הדיסק הוא מחרוזת בקובץ, ולכן מתודות הקבצים משתלבות ישירות עם ה-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.

לקבצים גדולים ולגופי HTTP, ההעמסות האסינכרוניות שעובדות עם streams חוסכות את בניית המחרוזת כולה בזיכרון:

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

ב-ASP.NET Core ועם HttpClient, רק לעיתים רחוקות קוראים ל-serializer בעצמכם: controllers מקשרים גופי JSON אוטומטית, ו-httpClient.GetFromJsonAsync<Order>(url) (ב-System.Net.Http.Json) מבצע את הבקשה ואת ה-deserialization בקריאה אחת.

שגיאות

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

Deserialize מחזיר null (ולא חריגה) כשטקסט ה-JSON הוא הליטרל null, ולכן בדקו את התוצאה כשהקלט מגיע מבחוץ.

תווים עם escape בפלט

כברירת מחדל, ה-serializer מבצע escape לתווים שאינם ASCII ולתווים שאינם בטוחים בתוך HTML:

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

זה JSON תקין, והוא נקרא בחזרה כטקסט המקורי; ה-escape קיים כדי שאפשר יהיה להכניס את הפלט לעמוד HTML בבטחה. לקבצים שבני אדם קוראים, הגדירו באפשרויות Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping (מ-System.Text.Encodings.Web). ה-"unsafe" מתייחס רק לשילוב התוצאה בתוך HTML.

System.Text.Json מול Newtonsoft.Json

Newtonsoft.Json (Json.NET, המחלקה JsonConvert) היה הסטנדרט במשך עשור ועדיין נמצא בכל מקום. ההבדלים העיקריים:

System.Text.JsonNewtonsoft.Json
זמינותמובנה ב-.NET Core 3.0+; חבילת NuGet ל-.NET Framework 4.6.2+חבילת NuGet ל-.NET Framework ול-.NET
Serialize / deserializeJsonSerializer.Serialize(obj) / Deserialize<T>(json)JsonConvert.SerializeObject(obj) / DeserializeObject<T>(json)
התאמת שמותרגישה לאותיות גדולות וקטנות כברירת מחדללא רגישה לאותיות גדולות וקטנות
קריאה בלי טיפוסיםJsonDocument, JsonNodeJObject, JToken, עם שאילתות JSONPath
שינוי שם של מאפיין[JsonPropertyName("x")][JsonProperty("x")]
סלחנותקפדני: בלי הערות, פסיקים בסוף או מספרים במירכאות אלא אם מפעיליםסלחני כברירת מחדל
ביצועיםמהיר יותר, פחות הקצאות, source generation ל-trimming ול-AOTאיטי יותר, מבוסס reflection

ל-attributes יש שמות ו-namespaces שונים, ולכן מעבר של בסיס קוד הוא עבודה של חיפוש והחלפה, ועוד בדיקות לפענוח הקפדני יותר. לקוד .NET חדש, התחילו עם System.Text.Json.

Source generation (.NET 6+)

JsonSerializer בדרך כלל בוחן את הטיפוסים שלכם עם reflection בזמן ריצה. לאפליקציות שעוברות trimming או קומפילציה מראש (Native AOT, Blazor WebAssembly), source generator כותב את הקוד הזה בזמן קומפילציה במקום:

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

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

טעויות נפוצות

  • JSON ב-camelCase לתוך מאפייני PascalCase עם אפשרויות ברירת המחדל. כל מאפיין נשאר ריק, בשקט. השתמשו ב-PropertyNameCaseInsensitive או ב-JsonSerializerDefaults.Web.
  • שדות ציבוריים במקום מאפיינים. הם לא עוברים serialize אלא אם IncludeFields מוגדר.
  • מאפיינים בלי setter. מאפיין עם get בלבד, בלי פרמטר בנאי תואם, לא מתמלא ב-deserialization.
  • JsonSerializerOptions חדש בכל קריאה. עשו שימוש חוזר במופע סטטי אחד.
  • כסף בנקודה צפה. double של 0.1 + 0.2 עובר serialize כ-0.30000000000000004. השתמשו ב-decimal לסכומים.

שאלות נפוצות

איך ממירים אובייקט ל-JSON ב-C#?

קראו ל-JsonSerializer.Serialize(obj) מ-System.Text.Json, שמובנה ב-.NET Core 3.0 ואילך בלי חבילה להתקין. הוא כותב כל מאפיין ציבורי: {"Name":"Desk lamp","Price":34.90}. העבירו new JsonSerializerOptions { WriteIndented = true } לפלט קריא ו-PropertyNamingPolicy = JsonNamingPolicy.CamelCase לשמות ב-camelCase.

איך ממירים JSON לאובייקט ב-C#?

var product = JsonSerializer.Deserialize<Product>(json); יוצר Product וממלא את המאפיינים הציבוריים שאפשר להשים להם מהשמות התואמים ב-JSON. ההתאמה רגישה לאותיות גדולות וקטנות כברירת מחדל, ולכן JSON ב-camelCase משאיר מאפייני PascalCase ריקים, אלא אם מעבירים PropertyNameCaseInsensitive = true או new JsonSerializerOptions(JsonSerializerDefaults.Web). JSON פגום או ערך מטיפוס שגוי זורקים JsonException.

איך קוראים JSON בלי ליצור מחלקה ב-C#?

השתמשו ב-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 מבצע escape לתווים כמו é ו-<?

המקודד שמוגדר כברירת מחדל מבצע escape לתווים שאינם ASCII ולתווים רגישים ב-HTML (<, >, &, ') כ-\uXXXX, כדי שיהיה בטוח לשלב את הפלט ב-HTML. זה עדיין JSON תקין, והוא עובר deserialize בחזרה לאותו טקסט. לפלט קריא, הגדירו Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping באפשרויות, אבל רק כשה-JSON לא נכתב לתוך HTML.

איור של שפות התכנות ב-Coddy

ללמוד תכנות עם Coddy

להתחיל