Working With JSON in C#: System.Text.Json Explained

Since .NET Core 3.0, C# has shipped with System.Text.Json as its built-in JSON library – no NuGet package required for the core functionality, unlike the earlier era where Newtonsoft.Json (Json.NET) was the de facto standard. System.Text.Json is fast and fully built into the framework, but its defaults differ from Json.NET’s in ways that surprise developers migrating between the two. This guide covers the essentials plus the specific default behaviors worth knowing before they cause confusion.

Every example below was compiled and run against .NET 8 to confirm actual output.

Deserializing JSON Into a Class

JsonSerializer.Deserialize<T>() converts a JSON string into an instance of a C# class:

using System.Text.Json;

class Product {
    public string? Name { get; set; }
    public double Price { get; set; }
    public bool InStock { get; set; }
}

string json = "{\"name\":\"Widget\",\"price\":9.99,\"inStock\":true}";
Product? p = JsonSerializer.Deserialize<Product>(json);

Pitfall #1: Property Matching Is Case-Sensitive by Default

This is the single most common surprise for developers new to System.Text.Json. Unlike some other JSON libraries, property name matching is case-sensitive by default — a JSON key that only differs in casing from your C# property won’t match:

string lowerJson = "{\"name\":\"Widget\",\"price\":9.99,\"instock\":true}"; // note: "instock", not "inStock"
Product? p3 = JsonSerializer.Deserialize<Product>(lowerJson);
Console.WriteLine(p3?.InStock); // False — silently defaults, doesn't throw

Notice this doesn’t throw an error — InStock just silently falls back to its default value (false) because the casing didn’t match. This is a genuinely dangerous default because it fails silently rather than loudly. The fix is to explicitly opt into case-insensitive matching:

var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
Product? p = JsonSerializer.Deserialize<Product>(json, options);

Given how often JSON APIs use camelCase while C# convention is PascalCase, setting PropertyNameCaseInsensitive = true is worth treating as a near-default for any code consuming external JSON, not an edge-case setting.

JSON in C#

Pitfall #2: Serialized Output Uses PascalCase by Default

The reverse direction has a related surprise. When you serialize a C# object, property names come out in PascalCase by default, matching your C# property names exactly — not the camelCase convention most JSON APIs use:

var p2 = new Product { Name = "Gadget", Price = 19.99, InStock = false };
Console.WriteLine(JsonSerializer.Serialize(p2));
// {"Name":"Gadget","Price":19.99,"InStock":false}

If you need camelCase output — which is the more common convention for JSON consumed by JavaScript frontends or external APIs — set a naming policy explicitly:

var camelOptions = new JsonSerializerOptions {
    PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
Console.WriteLine(JsonSerializer.Serialize(p2, camelOptions));
// {"name":"Gadget","price":19.99,"inStock":false}

Pretty-Printing Output

For indented, readable JSON rather than a single compact line, set WriteIndented:

var prettyOptions = new JsonSerializerOptions { WriteIndented = true };
Console.WriteLine(JsonSerializer.Serialize(p2, prettyOptions));
{
  "Name": "Gadget",
  "Price": 19.99,
  "InStock": false
}

Handling Invalid JSON

Invalid JSON throws a JsonException, with a message that includes the exact byte position and line number:

string bad = "{\"name\": \"Widget\", \"price\": 9.99,}"; // trailing comma

try {
    JsonSerializer.Deserialize<Product>(bad);
} catch (JsonException e) {
    Console.WriteLine(e.Message);
}

Output:

The JSON object contains a trailing comma at the end which is not supported in this mode. Change the reader options. Path: $ | LineNumber: 0 | BytePositionInLine: 33.

Notably, System.Text.Json‘s error message is unusually descriptive compared to many JSON libraries — it names the specific problem (a trailing comma) rather than a generic “unexpected token” message. That’s genuinely more actionable than what many other languages’ JSON errors provide, though it still points to a byte position rather than the line/column format you’d see in a browser-based tool.

Unknown Properties: Silently Ignored by Default

This is worth knowing if you’re coming from a stricter library. By default, System.Text.Json silently ignores any JSON property that doesn’t match a property on your target class — no error, no warning:

string extraFieldJson = "{\"name\":\"Widget\",\"price\":9.99,\"inStock\":true,\"extra\":\"oops\"}";
Product? p4 = JsonSerializer.Deserialize<Product>(extraFieldJson, options);
Console.WriteLine(p4?.Name); // Widget — no error, "extra" is just dropped

This is the same lenient default that Gson uses in Java, and the opposite of Jackson’s default strict behavior, which throws on unrecognized fields. If you’ve read our comparison of Jackson and Gson in Java, this is the identical trade-off showing up again in a completely different language — lenient parsing tolerates upstream JSON changes gracefully, but can also mask a real mismatch, like a renamed field going unnoticed. System.Text.Json doesn’t currently offer a simple built-in flag to flip this to strict mode the way Jackson does; catching unexpected extra fields requires custom validation logic if you need it.

Working With JSON Without a Predefined Class

For cases where you don’t have (or don’t want) a class matching the JSON’s shape, JsonDocument provides tree-style navigation:

using JsonDocument doc = JsonDocument.Parse(
    "{\"user\":{\"name\":\"Alex\",\"tags\":[\"admin\",\"active\"]}}"
);
JsonElement root = doc.RootElement;

string name = root.GetProperty("user").GetProperty("name").GetString()!;
string firstTag = root.GetProperty("user").GetProperty("tags")[0].GetString()!;

The using keyword matters here — JsonDocument implements IDisposable and holds pooled memory internally, so disposing it properly (either via using or an explicit .Dispose() call) avoids unnecessary memory pressure in code that parses a lot of JSON.

A Quick Way to Check Your JSON First

Several of the behaviors above — case mismatches, unknown fields, trailing commas — are much easier to catch by inspecting your JSON directly before it reaches your C# code, rather than debugging a silently-wrong default value after the fact. Running suspicious JSON through our JSON Formatter and Validator first shows you the exact structure and flags syntax errors immediately, which is often faster than adding debug output to a C# deserialization call to figure out why a property came back empty.

A Note on Reusing JsonSerializerOptions

One easy-to-miss performance detail: creating a new JsonSerializerOptions instance on every call is wasteful, since System.Text.Json caches metadata internally per options instance. If your application deserializes JSON frequently — in a web API handler, for example — construct your options once and reuse the same instance throughout your application, rather than creating a fresh one inline each time:

// Do this once, e.g. as a static readonly field
private static readonly JsonSerializerOptions Options = new() {
    PropertyNameCaseInsensitive = true,
    WriteIndented = false
};

// Reuse it everywhere
var result = JsonSerializer.Deserialize<Product>(json, Options);

This is a small change, but in high-throughput code paths (API endpoints handling many requests per second), reusing options instances measurably reduces overhead compared to constructing new ones repeatedly.

Frequently Asked Questions

For most new .NET projects, System.Text.Json is the better default choice since it’s built into the framework, generally faster, and actively maintained by Microsoft. Newtonsoft.Json still has a broader feature set in some areas (like more flexible custom converters), which is why some existing large codebases haven’t migrated.

Yes, records work well with System.Text.Json, including their positional constructor syntax, and are a common modern choice for representing JSON-mapped data immutably.

Yes — the [JsonIgnore] attribute excludes a specific property from serialization, and [JsonPropertyName("customName")] lets you map a C# property to a differently-named JSON key without changing your naming convention project-wide.

Summary

System.Text.Json‘s biggest gotchas both come from the same root cause: it takes C# naming conventions literally by default, rather than assuming JSON’s more common camelCase convention. Case-sensitive property matching and PascalCase serialization both trip up developers moving from other libraries or languages, but both are a single settings change away from behaving how most people expect. Combined with silently-ignored unknown properties, the pattern worth internalizing is the same one that shows up across most JSON libraries: know your library’s lenient-versus-strict defaults before they hide a real bug from you.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *