You can use a json constructor and use the property or field names as they are in reflection or use The JsonParameterName attribute and use no json constructor, you can't do both. Perhaps .net 9 will allow for it, but when you look at the source generator, it doesn't, as it uses reflection on the property names. To see for yourself, right-click ToDoJsonContext and select "go to definition." and look for how it expects the constructor to be called
As Yits mentions, remove the json constructor (meaning you need to support a parameterless constructor).
Here is a little test application:
using System.Diagnostics;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace JsonTestConsole
{
internal class Program
{
static async Task Main(string[] args)
{
/*test and work with a single entry*/
var entry = new ToDo() { Rank = Random.Shared.Next(1, int.MaxValue), Task = "test a class" };
var json = JsonSerializer.Serialize<ToDo>(entry, ToDoJsonContext.Default.ToDo);
Console.WriteLine(json);
Debug.Assert(json.Contains("\"a\":"), "supposed to have \"a\": property");
var copy = JsonSerializer.Deserialize<ToDo>(json, ToDoJsonContext.Default.ToDo);
Debug.Assert(copy is not null, "Serialization failed for a single ToDo");
Debug.Assert(copy.Rank == entry.Rank, $"supposed to have the same rank {copy.Rank} and the original has {entry.Rank}.");
Debug.Assert(string.Equals(copy.Task, entry.Task, StringComparison.Ordinal), $"Texts is supposed to be the same however copy task = {copy.Task} and entry task = {entry.Task}");
Console.WriteLine($"Copy has Rank:{copy.Rank} and entry has {entry.Rank}");
Console.WriteLine($"Copy has Task:{copy.Task} and entry has {entry.Task}");
/*test and work with a list*/
var data = new List<ToDo> { new() { Rank = 1, Task = "test json serialization" }, new() { Rank = 2, Task = "test call constructor" } };
using var stream = new MemoryStream();
await JsonSerializer.SerializeAsync(stream, data, ToDoListJsonContext.Default.ListToDo);
json = System.Text.Encoding.UTF8.GetString(stream.ToArray());
Console.WriteLine(json);
Debug.Assert(json.Contains("\"a\":"), "supposed to have \"a\": property");
var clone = JsonSerializer.Deserialize<List<ToDo>>(json, ToDoListJsonContext.Default.ListToDo);
Debug.Assert(clone is not null, "Serialization failed");
Debug.Assert(clone.Count == data.Count, $"supposed to have the same number of items, clone has {clone.Count} and data has {data.Count}.");
Console.WriteLine($"Cone has {clone.Count} entries");
#if !DEBUG
Console.WriteLine("Press enter to exit");
Console.ReadLine();
#endif
}
}
[System.Text.Json.Serialization.JsonSerializable(typeof(ToDo))]
[System.Text.Json.Serialization.JsonSourceGenerationOptions(
GenerationMode = System.Text.Json.Serialization.JsonSourceGenerationMode.Metadata)]
public partial class ToDoJsonContext : System.Text.Json.Serialization.JsonSerializerContext
{
}
[System.Text.Json.Serialization.JsonSerializable(typeof(List<ToDo>))]
[System.Text.Json.Serialization.JsonSourceGenerationOptions(GenerationMode = System.Text.Json.Serialization.JsonSourceGenerationMode.Metadata)]
public partial class ToDoListJsonContext : System.Text.Json.Serialization.JsonSerializerContext
{
}
public class ToDo
{
[JsonInclude]
[JsonPropertyName("a")]
public int Rank { get; set; }
[JsonInclude]
[JsonPropertyName("b")]
public required string Task { get; set; }
}
}
I also set my project to support AOT:
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<PublishAot>true</PublishAot>
</PropertyGroup>
I run it in release mode as well as in AoT using the following publish settings:

Works like a charm:
{"a":378268935,"b":"test a class"} [{"a":1,"b":"test json
serialization"},{"a":2,"b":"test call constructor"}] Press enter to
exit