System.Text.Json
Turn objects into JSON text and back with JsonSerializer, and control the property names the wire format uses.
public static string Serialize<TValue>(TValue value, JsonSerializerOptions? options = null)JSON is a text format for structured data: objects in braces, arrays in brackets, plus strings, numbers, true, false and null. Nearly every web API speaks it. Serializing means turning an object into that text; deserializing means reading the text back into an object. System.Text.Json is the built-in library for both, and it needs no package.
The two calls
JsonSerializer.Serialize(value) returns a string. JsonSerializer.Deserialize<Book>(json) returns a Book?, nullable because the text could be the word null. Only public properties with a getter are included; fields are skipped unless you set IncludeFields to true.
Options
A JsonSerializerOptions object carries the settings. PropertyNamingPolicy = JsonNamingPolicy.CamelCase writes firstName for a property called FirstName, which is what most APIs expect; SnakeCaseLower, SnakeCaseUpper, KebabCaseLower and KebabCaseUpper are also available. WriteIndented = true adds line breaks and two-space indentation. DefaultIgnoreCondition can skip members that are null.
new JsonSerializerOptions(JsonSerializerDefaults.Web) is shorthand for three settings at once: camelCase names, case-insensitive matching and numbers accepted inside quotes. ASP.NET Core uses those defaults. .NET 10 adds JsonSerializerOptions.Strict, a preset that rejects unknown and duplicate properties.
Names C# cannot carry
When the wire name does not fit C# naming, put [JsonPropertyName("isbn_13")] on the property. The attribute wins over the naming policy. [JsonIgnore] leaves a member out entirely, which is how you keep a password or an internal flag out of a response.
Build one options object and reuse it. A fresh one per call throws away the serializer's cached metadata and is noticeably slow.
Example
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
JsonSerializerOptions options = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
Book book = new Book { Title = "Dune", Year = 1965, Isbn = "0441013597" };
string json = JsonSerializer.Serialize(book, options);
Console.WriteLine(json);
// Deserialize returns Book? because the text could be the word null.
Book? copy = JsonSerializer.Deserialize<Book>(json, options);
Console.WriteLine(copy?.Title);
Console.WriteLine(copy?.Isbn);
public class Book
{
public string Title { get; set; } = "";
public int Year { get; set; }
// The attribute wins over the camelCase policy for this one member.
[JsonPropertyName("isbn_13")]
public string Isbn { get; set; } = "";
}
// Output:
// {"title":"Dune","year":1965,"isbn_13":"0441013597"}
// Dune
// 0441013597When to use it
- Send an object as the body of an HTTP request to another service.
- Store a settings object in a file and read it back on the next run.
- Match an external API's property names without renaming your C# properties.
- Keep a password or internal id out of a serialized response.
More examples
The Web preset and its longhand
Worth knowing both forms, because an ASP.NET Core endpoint already behaves like the first one and a console program does not. If JSON binds correctly inside a web API and silently comes back empty in a test or a worker, mismatched options are the usual cause.
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
// Shorthand: the Web preset.
JsonSerializerOptions web = new JsonSerializerOptions(JsonSerializerDefaults.Web);
// Longhand: the same three settings written out.
JsonSerializerOptions byHand = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
PropertyNameCaseInsensitive = true,
NumberHandling = JsonNumberHandling.AllowReadingFromString
};
// Wrong casing, and the number arrives inside quotes.
string json = """{ "itemcount": "3" }""";
Console.WriteLine(JsonSerializer.Deserialize<Cart>(json, web)?.ItemCount);
Console.WriteLine(JsonSerializer.Deserialize<Cart>(json, byHand)?.ItemCount);
public class Cart
{
public int ItemCount { get; set; }
}
// Output:
// 3
// 3Indenting, skipping nulls and hiding members
WriteIndented is for files and logs a person reads; leave it off for API responses, where it only adds bytes. JsonIgnore is the safety rail that keeps secrets out of a payload, and it is better than remembering to copy fields by hand, because a new property added later is not accidentally exposed.
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
JsonSerializerOptions options = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = true, // two-space indent
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull // drop null members
};
Person person = new Person { Name = "Ada", Nickname = null, Password = "hunter2" };
Console.WriteLine(JsonSerializer.Serialize(person, options));
public class Person
{
public string Name { get; set; } = "";
public string? Nickname { get; set; }
[JsonIgnore] // never written out, never read in
public string Password { get; set; } = "";
}
// Output:
// {
// "name": "Ada"
// }Reading JSON without declaring a type
Sometimes you only need one field out of a large unfamiliar payload, and writing a class for the whole shape is wasted work. JsonDocument gives read-only access to the parsed text. It owns pooled memory, so it implements IDisposable, and any JsonElement you take from it stops being valid once it is disposed.
using System;
using System.Text.Json;
string json = """{ "id": 4, "tags": ["red", "blue"] }""";
// JsonDocument parses the text and lets you walk it. It must be disposed.
using JsonDocument document = JsonDocument.Parse(json);
JsonElement root = document.RootElement;
Console.WriteLine(root.GetProperty("id").GetInt32());
Console.WriteLine(root.GetProperty("tags").GetArrayLength());
Console.WriteLine(root.GetProperty("tags")[0].GetString());
// Output:
// 4
// 2
// red
Discussion