5

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"
}
]
AaronLS
  • 37,329
  • 20
  • 143
  • 202
  • How are you actually doing the convert call? Serialize/Deserialize methods usually contain overloads which accepts converters to use if applicable. Your converter won't magically be called just because it exists, it must be passed in as one of those converters. – Jeff Mercado Nov 26 '13 at 21:56

3 Answers3

1

I had a similar issue deserializing objects inherited from a base class ( similar to how you need to deserialize a SantaClauseEx object but they are all defined as SantaClause objects). The issue is in JSon.Net not being able to identify the subtype.

See stackoverflow.com/questions/8030538/how-to-implement-custom-jsonconverter-in-json-net-to-deserialize-a-list-of-base

Community
  • 1
  • 1
BgRva
  • 1,521
  • 12
  • 26
  • Thanks. "not being able to identify the subtype", the type in the JSON is `$type: "SantaClaus,` which is fine, because my converter is for SantaClaus `CanConvert...typeof(SantaClaus)` and my converter handles converting it to SantaClausEx. I'm not expecting Json.net to magically determine the subtype. It is a problem with the order that Json.net is trying to create an instance of SantaClaus before checking CanConvert. So I believe the solution you linked will indeed work around the problem since I can implement creation logic for the base type instead of using converter, will test it. – AaronLS Nov 26 '13 at 21:28
1

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.
        }
    }
Rudis
  • 1,177
  • 15
  • 30
  • Perfect. Have to wait to award bounty till tomorrow. – AaronLS Nov 26 '13 at 23:32
  • 3
    Be careful. Notice how specifying the type is necessary in DeserializeObject. I've found that for top-level JSON objects, upon deserialization it fails to call the converter unless you explicitly hard-code the type like this one does in the generic `DeserializeObject` method overload. For example, if you had a JSON string like `{"$type":"SantaClaus","Name":"anyname"}` and tried to deserialize it with DeserializeObject, it would construct a SantaClause object, but not via your converter's ReadJson method. It would just instantiate the type and assign the property. A bug, IMO. – Triynko Nov 27 '13 at 05:38
  • @Triynko Good point. I think the lack of the generic type on deserialize was the one thing I was missing with my initial attempt. – AaronLS Dec 02 '13 at 21:27
0

Once I got Rudus's answer working, I identified the issue with my original attempt. His is great when you have a type with no default constructor, but can map property values to one of its other constructors and is certainly easier for my specific case.

If for some reason you really do need something like what I was originally trying to do where you create a different type when deserializing, I was able to get that working.

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)
    {
        //temporarily switch off name handling so it ignores "SantaClaus" type when
        //explicitely deserialize as SantaClausEx
        //This could cause issues with nested types however in a more complicated object graph
        var temp = serializer.TypeNameHandling;
        serializer.TypeNameHandling = TypeNameHandling.None;
        var desr = serializer.Deserialize<SantaClausEx>(reader);
        serializer.TypeNameHandling = temp;//restore previous setting

        return desr;
    }

    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 { false; } } //only for reading

}
AaronLS
  • 37,329
  • 20
  • 143
  • 202