Skip to content
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

JsonConverter inconsistency when deserializing into property without public getter #56212

Open
Georgiks opened this issue Jul 23, 2021 · 10 comments
Assignees
Labels
Milestone

Comments

@Georgiks
Copy link

Georgiks commented Jul 23, 2021

Description

With given code

public class PrivateGetter<T>
{
    public T Property { private get; set; }

    public override string ToString()
    {
        return Property is null ? "null" : Property.ToString();
    }
}

public class PublicGetter<T>
{
    public T Property { get; set; }

    public override string ToString()
    {
        return Property is null ? "null" : Property.ToString();
    }
}
var jsonListOfInts = @"
{
    ""Property"": [ 1 ]
}
";

var jsonInt = @"
{
    ""Property"": 1
}
";
Console.WriteLine(JsonSerializer.Deserialize<PrivateGetter<List<int>>>(jsonListOfInts));
Console.WriteLine(JsonSerializer.Deserialize<PublicGetter<List<int>>>(jsonListOfInts));

Console.WriteLine(JsonSerializer.Deserialize<PrivateGetter<int>>(jsonInt));
Console.WriteLine(JsonSerializer.Deserialize<PublicGetter<int>>(jsonInt));

Output:

null
System.Collections.Generic.List`1[System.Int32]
1
1

When deserializing a JSON with a List<> property with a non-public getter, the value is not deserialized and property is null.
However when serializing and int the same way, a value of 1 is (correctly) assigned.
I expect that the behaviour of deserializing into property with non-public getter is independent of the property type.

Unfortunatelly the documentation site https://docs.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-immutability does not specify whether the behaviour is to allow deserializing into property without public getter or not, therefore it is hard to guess whether the case with int is wrong or the List<> one.

Configuration

.NET Core 3.1

Regression?

Other information

@dotnet-issue-labeler dotnet-issue-labeler bot added area-System.Text.Json untriaged New issue has not been triaged by the area owner labels Jul 23, 2021
@ghost
Copy link

ghost commented Jul 23, 2021

Tagging subscribers to this area: @eiriktsarpalis, @layomia
See info in area-owners.md if you want to be subscribed.

Issue Details

Description

With given code

public class PrivateGetter<T>
{
    public T Property { private get; set; }

    public override string ToString()
    {
        return Property is null ? "null" : Property.ToString();
    }
}

public class PublicGetter<T>
{
    public T Property { get; set; }

    public override string ToString()
    {
        return Property is null ? "null" : Property.ToString();
    }
}
var jsonListOfInts = @"
{
    ""Property"": [ 1 ]
}
";

var jsonInt = @"
{
    ""Property"": 1
}
";
Console.WriteLine(JsonSerializer.Deserialize<PrivateGetter<List<int>>>(jsonListOfInts));
Console.WriteLine(JsonSerializer.Deserialize<PublicGetter<List<int>>>(jsonListOfInts));

Console.WriteLine(JsonSerializer.Deserialize<PrivateGetter<int>>(jsonInt));
Console.WriteLine(JsonSerializer.Deserialize<PublicGetter<int>>(jsonInt));

Output:

null
System.Collections.Generic.List`1[System.Int32]
1
1

When deserializing a JSON with a List<> property with private getter, the value is not deserialized and property is null.
However when serializing and int the same way, a value of 1 is correctly assigned.
I expect that the behaviour of deserializing into property with non-public getter is independent on the property type.

Unfortunatelly the documentation site https://docs.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-immutability does not specify whether the behaviour is to allow deserializing into property without public getter or not, therefore it is hard to guess whether the case with int is wrong or the List<> one.

Configuration

.NET Core 3.1

Regression?

Other information

Author: Georgiks
Assignees: -
Labels:

area-System.Text.Json, untriaged

Milestone: -

@eiriktsarpalis
Copy link
Member

eiriktsarpalis commented Jul 23, 2021

I can reproduce -- the issue is certainly surprising although I was able to force deserialization for the List case by attaching a JsonInclude attribute to the property.

That being said, the private getter/public setter combination is unusual, and we generally recommend against using it. We should still fix this and ensure relevant test coverage is added for that particular combination.

@eiriktsarpalis eiriktsarpalis added bug Priority:3 Work that is nice to have and removed untriaged New issue has not been triaged by the area owner labels Jul 23, 2021
@eiriktsarpalis eiriktsarpalis added this to the 6.0.0 milestone Jul 23, 2021
@eiriktsarpalis eiriktsarpalis self-assigned this Jul 26, 2021
@eiriktsarpalis
Copy link
Member

Doing a bit more digging -- the root cause appears to be this method which seems to applying different logic depending on whether the property type is a collection or not. Not sure why though, I couldn't find any comments justifying the split.

@layomia is this by design? should we be making the change to support setter-only deserialization?

@eiriktsarpalis
Copy link
Member

eiriktsarpalis commented Jul 28, 2021

We seem to have a test that validates the behavior as reported:

public async Task NoGetter()
{
ClassWithNoGetter objWithNoGetter = await JsonSerializerWrapperForString.DeserializeWrapper<ClassWithNoGetter>(
@"{""MyString"":""Hello"",""MyIntArray"":[0],""MyIntList"":[0]}");
Assert.Equal("Hello", objWithNoGetter.GetMyString());
// Currently we don't support setters without getters.
Assert.Equal(0, objWithNoGetter.GetMyIntArray().Length);
Assert.Equal(0, objWithNoGetter.GetMyIntList().Count);
}

and for value types:
Assert.Equal(2f, obj.GetMyFloat);
Assert.Equal(new Uri("https://microsoft.com"), obj.MyUri);

Again, I don't have context as to why we're doing this, but likely we're closing this as by-design behavior.

@Georgiks
Copy link
Author

Understood, thanks anyway.

@eiriktsarpalis
Copy link
Member

After private discussion with @layomia we have concluded that we should change the DetermineSerializationCapabilities method to not distinguish between collection and object/value converters.

That being said, it is a fix that probably doesn't meet the bug bar for RC1 since a) it affects a relatively rare corner case and b) it can be easily worked around by adding a JsonInclude attribute to the property. Making changes here carries a nonzero probability of introducing unanticipated breaks, so I'm moving this to 7.0.0.

@eiriktsarpalis eiriktsarpalis modified the milestones: 6.0.0, 5.0.x, 7.0.0 Jul 28, 2021
@dvdvorle
Copy link

dvdvorle commented Dec 8, 2021

Just documenting a real-life use case here to make sure this makes it into 7.0.0

I'm using a write-only property for refactoring purposes such as property renames and the like, where the object is persisted as json.

[Obsolete("Don't use this, only for data-migration purposes.")]
public List<EnvironmentHistoryItem> EnvironmentHistory
{
  set => EnvironmentHistoryV2 = new EnvironmentHistory { Items = value };
}

public EnvironmentHistory EnvironmentHistoryV2 { get; set; } = new EnvironmentHistory();

I didn't expect this bug, took me a couple of hours to find it (and this github issue), and Newtonsoft also deserializes this correctly.

The JsonInclude workaround only applies if you have a private getter btw, if you have no getter at all it won't work, even with the JsonInclude attribute.

@eiriktsarpalis
Copy link
Member

eiriktsarpalis commented May 10, 2022

We won't have time to work on this for .NET 7, moving to Future.

@eiriktsarpalis
Copy link
Member

FWIW we passed an opportunity to fix this behavior when refactoring for .NET 7 work. Even though the current behavior is inconsistent, we felt it would be too much of breaking change to justify fixing. We might revisit in the future assuming there is demand.

@layomia
Copy link
Contributor

layomia commented Dec 2, 2022

This overlaps with feature #78556. The major scenario for modifying/populating properties (rather than replacing them with new instances) is for collections. FYI @krwq.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

4 participants