Serialization converts the state of an object (the value of its properties) into a form that can be stored/transmitted. The serialized form doesn’t include any information about an object’s associated methods.
System.Text.Json emphasizes high performance and low memory allocation over an
extensive feature set. It has built-in UTF-8 support because UTF-8 is the most
prevalent encoding for data on the web and files on disk.
How to Serialize and Deserialize .NET Objects
To write JSON to a string, call JsonSerializer.Serialize. To read from a JSON
string, call JsonSerializer.Deserialize<T>. For example:
Foo foo = new(1, "two");
var jsonStr = JsonSerializer.Serialize(foo);
Console.WriteLine(jsonStr); // {"s":"two","n":1}
var roundTripSuccess = JsonSerializer.Deserialize<Foo>(jsonStr) == foo;
Console.WriteLine(roundTripSuccess); // True
abstract record Base(int n);
record Foo(int n, string s): Base(n);
JsonSerializer.Serialize<Base>(foo) only serializes properties in Base,
i.e., {"n":1}. When deserializing, any properties not represented in your T
are ignored by default.
One can also serialize to and deserialize from a file using
JsonSerializer.SerializeAsync and JsonSerializer.DeserializeAsync<T>,
respectively, e.g.,
string fileName = "my-data.json";
await using FileStream outputStream = File.Create(fileName);
await JsonSerializer.SerializeAsync(outputStream, foo);
await using FileStream inputStream = File.OpenRead(fileName);
Foo? foo = await JsonSerializer.DeserializeAsync<Foo>(inputStream);
There exist synchronous APIs as well, e.g., File.WriteAllText and
File.ReadAllText. When would one choose the synchronous versions over the
asynchronous versions? I’ve been under the impression that I/O should be done
async whenever possible.
If you don’t have a T to deserialize into, you can use the Utf8JsonReader
directly, or deserialize into a JsonNode DOM, which lets you navigate to any
subsection of a JSON payload.
Types supported out-of-the-box include: .NET primitives that map to JavaScript
primitives; user-defined plain old CLR objects; T[] and T[][], collections
and dictionaries from System.Collections.
Because bytes (as UTF-8) don’t need to be converted to strings (UTF-16), it is
5-10% faster to use JsonSerializer.SerializeToUtf8Bytes instead of
JsonSerializer.Serialize . There is a
JsonSerializer.Deserialize<T> overload that takes in a Utf8JsonReader or a
ReadOnlySpan<byte> to deserialize the bytes.
JsonSerializer.Serialize accepts a JsonSerializerOptions to configure the
output, e.g.,
var options = new JsonSerializerOptions { WriteIndented = true };
string jsonString = JsonSerializer.Serialize(foo, options);
How to Deserialize with Required Properties
If there are any required properties of T missing from the JSON payload,
then deserialization throws a JsonException at runtime.
public class Foo
{
// Option 1: `s` is required in all contexts, even outside of serialization.
public required string s { get; set; }
// Option 2: `n` is required only in a serialization context.
[JsonRequired]
public int n {get; set; }
}
From a deserialization perspective, the required keyword is equivalent to the
JsonRequired attribute. The latter is useful when using source generation
because at compile time, the required constraint can’t be satisfied.
The required constraint is checked at runtime. It’s possible to control this
through the TypeInfoResolver passed to the JsonSerializerOptions, e.g., by
setting JsonPropertyInfo.IsRequired.
With JsonSerializerOptions.RespectRequiredConstructorParameters set,
non-optional constructor parameters, e.g., Name in record Person(string Name, int? Age = null), are treated as required.
How to Customize Property Names and Values
By default, properties are serialized with the same name and in the order in
which they are defined. JsonNameProperty and JsonPropertyOrder can modify
this, e.g.,
Foo foo = new Foo {};
// Output: {"nTwo":0,"n_One":0,"tatu":0}
Console.WriteLine(JsonSerializer.Serialize(foo));
public class Foo
{
public int n_One { get; set; }
[JsonPropertyOrder(-3)]
public int nTwo { get; set; }
[JsonPropertyName("tatu")]
public int n_three { get; set; }
}
Without the attributes, the serialization would have been
{"n_One":0,"nTwo":0,"n_three":0}.
JsonSerializerOptions has a PropertyNamingPolicy that takes in a policy for
generating names when serializing. The JsonPropertyName overrides this though:
Foo foo = new Foo {};
var options = new JsonSerializerOptions
{
PropertyNamingPolicy = new ContosoNamingPolicy()
};
// Output: {"contoso_N_ONE":0,"contoso_NTWO":0,"tatu":0}
Console.WriteLine(JsonSerializer.Serialize(foo, options));
public class Foo
{
public int n_One { get; set; }
public int nTwo { get; set; }
[JsonPropertyName("tatu")]
public int n_three { get; set; }
}
public class ContosoNamingPolicy : JsonNamingPolicy
{
public override string ConvertName(string name) =>
$"contoso_{name.ToUpper()}";
}
There are several in-built JsonNamingPolicy.* classes, e.g., CamelCase
(fooBar), KebabCaseLower (foo-bar), KebabCaseUpper (FOO-BAR),
SnakeCaseLower (foo_bar) and SnakeCaseUpper (FOO_BAR).
If serializing a Dictionary<string, TValue>, then one can supply a
JsonNamingPolicy to JsonSerializerOptions’s DictionaryKeyPolicy to specify
how the keys will be serialized. However, unlike PropertyNamingPolicy,
JsonNamingPolicy only applies during serialization and not during
deserialization.
If JsonNamingPolicy does not support round-tripping data, then what use is it?
Seems like a risky API…
By default, enums are serialized as numbers. One can specify attributes on enums to serialize them differently, e.g.,
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum Foo
{
Bar, // Output: Bar
[JsonStringEnumMemberName("AlternateBazName")]
Baz, // Output: AlternateBazName
Qux // Output: Qux
}
One can also apply converters to the JsonSerializerOptions, e.g.,
var options = new JsonSerializerOptions
{
Converters = { new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) }
};
Console.WriteLine(JsonSerializer.Serialize(Foo.Baz, options)); // Output: baz
public enum Foo { Bar, Baz, Qux }
Of the various modifications to JsonSerializerOptions, serializing enums as
strings is my favorite. Helps a ton with readability when debugging data flows
because I don’t need to maintain a map of what enumerator corresponds with a
given number. It also makes it easy to re-order enums because their underlying
numerical values are inconsequential.
How to Exclude Properties
By default, all public properties are serialized. The JsonIgnore attribute
allows us to ignore individual properties, e.g.,
public class Foo
{
[JsonIgnore(Condition = JsonIgnoreCondition.Always)]
public string s1 { get; set; } = "Paukwa"; // Will not be serialized.
public string s2 { get; set; } = "Pakawa";
}
JsonIgnoreCondition values include Always (default), Never,
WhenWritingDefault, and WhenWritingNull.
Read-only properties are ones with a public getter but a non-public setter,
e.g., public string s { get; private set; } = "Top Secret".
JsonSerializerOptions’s IgnoreReadOnlyProperties can be set to true to
override the default behavior where such properties get serialized.
JsonSerializerOptions’s DefaultIgnoreCondition can be set to ignore
properties based on a criteria, e.g., WhenWritingNull to drop nulls,
WhenWritingDefault to drop defaults and nulls.
How to Include Fields
A field defines a storage location.
public class Person
{
public string? FirstName;
}
… while a property is an outward-facing declaration:
public class Person
{
// The compiler generates a hidden backing field, and implements the body of
// the get and set accessors.
public string FirstName { get; set; } = string.Empty;
}
Think of properties as smart fields. You can provide validation, lazy evaluation, different accessibility, etc.
By default, fields are not serialized. Set JsonSerializerOptions.IncludeFields
or use the [JsonInclude] attribute on them to include them. Set
JsonSerializerOptions.IgnoreReadOnlyFields to ignore fields marked with
readonly.
Reflection vs. Source Generation
By default, System.Text.Json gathers the metadata needed to access properties
of objects for serialization at run time using
reflection
.
Alternatively, System.Text.Json can use source generation to improve
performance, reduce private memory, and facilitate assembly trimming, which
reduces app size.
References
- Serialize and deserialize JSON using C# - .NET | Microsoft Learn. learn.microsoft.com . Accessed Jul 5, 2025.
- How to serialize JSON in C# - .NET | Microsoft Learn. learn.microsoft.com . Accessed Jul 5, 2025.
- JsonSerializerOptions Class (System.Text.Json) | Microsoft Learn. learn.microsoft.com . Accessed Jul 5, 2025.
- How to customize property names and values with System.Text.Json - .NET | Microsoft Learn. learn.microsoft.com . Accessed Jul 5, 2025.
- How to ignore properties with System.Text.Json - .NET | Microsoft Learn. learn.microsoft.com . Accessed Jul 5, 2025.
- Properties - C# | Microsoft Learn. learn.microsoft.com . Accessed Jul 5, 2025.
- Include fields in serialization - .NET | Microsoft Learn. learn.microsoft.com . Accessed Jul 5, 2025.
- How to deserialize JSON in C# - .NET | Microsoft Learn. learn.microsoft.com . Accessed Jul 6, 2025.
- Require properties for deserialization - .NET | Microsoft Learn. learn.microsoft.com . Accessed Jul 6, 2025.
Using
JsonSerializer.Serialize<T>(foo)whereFoocannot be converted to aTleads to a compiler error.