-
Notifications
You must be signed in to change notification settings - Fork 4.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
JsonObjectCreationHandling.Populate not working with parameterized constructors #92877
Comments
Tagging subscribers to this area: @dotnet/area-system-text-json, @gregsdennis Issue DetailsThanks, this looks like a real bug. FWIW it's not specific to primary constructors or records, this fails too: using System.Text.Json.Serialization;
using System.Text.Json;
var singleUserJson = """{"Username":"Filip","PhoneNumbers":["123456"]}""";
var userFromJson = JsonSerializer.Deserialize<User>(singleUserJson);
Console.WriteLine(userFromJson.PhoneNumbers.Count); // 0
public class User
{
public User(string name)
=> Name = name;
public string Name { get; }
[JsonObjectCreationHandling(JsonObjectCreationHandling.Populate)]
public List<string> PhoneNumbers { get; } = new();
} Originally posted by @eiriktsarpalis in dotnet/docs#37329 (comment)
|
It behaves the same even if you mark the parameter as optional (default) value. Here's another case where it does not work, when using in combination with If you create a set of records like this:
The following will not populate
If you change the
|
It should be |
On closer inspection, the problem appears to be less trivial than I originally thought. The converter used for serializing types with parameterized constructors will deserialize both constructor parameters and properties before it instantiates the deserialized object. This is not accidental, instantiating the object depends on all constructor parameters being deserialized, but at the same time deserializing properties with populate semantics depends on the object being instantiated ahead of time, so the two requirements are fundamentally in conflict. I can't think of any good fixes other than buffering the object's JSON payload and replaying deserialization for properties after instantiation has completed, however that would require a substantial rearchitecting of the parameterized constructor implementation which is probably too risky for a .NET 8 servicing update. My recommendation is to simply mark populate semantics as unsupported in .NET 8 and work on a proper fix in future releases. |
Current problem can be worked around like this: using System.Text.Json.Serialization;
using System.Text.Json;
using System.Text.Json.Serialization.Metadata;
JsonSerializerOptions options = new()
{
TypeInfoResolver = new DefaultJsonTypeInfoResolver()
{
Modifiers = { PopulateForCollectionsAndDictionariesFix }
}
};
var singleUserJson = """{"Name":"Filip","PhoneNumbers":["123456"]}""";
var userFromJson = JsonSerializer.Deserialize<User>(singleUserJson, options);
Console.WriteLine(userFromJson.PhoneNumbers.Count); // 1
static void PopulateForCollectionsAndDictionariesFix(JsonTypeInfo typeInfo)
{
foreach (var property in typeInfo.Properties)
{
if (property.ObjectCreationHandling == JsonObjectCreationHandling.Populate && property.Set == null)
{
// adding a setter causes CanDeserialize flag to be true and populate to work
property.Set = (obj, val) => throw new JsonException("Setter should not be used since this property is Populate");
}
}
}
public class User
{
public User(string name)
=> Name = name;
public string Name { get; }
[JsonObjectCreationHandling(JsonObjectCreationHandling.Populate)]
public List<string> PhoneNumbers { get; } = new();
} I'd prefer we rather went ahead with doing nothing and allow people taking workaround until fix comes in than disallowing this entirely. I think the entire fix is literally replacing CanDeserialize with CanDeserializeAndPopulate in the file I mentioned earlier. |
@krwq this doesn't work if you try to use async: JsonSerializerOptions options = new()
{
TypeInfoResolver = new DefaultJsonTypeInfoResolver()
{
Modifiers = { PopulateForCollectionsAndDictionariesFix }
}
};
var stream = new MemoryStream("""{"Name":"Filip","PhoneNumbers":["123456"]}"""u8.ToArray());
var userFromJson = await JsonSerializer.DeserializeAsync<User>(stream, options); // exception
static void PopulateForCollectionsAndDictionariesFix(JsonTypeInfo typeInfo)
{
foreach (var property in typeInfo.Properties)
{
if (property.ObjectCreationHandling == JsonObjectCreationHandling.Populate && property.Set == null)
{
// adding a setter causes CanDeserialize flag to be true and populate to work
property.Set = (obj, val) => throw new JsonException("Setter should not be used since this property is Populate");
}
}
}
public class User
{
public User(string name)
=> Name = name;
public string Name { get; }
[JsonObjectCreationHandling(JsonObjectCreationHandling.Populate)]
public List<string> PhoneNumbers { get; } = new();
} Fundamentally the issue lies with async deserialization and how it handles parameterized constructors: Lines 207 to 213 in d3569b9
|
#92937 added validation explicitly prohibiting the scenario, moving to 9.0.0 for a proper fix. |
It's unfortunate that this won't be in .NET 9 because it's one of the two reasons that force us to create boilerplate constructors: the first is nullability, which has been addressed in preview 6 (🎉) and the other is creating a collection with a specific comparer. Since it's not mentioned here or in the docs I want to point it out, as it is a frequent use case for us and requires various workarounds in STJ. We frequently deal with case-insensitive dictionaries or sets and need to create a parameterized JSON constructor simply to deep-copy the dictionary/set created by STJ into one that uses the correct comparer. If we were able to pre-populate the collection property, the need for boilerplate code and deep copies would go away. |
@EnCey would you be interested in contributing a fix? We have about 3 weeks left before .NET 9 development freezes. |
What would be an acceptable fix @eiriktsarpalis ? Based on your post above:
I suspect that I wouldn't be able to implement such a fundamental change (in time) that meets your quality targets. I'm not familiar with the code base, but I assume performance and memory use are very important to you and a naive solution won't fly. If the use case of case-insensitive collections is deemed important enough, perhaps a simpler solution would be acceptable? Something like an attribute or option that allows a user to specify a comparer for collections, used by STJ when instantiating the collection? |
That would require adding new API, unfortunately we're out of time when it comes to adding new features to .NET 9. |
We recently hit this issue in AI related work, so we should probably try to prioritize this. |
Originally posted by @eiriktsarpalis in dotnet/docs#37329 (comment)
The text was updated successfully, but these errors were encountered: