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.Json | Newtonsoft.Json | |
|---|---|---|
| זמינות | מובנה ב-.NET Core 3.0+; חבילת NuGet ל-.NET Framework 4.6.2+ | חבילת NuGet ל-.NET Framework ול-.NET |
| Serialize / deserialize | JsonSerializer.Serialize(obj) / Deserialize<T>(json) | JsonConvert.SerializeObject(obj) / DeserializeObject<T>(json) |
| התאמת שמות | רגישה לאותיות גדולות וקטנות כברירת מחדל | לא רגישה לאותיות גדולות וקטנות |
| קריאה בלי טיפוסים | JsonDocument, JsonNode | JObject, 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.