JSON is how most C# programs talk to web APIs, store settings and exchange data. Modern .NET reads and writes it with System.Text.Json, which is part of the runtime from .NET Core 3.0 on: no NuGet package is needed. Its main entry point is the static class JsonSerializer, which turns objects into JSON strings and back.
System.Text.Json is built into .NET Core 3.0 and every later release (.NET 5 through .NET 10). Projects on .NET Framework 4.6.2 and later, or on .NET Standard 2.0, can use it too by installing the System.Text.Json NuGet package. The examples on this page are shown as plain code with the output in comments. Add these using directives to run them in a .NET project:
using System.Text.Json;
using System.Text.Json.Serialization;
Serialize: object to JSON
JsonSerializer.Serialize writes every public property of an object, using the property names as they are:
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}
Collections become arrays, dictionaries with string keys become objects ({"apples":3,"pears":5}), null stays null, and DateTime becomes an ISO 8601 string ("2026-03-01T14:30:00"). Two defaults catch people out:
- Fields are skipped. Only properties are serialized. A class with
public int X;serializes as{}unless you setIncludeFields = truein the options or turn the field into a property. - Enums are numbers.
OrderStatus.Shippedis written as1(the enum's underlying value). AddJsonStringEnumConverter(below) to write"Shipped".
Deserialize: JSON to object
JsonSerializer.Deserialize<T> creates a T and sets its properties from the 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 properties with no matching C# property are ignored, and C# properties with no matching JSON keep their default values. Neither is an error by default.
The rule that trips up nearly everyone: name matching is case-sensitive. Most web APIs send camelCase, and camelCase does not match PascalCase properties:
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) gives the settings ASP.NET Core uses: case-insensitive reading, camelCase writing, and numbers accepted as quoted strings.
Deserialization needs a way to set each value: a public setter, an init accessor, or a constructor whose parameter names match the properties. That last rule is why records work out of the box:
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 and indented output
JsonSerializerOptions controls naming, formatting and more. Create one instance and reuse it: the serializer caches metadata per options instance, so building new options for every call is measurably slower.
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
// }
Other options worth knowing: DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull to leave out null properties, IncludeFields = true, NumberHandling to accept numbers written as strings, and ReadCommentHandling = JsonCommentHandling.Skip plus AllowTrailingCommas = true for hand-edited config files. .NET 8 added JsonNamingPolicy.SnakeCaseLower for APIs that use snake_case.
Attributes: rename, ignore, enums as strings
Attributes on the class control one property at a time and win over the 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"}
To write every enum as a string instead of marking each property, add the converter to the options: options.Converters.Add(new JsonStringEnumConverter());.
Reading JSON without a class: JsonDocument and JsonNode
When you only need a few values from a large response, or its shape varies, skip the class. JsonDocument parses into a read-only tree of JsonElement values:
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 throws KeyNotFoundException for a missing name, so use TryGetProperty for optional fields. JsonDocument rents pooled memory, which is why it is disposed with using.
To modify JSON, use JsonNode from System.Text.Json.Nodes (.NET 6 and later), which gives a mutable tree with indexers:
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}
Files and streams
JSON on disk is a string in a file, so the file methods compose directly with the 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.
For large files and HTTP bodies, the async stream overloads avoid building the whole string in memory:
await using FileStream stream = File.OpenRead("orders.json");
List<Order> orders = await JsonSerializer.DeserializeAsync<List<Order>>(stream);
In ASP.NET Core and with HttpClient, you rarely call the serializer yourself: controllers bind JSON bodies automatically, and httpClient.GetFromJsonAsync<Order>(url) (in System.Net.Http.Json) does the request and the deserialization in one call.
Errors
Invalid JSON, or a value that cannot be converted to the property's type, throws JsonException. Its message names the JSON path and position, which is usually enough to find the problem:
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 returns null (not an exception) when the JSON text is the literal null, so check the result when the input comes from outside.
Escaped characters in the output
By default, the serializer escapes non-ASCII characters and characters that are unsafe inside HTML:
Console.WriteLine(JsonSerializer.Serialize(new { city = "São Paulo", note = "5 > 3" }));
// {"city":"S\u00E3o Paulo","note":"5 \u003E 3"}
This is valid JSON and reads back as the original text; it is escaped so that the output can be dropped into an HTML page safely. For human-readable files, set Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping (from System.Text.Encodings.Web) in the options. The "unsafe" refers only to embedding the result in HTML.
System.Text.Json vs Newtonsoft.Json
Newtonsoft.Json (Json.NET, the JsonConvert class) was the standard for a decade and is still everywhere. The main differences:
| System.Text.Json | Newtonsoft.Json | |
|---|---|---|
| Availability | built into .NET Core 3.0+; NuGet package for .NET Framework 4.6.2+ | NuGet package for .NET Framework and .NET |
| Serialize / deserialize | JsonSerializer.Serialize(obj) / Deserialize<T>(json) | JsonConvert.SerializeObject(obj) / DeserializeObject<T>(json) |
| Name matching | case-sensitive by default | case-insensitive |
| Untyped reading | JsonDocument, JsonNode | JObject, JToken, with JSONPath queries |
| Rename a property | [JsonPropertyName("x")] | [JsonProperty("x")] |
| Leniency | strict: no comments, trailing commas or quoted numbers unless enabled | lenient by default |
| Performance | faster, fewer allocations, source generation for trimming and AOT | slower, reflection-based |
The attributes have different names and namespaces, so switching a codebase is a search-and-replace job plus tests for the stricter parsing. For new .NET code, start with System.Text.Json.
Source generation (.NET 6+)
JsonSerializer normally inspects your types with reflection at run time. For apps trimmed or compiled ahead of time (Native AOT, Blazor WebAssembly), a source generator writes that code at compile time instead:
[JsonSerializable(typeof(Product))]
internal partial class AppJsonContext : JsonSerializerContext { }
string json = JsonSerializer.Serialize(lamp, AppJsonContext.Default.Product);
Common mistakes
- camelCase JSON into PascalCase properties with default options. Every property stays empty, silently. Use
PropertyNameCaseInsensitiveorJsonSerializerDefaults.Web. - Public fields instead of properties. They are not serialized unless
IncludeFieldsis set. - Properties with no setter. A get-only property without a matching constructor parameter is not filled on deserialization.
- A new
JsonSerializerOptionsper call. Reuse one static instance. - Floating-point money. A
doubleof0.1 + 0.2serializes as0.30000000000000004. Usedecimalfor amounts.
Frequently Asked Questions
How do I convert an object to JSON in C#?
Call JsonSerializer.Serialize(obj) from System.Text.Json, which is built into .NET Core 3.0 and later with no package to install. It writes every public property: {"Name":"Desk lamp","Price":34.90}. Pass new JsonSerializerOptions { WriteIndented = true } for readable output and PropertyNamingPolicy = JsonNamingPolicy.CamelCase for camelCase names.
How do I convert JSON to an object in C#?
var product = JsonSerializer.Deserialize<Product>(json); creates a Product and fills its public settable properties from the matching JSON names. Matching is case-sensitive by default, so camelCase JSON leaves PascalCase properties empty unless you pass PropertyNameCaseInsensitive = true or new JsonSerializerOptions(JsonSerializerDefaults.Web). Malformed JSON or a wrong value type throws JsonException.
How do I read JSON without creating a class in C#?
Use JsonDocument.Parse(json) and walk RootElement with GetProperty("name"), GetString(), GetInt32() and EnumerateArray(); it is read-only and fast, and must be disposed. For JSON you want to modify, JsonNode.Parse(json) (.NET 6+) gives a mutable tree: node["city"], assignments, and ToJsonString().
Should I use System.Text.Json or Newtonsoft.Json?
For new code on .NET Core 3.0 or later, System.Text.Json: it is built in, faster, allocates less, and ASP.NET Core uses it by default. Newtonsoft.Json (Json.NET) is still common in .NET Framework projects (where System.Text.Json is available only as a NuGet package), in code that depends on its extra features (JSONPath queries, very lenient parsing, TypeNameHandling), and in large codebases already built on it.
Why does System.Text.Json escape characters like é and <?
The default encoder escapes non-ASCII and HTML-sensitive characters (<, >, &, ') as \uXXXX, so the output is safe to embed in HTML. It is still valid JSON and deserializes back to the same text. For readable output, set Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping in the options, but only when the JSON is not written into HTML.