Big McLargeHuge
Big McLargeHuge

Reputation: 16056

How do I (de)serialize a list of type that requires a custom JsonConverter?

I have a class that needs serializing/deserializing. It looks like this:

public class Animation
{
    [JsonProperty]
    private readonly string name;

    [JsonProperty]
    private readonly IList<Rectangle> frames;
}

However, Json.Net doesn't play nice with XNA's Rectangle class. It needs a custom JsonConverter, which I managed to scrape together. This works fine on classes with a single Rectangle property, like:

public class Sprite
{
    [JsonConverter(typeof(RectangleConverter))]
    [JsonProperty]
    private readonly Rectangle frame;
}

But how do I apply that converter to the list of Rectangles in my Animation class?

Upvotes: 3

Views: 2766

Answers (2)

Brian Rogers
Brian Rogers

Reputation: 129787

According to the doc, the XNA Rectangle struct is marked as having a TypeConverter, and by default type converters always return true when asked to convert to string. Because of this Json.Net will by default try to create a string contract instead of an object contract, which will lead to an error when used with your RectangleConverter. This can be fixed by using a custom ContractResolver to tell Json.Net that Rectangle should be treated as an object, not a string. We can also set the converter inside the resolver, so you do not need to decorate your class properties with a [JsonConverter] attribute wherever you use a Rectangle.

Here is the code you would need:

class CustomResolver : DefaultContractResolver
{
    protected override JsonContract CreateContract(Type objectType)
    {
        if (objectType == typeof(Rectangle) || objectType == typeof(Rectangle?))
        {
            JsonContract contract = base.CreateObjectContract(objectType);
            contract.Converter = new RectangleConverter();
            return contract;
        }
        return base.CreateContract(objectType);
    }
}

To use the resolver, add it to the serializer settings and pass that to DeserializeObject() or SerializeObject() like this:

JsonSerializerSettings settings = new JsonSerializerSettings();
settings.ContractResolver = new CustomResolver();

Animation anim = JsonConvert.DeserializeObject<Animation>(json, settings);

Upvotes: 2

Big McLargeHuge
Big McLargeHuge

Reputation: 16056

Hopefully someone comes up with a better solution for me (there's got to be one), but this is working for me right now:

public class RectangleListConverter : RectangleConverter
{
    public override void WriteJson( JsonWriter writer, object value, JsonSerializer serializer )
    {
        var rectangleList = (IList<Rectangle>)value;

        var jArray = new JArray();

        foreach ( var rectangle in rectangleList )
        {
            jArray.Add( GetObject( rectangle ) );
        }

        jArray.WriteTo( writer );
    }

    public override object ReadJson( JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer )
    {
        var rectangleList = new List<Rectangle>();

        var jArray = JArray.Load( reader );

        foreach ( var jToken in jArray )
        {
            rectangleList.Add( GetRectangle( jToken ) );
        }

        return rectangleList;
    }

    public override bool CanConvert( Type objectType )
    {
        throw new NotImplementedException();
    }
}

I had to modify the RectangleConverter I was using:

public class RectangleConverter : JsonConverter
{
    public override void WriteJson( JsonWriter writer, object value, JsonSerializer serializer )
    {
        var rectangle = (Rectangle)value;

        var jObject = GetObject( rectangle );

        jObject.WriteTo( writer );
    }

    public override object ReadJson( JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer )
    {
        var jObject = JObject.Load( reader );

        return GetRectangle( jObject );
    }

    public override bool CanConvert( Type objectType )
    {
        throw new NotImplementedException();
    }

    protected static JObject GetObject( Rectangle rectangle )
    {
        var x = rectangle.X;
        var y = rectangle.Y;
        var width = rectangle.Width;
        var height = rectangle.Height;

        return JObject.FromObject( new { x, y, width, height } );
    }

    protected static Rectangle GetRectangle( JObject jObject )
    {
        var x = GetTokenValue( jObject, "x" ) ?? 0;
        var y = GetTokenValue( jObject, "y" ) ?? 0;
        var width = GetTokenValue( jObject, "width" ) ?? 0;
        var height = GetTokenValue( jObject, "height" ) ?? 0;

        return new Rectangle( x, y, width, height );
    }

    protected static Rectangle GetRectangle( JToken jToken )
    {
        var jObject = JObject.FromObject( jToken );

        return GetRectangle( jObject );
    }

    protected static int? GetTokenValue( JObject jObject, string tokenName )
    {
        JToken jToken;
        return jObject.TryGetValue( tokenName, StringComparison.InvariantCultureIgnoreCase, out jToken ) ? (int)jToken : (int?)null;
    }
}

Animation class with new JsonConverter attribute:

public class Animation
{
    [JsonProperty]
    private readonly string name;

    [JsonConverter(typeof(RectangleListConverter))]
    [JsonProperty]
    private readonly IList<Rectangle> frames;
}

Upvotes: 1

Related Questions