Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

JSON.Net not calling CanConvert for collection item?

Tags:

c#

json.net

I have a converter that I only want used when deserializing. So I set CanWrite to false, which works fine and everything Serializes fine. The Json string then contains an object graph within which there is a SantaClauseCollection with an array of SantaClause items and a $type indicating they are concrete type SantaClause.

However, when it encounters a collection of SantaClaus while deserializing, it never calls CanConvert(I have a break point and see the SantaClausCollection, hit F5 to continue, which should then hit the break point again when encountering an item in the collection of SantaClaus, but it doesn't). It's not trying to call CanConvert when it gets to the SantaClaus item. Without even calling CanConvert for that item to check if my converter will handle it, it instead tries to deserialize it itself, which won't work because the class has no default constructor and no constructor with property-name matching conventions:

Unable to find a constructor to use for type SantaClaus. A class should either have a default constructor, one constructor with arguments or a constructor marked with the JsonConstructor attribute.

I understand why I get this error, but the problem is it indicates that Json.net tried to deserialize the object, instead of calling CanConvert to check and see if my converter wanted to handle the deserialization instead.

Why is CanConvert not being called for each item in the collection?

My converter:

class SantaClaus2JsonConverter : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        return objectType == typeof(SantaClaus);          
    }

    /// <summary>
    /// Deserializes a SantaClaus as a SantaClausEx which has a matching constructor that allows it to deserialize naturally.
    /// </summary>       
    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        return serializer.Deserialize<SantaClausEx>(reader);
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        serializer.Serialize(writer, value);
    }


    public override bool CanRead
    {
        get
        {
            return true;
        }
    }

    public override bool CanWrite
    {
        get
        {
            return false;//We only need this converter when reading.
        }
    }

}

SantaClausEx is just inherits from SantaClaus to add a constructor with a renamed parameter to match properties:

class SantaClaus //a third party class I can't modify
{
    string Name {get;set;}
    public SantaClaus(string santaClauseName) { this.Name = santaClauseName }
}

class SantaClausEx:SantaClaus 
{
    //provide a constructor with param names matching property names
    public SantaClausEx(string name) : base(name)
}

Json.net can't deserialize a SantaClaus, but it can deserialize a SantaClauseEx.

I use that SantaClauseEx class everywhere and it works just fine, but I wanted to make a converter to do this automatically.

This is what the Json looks like for the collection:

SantaClausCollection: [
{
  $type: "SantaClaus, XMasClasses.NET20"
  Name: "St. Bob"
},
{
  $type: "SantaClaus, XMasClasses.NET20"
  Name: "St. Jim"
}
]
like image 288
AaronLS Avatar asked Nov 03 '22 04:11

AaronLS


1 Answers

I suppose that you added your converter to Converters collection in settings object.

I wrote simple test with converter which works

public class SantaClausJsonTest
{
    public SantaClausJsonTest()
    {
        Settings = new JsonSerializerSettings();
        Settings.TypeNameHandling = TypeNameHandling.Objects;
        Settings.Converters.Add(new SantaClaus2JsonConverter());
    }

    private JsonSerializerSettings Settings;

    [Fact]
    public void SerializeAndDeserialize()
    {
        var collection = new []
            {
                new SantaClaus("St. Bob"),
                new SantaClaus("St. Jim"),
            };

        var serialized = JsonConvert.SerializeObject(collection, Settings);

        Console.WriteLine(serialized);
        Assert.False(string.IsNullOrEmpty(serialized));

        var deserialized = JsonConvert.DeserializeObject<SantaClaus[]>(serialized, Settings);

        Console.WriteLine(deserialized.GetType().ToString());
        Assert.NotNull(deserialized);
        Assert.True(deserialized.Any(a => a.Name == "St. Bob"));
        Assert.True(deserialized.Any(a => a.Name == "St. Jim"));
    }
}

public class SantaClaus
{
    public SantaClaus(string santaClauseName)
    {
        Name = santaClauseName;
    }

    public string Name { get; private set; }
}

public class SantaClaus2JsonConverter : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        return objectType == typeof(SantaClaus);
    }

    /// <summary>
    /// Deserializes a SantaClaus as a SantaClausEx which has a matching constructor that allows it to deserialize naturally.
    /// </summary>       
    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        var name = string.Empty;

        while (reader.Read())
        {
            if (reader.TokenType == JsonToken.String && reader.Path.EndsWith("Name"))
            {
                name = reader.Value as string;
            }
            if (reader.TokenType == JsonToken.EndObject)
            {
                break;
            }
        }

        return Activator.CreateInstance(objectType, name);
    }

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


    public override bool CanRead
    {
        get
        {
            return true;
        }
    }

    public override bool CanWrite
    {
        get
        {
            return false;//We only need this converter when reading.
        }
    }
like image 158
Rudis Avatar answered Nov 15 '22 05:11

Rudis