JSON serialization is a common part of modern .NET applications. APIs use it to exchange data, applications use it to store configuration, and background services rely on it to process messages.
System.Text.Json supports two approaches for discovering the properties and structure of .NET types: reflection and source generation.
Reflection is convenient because the serializer can inspect types at runtime without requiring additional generated code. Source generation moves much of that work to compile time, which can improve startup performance, reduce runtime metadata requirements, and make applications more suitable for trimming and Native AOT.
However, source generation is not automatically the best choice for every application. The right approach depends on the application's deployment model, serialization requirements, performance needs, and maintenance preferences.
1. Understand JSON Serialization in .NET
Before comparing reflection and source generation, it is important to understand what happens when an application serializes a .NET object.
Consider a simple employee model:
public class Employee
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public string Department { get; set; } = string.Empty;
}
You can serialize this object using System.Text.Json:
var employee = new Employee
{
Id = 101,
Name = "John",
Department = "Engineering"
};
string json = JsonSerializer.Serialize(employee);
The output looks like this:
{
"Id": 101,
"Name": "John",
"Department": "Engineering"
}
To perform this operation, the serializer needs information about the employee type, including:
Which properties should be serialized.
How property values can be accessed.
Which constructors and setters are available for deserialization.
Whether serialization attributes are applied.
Which serialization options should be used.
This information is known as serialization metadata.
System.Text.Json needs this metadata whether you use reflection or source generation. The difference is when and how the metadata is created.
2. How Reflection Works in System.Text.Json
Reflection allows .NET applications to inspect types, properties, methods, constructors, and other metadata at runtime.
By default, System.Text.Json uses reflection to discover the structure of a type when it first needs to serialize or deserialize it.
For example:
var employee = new Employee
{
Id = 101,
Name = "John",
Department = "Engineering"
};
string json = JsonSerializer.Serialize(employee);
var result = JsonSerializer.Deserialize<Employee>(json);
You do not need to register the Employee type or create additional serialization configuration.
The serializer discovers the properties at runtime and caches the metadata for subsequent operations.
Advantages of reflection
Reflection provides several practical benefits:
Minimal configuration is required.
New types can be serialized without updating a source-generation context.
Runtime-defined types are easier to handle.
It supports a broad range of serialization customizations.
It is convenient for applications with relatively simple serialization requirements.
For many traditional ASP.NET Core applications, reflection is sufficient.
Limitations of reflection
Reflection also has some disadvantages:
Metadata discovery introduces runtime work.
It can increase private memory usage.
Reflection-based code can create challenges for trimming.
Some reflection patterns are incompatible with Native AOT.
Runtime behavior can be harder to diagnose when required metadata has been removed during publishing.
These limitations become more important in applications that have strict startup, memory, or deployment requirements.
3. What Is JSON Source Generation?
JSON source generation uses the C# compiler's source-generation capabilities to create serialization-related code during compilation.
Instead of discovering every type's metadata at runtime, System.Text.Json can use generated code that already describes the types.
You define a serialization context and register the types that need to be supported.
For example:
using System.Text.Json.Serialization;
[JsonSerializable(typeof(Employee))]
internal partial class AppJsonContext : JsonSerializerContext
{
}
The partial context is important because the source generator produces the remaining implementation during compilation.
You can then serialize and deserialize using the generated metadata:
var employee = new Employee
{
Id = 101,
Name = "John",
Department = "Engineering"
};
string json = JsonSerializer.Serialize(
employee,
AppJsonContext.Default.Employee
);
var result = JsonSerializer.Deserialize(
json,
AppJsonContext.Default.Employee
);
The generated context provides the serializer with the metadata required for the Employee type.
You do not need to manually write property-access code or implement a custom serializer.
Benefits of source generation
Source generation provides several advantages:
Moves metadata discovery from runtime to compile time.
Reduces dependence on runtime reflection.
Can improve application startup performance.
Can reduce private memory usage.
Helps applications work with trimming.
Supports Native AOT scenarios when the required serialization metadata is generated.
It is particularly useful when the application has a known set of data-transfer objects and serialization requirements.
4. Understand the Two Source Generation Modes
One important detail is that System.Text.Json source generation supports two modes.
They solve related but different problems.
Metadata-based mode
Metadata-based source generation creates the serialization metadata during compilation.
It supports both serialization and deserialization.
For example:
[JsonSourceGenerationOptions(
GenerationMode = JsonSourceGenerationMode.Metadata)]
[JsonSerializable(typeof(Employee))]
internal partial class AppJsonContext : JsonSerializerContext
{
}
This approach is useful when you want to avoid runtime metadata discovery while keeping normal serializer functionality.
It is often a practical starting point for applications moving from reflection to source generation.
Serialization-optimization mode
Serialization-optimization mode generates optimized code for writing JSON.
Instead of relying only on generated metadata, it can generate code that writes JSON directly through Utf8JsonWriter.
For example:
[JsonSourceGenerationOptions(
GenerationMode = JsonSourceGenerationMode.Serialization)]
[JsonSerializable(typeof(Employee))]
internal partial class EmployeeJsonContext : JsonSerializerContext
{
}
This mode is designed to improve serialization throughput.
However, it has limitations:
It supports serialization, not deserialization, as a fast path.
It does not support every serializer option and customization.
Unsupported configurations may require metadata-based fallback.
Asynchronous serialization does not use the fast path in the same way as synchronous serialization.
For applications that need both optimized serialization and deserialization, generating both modes is generally more flexible.
[JsonSerializable(typeof(Employee))]
internal partial class AppJsonContext : JsonSerializerContext
{
}
When no generation mode is specified, the default configuration generates both metadata-based and serialization-optimization code.
5. Reflection vs Source Generation
The following comparison helps explain where each approach fits.
Area | Reflection | Source generation |
|---|
Metadata discovery | Runtime | Compile time |
Initial runtime work | Requires metadata discovery | Uses generated metadata |
Startup performance | Can introduce additional work | Can reduce startup work |
Memory usage | Runtime metadata is cached | Can reduce private metadata overhead |
Deserialization | Supported | Supported in metadata mode |
Optimized serialization | Standard serializer path | Available through fast-path mode |
Trimming | Requires careful configuration | Better suited to trimming |
Native AOT | Reflection patterns can be problematic | Preferred when metadata is generated |
Configuration | Usually simpler | Requires a context and registered types |
Runtime-defined types | More flexible | Requires appropriate metadata or fallback |
The important distinction is that source generation is not a complete replacement for reflection in every scenario. It provides a more predictable and compile-time-oriented approach for applications with known serialization contracts.
6. Use Source Generation in ASP.NET Core Applications
Consider an ASP.NET Core API that returns employee information.
A typical controller might look like this:
[ApiController]
[Route("api/employees")]
public class EmployeesController : ControllerBase
{
[HttpGet("{id}")]
public IActionResult GetEmployee(int id)
{
var employee = new Employee
{
Id = id,
Name = "John",
Department = "Engineering"
};
return Ok(employee);
}
}
ASP.NET Core uses its configured JSON output formatter to serialize the response.
To use source-generated metadata with HTTP JSON options, configure the generated context in the application's service configuration:
using System.Text.Json.Serialization;
builder.Services.ConfigureHttpJsonOptions(options =>
{
options.SerializerOptions.TypeInfoResolverChain.Insert(
0,
AppJsonContext.Default
);
});
This configuration applies to HTTP JSON services that use these options, such as minimal API JSON handling.
For MVC controllers, configure the MVC JSON options separately:
builder.Services
.AddControllers()
.AddJsonOptions(options =>
{
options.JsonSerializerOptions.TypeInfoResolverChain.Insert(
0,
AppJsonContext.Default
);
});
The generated context should include the types that the application needs to serialize and deserialize.
For example:
[JsonSerializable(typeof(Employee))]
[JsonSerializable(typeof(List<Employee>))]
[JsonSerializable(typeof(EmployeeResponse))]
internal partial class AppJsonContext : JsonSerializerContext
{
}
Registering the context does not mean every application type is automatically supported. If an endpoint returns a new response type, make sure its serialization metadata is available through the configured resolver chain or another supported resolver.
7. Use Source Generation for Native AOT Applications
Native AOT compiles an application into native code ahead of time.
This deployment model can reduce startup overhead and remove the need for a JIT compiler at runtime. However, it also introduces restrictions around dynamic code generation and reflection-based discovery.
Consider the following serialization call:
var employee = JsonSerializer.Deserialize<Employee>(json);
This simple call uses the default serializer behavior. In a Native AOT application, relying on reflection-based metadata discovery can cause trimming or runtime compatibility problems.
Using a generated context makes the serialization contract explicit:
var employee = JsonSerializer.Deserialize(
json,
AppJsonContext.Default.Employee
);
You can also disable reflection-based serialization defaults to detect accidental reflection usage earlier:
AppContext.SetSwitch(
"System.Text.Json.JsonSerializer.IsReflectionEnabledByDefault",
false
);
When reflection-based serialization is disabled, code that depends on the default reflection resolver can fail rather than silently relying on reflection.
This makes it easier to identify missing generated metadata during testing.
For Native AOT applications, review all serialization paths, including:
API request and response models.
Background message payloads.
Configuration objects.
Cache values.
Third-party library integration.
Polymorphic models.
Custom JSON converters.
Generating metadata for only the main API response types may not be sufficient if other parts of the application still depend on reflection-based serialization.
8. Understand the Limitations Before Switching
Source generation provides useful performance and deployment benefits, but there are some limitations to consider.
Not every type is automatically supported
Reflection can discover types dynamically at runtime. Source generation works best when the required types are known at compile time.
If an application receives runtime-defined types from plugins or external components, you need to plan how their serialization metadata will be supplied.
Private members have restrictions
Source generation has different accessibility limitations from reflection.
In .NET 10 and earlier, source generation does not support private or protected members and accessors in the same way as reflection. Internal members have specific accessibility requirements.
If your models depend on private properties or private setters marked with JsonInclude, review the target framework's source-generation support before migrating.
Fast-path serialization has feature limitations
Serialization-optimization mode does not support every JsonSerializerOptions setting or serialization attribute.
When an unsupported feature is used, the serializer can fall back to metadata-based processing if the required metadata was generated.
If you generate only serialization-optimization code, some types or configurations may not work as expected.
Generated code does not remove the need for testing
Source generation changes how serialization metadata is provided, but it does not guarantee that the resulting JSON matches every external contract.
Test the actual JSON output, deserialization behavior, naming policies, converters, null handling, and polymorphic behavior before changing an existing application.
9. Common Mistakes to Avoid
When adopting source generation, watch out for these common mistakes:
Registering only one type: A response model may contain nested types that also need serialization metadata.
Assuming fast-path mode supports deserialization: Fast-path optimization is for serialization. Use metadata mode for deserialization.
Forgetting custom converters: Some converter configurations are not supported by the optimized path and may require metadata fallback.
Enabling Native AOT without testing all serialization paths: Configuration, messaging, and third-party integrations can still depend on reflection.
Expecting automatic support for runtime-defined types: Source generation requires metadata to be available for the types being serialized.
Migrating without measuring performance: Startup, memory usage, serialization throughput, and application size should be measured against the actual workload.
10. Best Practices for Production Applications
For a production .NET application, consider the following practices:
Start with metadata-based source generation when replacing reflection.
Generate metadata for all important request and response models.
Use serialization-optimization mode when serialization throughput is a measurable concern.
Keep generated contexts close to the application's API or messaging contracts.
Test custom converters and polymorphic models carefully.
Disable reflection defaults in applications targeting Native AOT to expose unsupported paths during testing.
Verify trimming and Native AOT compatibility in published builds, not only during local development.
Compare startup time, private memory usage, application size, and throughput before and after the change.
Keep reflection where runtime flexibility is genuinely required and supported by the deployment model.
You do not need to convert every serialization operation immediately. A gradual migration is often easier to validate and maintain.
Summary
Reflection and source generation are two ways System.Text.Json obtains the metadata required to serialize and deserialize .NET objects.
Reflection is convenient and flexible because metadata is discovered at runtime. It remains a reasonable choice for applications that do not have strict startup, trimming, or Native AOT requirements.
Source generation moves metadata creation to compile time. Its metadata-based mode supports serialization and deserialization, while serialization-optimization mode can improve the performance of supported serialization operations.
For modern .NET applications with known data contracts, source generation is worth considering, especially when startup performance, memory usage, trimming, or Native AOT compatibility matters.
The best approach is to begin with metadata-based generation, validate the application's JSON contracts, and introduce fast-path serialization only when measurements show that it provides a meaningful benefit.
System.Text.JsonJSON source generationreflection.NETNative AOTtrimmingJSON serialization
Join the conversation! Your thoughts help the community grow.