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

Class static field of same type is overridden during de-serialization #3011

Closed
ppilev opened this issue Jan 16, 2025 · 3 comments
Closed

Class static field of same type is overridden during de-serialization #3011

ppilev opened this issue Jan 16, 2025 · 3 comments

Comments

@ppilev
Copy link

ppilev commented Jan 16, 2025

Hello,

I've encountered a strange behavior when de-serializing an instance of a type which has a static field of type same as the enclosing type.
Note I haven't applied any attributes or fancy settings, I only serialize -> deserialize.
Below is simple code demonstrating the issue.

public class Tests
{
    [Test]
    public void Test1()
    {
        Assert.That(Encoding.Default.Type, Is.EqualTo(EncondingType.Hex));
        var model = new Model
        {
            Encoding = new Encoding
            {
                Type = EncondingType.Custom,
            }
        };

        var json = JsonConvert.SerializeObject(model);
        var instance = JsonConvert.DeserializeObject<Model>(json);

        // why my static field value did change after the instance deserialization
        Assert.That(Encoding.Default.Type, Is.EqualTo(EncondingType.Hex));
    }
}

public class Model
{
    public Encoding Encoding { get; init; } = Encoding.Default;
}

public class Encoding
{
    public static readonly Encoding Default = new Encoding
    {
        Type = EncondingType.Hex,
    };

    public required EncondingType Type { get; init; }
}

public enum EncondingType
{
    Hex = 0,
    Beta = 1,
    Custom = 100
}

Any help or workaround is appreciated,
Thanks,

@ppilev ppilev changed the title Class static property of same type is overridden during de-serialization Class static field of same type is overridden during de-serialization Jan 16, 2025
@elgonzo
Copy link

elgonzo commented Jan 17, 2025

Solution to your problem is pretty simple: Configure the deserializer with the ObjectCreationHandling setting set to ObjectCreationHandling.Replace.

You will find quite a few similar issue reports here that are caused by the same default value/behavior of Newtonsoft.Json regarding its ObjectCreationHandling setting (https://www.newtonsoft.com/json/help/html/SerializationSettings.htm#ObjectCreationHandling). If you read the documentation i linked to, you will notice that by default Newtonsoft.Json will reuse existing object instances during deserialization.

On top of that, Newtonsoft.Json isn't really aware of the init modifier for property set accessors (nor any other new-ish modifiers supported by modern .NET) and treats them just like any other set accessor regardless of whether they are declared init instead of set (by virtue of Newtonsoft.Json being entirely reflection-based).

So, if you are deserializing a Model instance, Newtonsoft.Json creates a new Model instance, then inspects its Encoding property and finds it having an instance value (the Encoding.Default instance). It then proceeds reusing that instance during deserialization and populates it with the respective data from the json input. Et voila - the deserializer has just successfully modified your Encoding.Default instance.

It's technically not a bug but still very unfortunate default behavior that quite a few unsuspecting users were and are surprised of.

I am not involved in maintaining Newtonsoft.Json, but in my frank personal opinion, the default behavior of the deserializer should have been "Replace" from the get go, but considering Newtonsoft.Json's age and wide-spread install base, it's unfortunately much too late for a change of such a default behavior to not risk breaking countless deployments out there in the world.

@ppilev
Copy link
Author

ppilev commented Jan 17, 2025

hello @elgonzo
thank you for explaining it.

if I got it right this behavior emerged due to this line which has already assigned some instance to that property before even de-serialization kicks in:

public Encoding Encoding { get; init; } = Encoding.Default;

so de-serializer just reuse that instance (in our case Encoding.Default) and overrides the existing values with whatever it was given with in the json

@elgonzo
Copy link

elgonzo commented Jan 17, 2025

Yup, exactly like that.

@ppilev ppilev closed this as completed Jan 17, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants