dotnet / dotnet/runtime

System.Text.Json deserializes struct properties as null when reading from a stream - bug introduced in v9+

Open
#125,237 2 comments 0 reactions 0 assignees View on GitHub
area-System.Text.Json bug
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

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.