Why is Json.NET trying to deserialise my get only property?

I've been attempting to serialize and deserialize my object in such a way that I am able to specify that certain properties are to be serialized but not deserialized.

Example code as follows:

public interface ISupYo
{
    string Hi { get; }
}
public class SupYo : ISupYo
{
    public string Hi { get; } = "heya";
}

public interface ISup
{
    int hiyo { get; }
}
public class Sup : ISup
{ 
    public Sup(int hiyo)
    {
        this.hiyo = hiyo;
    }

    public int hiyo { get; }
    public ISupYo yo => new SupYo();
}

var myNewSup = JsonConvert.SerializeObject(new Sup(2));
var mySup = JsonConvert.DeserializeObject<Sup>(myNewSup);

If I remove the constructor from class Sup all is well.

But as-is deserialization fails with the following error due to json.net trying to construct the interface ISupYo...

Newtonsoft.Json.JsonSerializationException: 'Could not create an instance of type Scratchpad.Program+ISupYo. Type is an interface or abstract class and cannot be instantiated. Path 'yo.Hi', line 1, position 21.'

I tried out the instructions here Serialize Property, but Do Not Deserialize Property in Json.Net but deserialization fails in the same way.

Using a JsonConverter in this way http://pmichaels.net/tag/type-is-an-interface-or-abstract-class-and-cannot-be-instantiated/ is successful, and so is specifying typeNameHandling and format handling during serialization/deserialization

Why this discrepancy between using/not using the default constructor?

Upvotes: 5

Views: 5188

Answers (1)

dbc
dbc

Reputation: 116710

The cause of the exception you are seeing is an unfortunate interaction of Json.NET functionalities:

  1. If an object being deserialized has a read-only reference type member, Json.NET will populate its value's contents from the JSON stream as long as it is pre-allocated. This is true even if the declared type of the member is abstract or an interface, as the real object returned must obviously be concrete.

    Sample .Net fiddle demonstrating this here.

  2. If an object being deserialized specifies use of a parameterized constructor, Json.NET will read the entire object from the JSON stream, deserialize all properties to their declared types, then match the deserialized properties to constructor arguments by name (modulo case) and construct the object using the matched, deserialized properties. Finally, any unmatched properties will be set back into the object.

  3. Json.NET is a single-pass deserializer that never goes back to re-read previously read JSON tokens.

Sadly, the first two functionalities don't play well together. If all the properties of a parameterized type must be deserialized before the type can be constructed, there's no possibility to populate a pre-allocated, read-only member from the JSON stream, since the stream has already been read.

What's worse, Json.NET seems to try to deserialize JSON properties that do not correspond to a constructor parameter but do correspond to a read-only member, even though it should probably just skip them. Since your ISupYo yo member is an interface, you get the exception you see (unless you have specified TypeNameHandling, in which case you don't). This might be a bug; you could report an issue if you want. The specific problem seems to be that JsonSerializerInternalReader.ResolvePropertyAndCreatorValues() is missing the check to see that non-constructor properties are Writable.

The simplest workaround will require use of a special JsonConverter, as the abovementioned ResolvePropertyAndCreatorValues() does check for the presence of converters. First, introduce SkipDeserializationConverter:

public class SkipDeserializationConverter : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        throw new NotImplementedException();
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        reader.Skip();
        return existingValue;
    }

    public override bool CanWrite { get { return false; } }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        throw new NotImplementedException();
    }
}

And apply it to your type as follows:

[JsonConverter(typeof(SkipDeserializationConverter))]
public ISupYo yo { get { return new SupYo(); } }

The converter simply skips all children of the token currently being read without attempting to deserialize anything. Using it is probably preferable to using TypeNameHandling since the latter can introduce security risks as explained in TypeNameHandling caution in Newtonsoft Json.

Sample working .Net fiddle.

Upvotes: 10

Related Questions