System.Text.Json deserializes struct properties as null when reading from a stream - bug introduced in v9+
- Dominant language
- C#
- Stars
- 18.3k
- Forks
- 5.6k
- PR merge metrics
- PR metrics pending
Description
### Description
For very specific JSON input when reading from a stream, a bug was introduced in .NET 9: if a property is a nullable struct, the value may be set to null even if the JSON input includes the value. If the property is a class, the deserialization succeeds.
### Reproduction Steps
Please see below setup. The project with JSON input used is attached. Note that this test succeeds on .NET 8.
[SystemTextJsonBug.zip](https://github.com/user-attachments/files/25779820/SystemTextJsonBug.zip)
```csharp
using System.Text.Json;
using Xunit;
// ReSharper disable ClassNeverInstantiated.Global
// ReSharper disable UnusedAutoPropertyAccessor.Global
namespace SystemTextJsonBug;
public class StjBugTests
{
[Fact]
public void WithStruct()
{
var filename = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Input.json");
var bytes = File.ReadAllBytes(filename);
var fromBytes = JsonSerializer.Deserialize(bytes);
Assert.NotNull(fromBytes);
Assert.NotNull(fromBytes.Scheduled[3].Job);
Assert.NotNull(fromBytes.Scheduled[3].Job!.JobWeight.Remaining);
Assert.True(fromBytes.Scheduled[3].Job!.JobWeight.Remaining!.Value.Total > 0);
using var fs = new FileStream(filename, FileMode.Open, FileAccess.Read);
var fromStream = JsonSerializer.Deserialize(fs);
Assert.NotNull(fromStream);
Assert.NotNull(fromStream.Scheduled[3].Job);
Assert.NotNull(fromStream.Scheduled[3].Job!.JobWeight.Remaining); // Fails on .NET 9+
Assert.True(fromStream.Scheduled[3].Job!.JobWeight.Remaining!.Value.Total > 0);
}
[Fact]
public void WithClass()
{
var filename = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Input.json");
var bytes = File.ReadAllBytes(filename);
var fromBytes = JsonSerializer.Deserialize(bytes);
Assert.NotNull(fromBytes);
Assert.NotNull(fromBytes.Scheduled[3].Job);
Assert.NotNull(fromBytes.Scheduled[3].Job!.JobWeight.Remaining);
Assert.True(fromBytes.Scheduled[3].Job!.JobWeight.Remaining!.Total > 0);
using var fs = new FileStream(filename, FileMode.Open, FileAccess.Read);
var fromStream = JsonSerializer.Deserialize(fs);
Assert.NotNull(fromStream);
Assert.NotNull(fromStream.Scheduled[3].Job);
Assert.NotNull(fromStream.Scheduled[3].Job!.JobWeight.Remaining);
Assert.True(fromStream.Scheduled[3].Job!.JobWeight.Remaining!.Total > 0);
}
public class Model1Struct
{
public required Model2Struct[] Scheduled { get; init; }
}
public class Model2Struct
{
public required Model3Struct? Job { get; init; }
}
public class Model3Struct
{
public required Model4Struct JobWeight { get; init; }
}
public class Model4Struct
{
public required Model5Struct? Remaining { get; init; }
}
public readonly record struct Model5Struct
{
public required decimal Total { get; init; }
}
public class Model1Class
{
public required Model2Class[] Scheduled { get; init; }
}
public class Model2Class
{
public required Model3Class? Job { get; init; }
}
public class Model3Class
{
public required Model4Class JobWeight { get; init; }
}
public class Model4Class
{
public required Model5Class? Remaining { get; init; }
}
public record Model5Class
{
public required decimal Total { get; init; }
}
}
```
### Expected behavior
JSON input should successfully deserialize into property.
### Actual behavior
If property is a nullable struct and when reading from a stream the property may be set to null.
### Regression?
Yes, introduced on .NET 9.
### Known Workarounds
Use class models instead of struct.
### Configuration
_No response_
### Other information
_No response_
Contributor guide
Assessment
This issue has not been assessed yet.