简体   繁体   中英

Custom JsonConverter ignored when deserializing item in array

Edit: made an easier and more transparant sample case

I'm trying to deserialize an array of components (that belong to an entity). One of the components is a Sprite component, which holds a texture and animation information. I've implemented a CustomConverter for this, since the original sprite class is kind of bloated, and also does not have a parameterless constructor (the class is from a seperate library so I can't modify it).

The actual use-case is a bit more complicated, but I've added a similar sample down below. I tested the code and the same problem occurs. ReadJson is never used when Deserializing. But when serializing WriteJson does get called perfectly fine.

these are the components and the custom converter for it;

 public class ComponentSample
{
    int entityID;
}


public class Rect
{
    public int x;
    public int y;
    public int width;
    public int height;

    public Rect( int x, int y, int width, int height )
    {
        this.x = x;
        this.y = y;
        this.width = width;
        this.height = height;
    }
}

//this class would normally have a texture and a bunch of other data that is hard to serialize
//so we will use a jsonconverter to instead point to an atlas that contains the texture's path and misc data
public class SpriteSample<TEnum> : ComponentSample
{
    Dictionary<TEnum, Rect[]> animations = new Dictionary<TEnum, Rect[]>();

    public SpriteSample( TEnum animationKey, Rect[] frames )
    {
        this.animations.Add( animationKey, frames );
    }
}

public class SpriteSampleConverter : JsonConverter<SpriteSample<int>>
{
    public override SpriteSample<int> ReadJson( JsonReader reader, Type objectType, SpriteSample<int> existingValue, bool hasExistingValue, JsonSerializer serializer )
    {
        JObject jsonObj = JObject.Load( reader );

        //get texturepacker atlas
        string atlasPath = jsonObj.Value<String>( "atlasPath" );

        //some wizardy to get all the animation and load the texture and stuff
        //for simplicity sake I'll just put in some random data
        return new SpriteSample<int>( 99, new Rect[ 1 ] { new Rect( 0, 0, 16, 16 ) } );
    }

    public override void WriteJson( JsonWriter writer, SpriteSample<int> value, JsonSerializer serializer )
    {
        writer.WriteStartObject();

        writer.WritePropertyName( "$type" );
        //actually don't know how to get the type, so I just serialized the SpriteSample<int> to check
        writer.WriteValue( "JsonSample.SpriteSample`1[[System.Int32, mscorlib]], NezHoorn" );

        writer.WritePropertyName( "animationAtlas" );
        writer.WriteValue( "sampleAtlasPathGoesHere" );

        writer.WriteEndObject();
    }
}

It generates the Json correctly when Serializing;

        JsonSerializer serializer = new JsonSerializer();

        JsonSerializerSettings settings = new JsonSerializerSettings();
        settings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Serialize;
        settings.PreserveReferencesHandling = PreserveReferencesHandling.All; //none
        settings.TypeNameHandling = TypeNameHandling.All;
        settings.Formatting = Formatting.Indented;
        settings.MissingMemberHandling = MissingMemberHandling.Ignore;
        settings.DefaultValueHandling = DefaultValueHandling.Ignore;
        settings.ConstructorHandling = ConstructorHandling.AllowNonPublicDefaultConstructor;
        settings.Converters.Add( new SpriteSampleConverter() );

        ComponentSample[] components = new ComponentSample[]
        {
            new ComponentSample(),
            new ComponentSample(),
            new SpriteSample<int>(10, new Rect[] { new Rect(0,0,32,32 ) } )
        };

        string fullFile = "sample.json";

        string directoryPath = Path.GetDirectoryName( fullFile );
        if ( directoryPath != "" )
            Directory.CreateDirectory( directoryPath );

        using ( StreamWriter file = File.CreateText( fullFile ) )
        {
            string jsonString = JsonConvert.SerializeObject( components, settings );
            file.Write( jsonString );
        }

Json:

{
  "$id": "1",
  "$type": "JsonSample.ComponentSample[], NezHoorn",
  "$values": [
    {
      "$id": "2",
      "$type": "JsonSample.ComponentSample, NezHoorn"
    },
    {
      "$id": "3",
      "$type": "JsonSample.ComponentSample, NezHoorn"
    },
    {
      "$type": "JsonSample.SpriteSample`1[[System.Int32, mscorlib]], NezHoorn",
      "animationAtlas": "sampleAtlasPathGoesHere"
    }
  ]
}

But when I try to to deserialize the list it never calls ReadJson on the SpriteSampleConverter and just tries to Deserialize the object as is.

        JsonSerializerSettings settings = new JsonSerializerSettings();
        settings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore;
        settings.PreserveReferencesHandling = PreserveReferencesHandling.All;
        settings.TypeNameHandling = TypeNameHandling.All;
        settings.Formatting = Formatting.Indented;
        settings.MissingMemberHandling = MissingMemberHandling.Ignore;
        settings.NullValueHandling = NullValueHandling.Ignore;
        settings.DefaultValueHandling = DefaultValueHandling.Ignore;
        settings.ConstructorHandling = ConstructorHandling.AllowNonPublicDefaultConstructor;

        settings.Converters.Add( new SpriteSampleConverter() );

        using ( StreamReader file = File.OpenText( "sample.json" ) )
        {
            //JsonConvert.PopulateObject( file.ReadToEnd(), man, settings );
            JObject componentsJson = JObject.Parse( file.ReadToEnd() );
            //ComponentList components = JsonConvert.DeserializeObject<ComponentList>( componentsJson.ToString(), settings );
            JArray array = JArray.Parse( componentsJson.GetValue( "$values" ).ToString() );
            ComponentSample[] list = JsonConvert.DeserializeObject<ComponentSample[]>( array.ToString(), settings );

            //The SpriteSampleConverter does work here!
            SpriteSample<int> deserializedSprite = JsonConvert.DeserializeObject<SpriteSample<int>>( componentsJson.GetValue( "$values" ).ElementAt(2).ToString(), settings );
        }

I did a quick test to see if the SpriteSampleConverter works, and ReadJson does get called here;

SpriteSample deserializedSprite = JsonConvert.DeserializeObject>( componentsJson.GetValue( "$values" ).ElementAt(2).ToString(), settings );

This is no valid solution though as I cannot know if/where the object will have a sprite component. I'm guessing Deserializing to Component[] makes the serializer just use the defeault converter? Any ideas what I could be doing wrong?

Edit I've just tried a non-generic JsonConverter to see if CanConvert is called, and surprisingly it's called when checking the type ComponentSample[] and ComponentSample but SpriteSample never goes through the check.

public class SpriteSampleConverterTwo : JsonConverter
{
    public override bool CanConvert( Type objectType )
    {
        return objectType == typeof( SpriteSample<int> );
    }

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

        //get texturepacker atlas
        string atlasPath = jsonObj.Value<String>( "atlasPath" );

        //some wizardy to get all the animation and load the texture and stuff
        //for simplicity sake I'll just put in some random data
        return new SpriteSample<int>( 99, new Rect[ 1 ] { new Rect( 0, 0, 16, 16 ) } );
    }

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

        writer.WritePropertyName( "$type" );
        //actually don't know how to get the type, so I just serialized the SpriteSample<int> to check
        writer.WriteValue( "JsonSample.SpriteSample`1[[System.Int32, mscorlib]], NezHoorn" );

        writer.WritePropertyName( "animationAtlas" );
        writer.WriteValue( "sampleAtlasPathGoesHere" );

        writer.WriteEndObject();
    }
}

I wish II could look at the source of json.net but I'm heaving a heap of trouble getting that to run.

I took a look at the json.net source code and came to the conclusion that only the array's declared type will be used to check for converters;

        JsonConverter collectionItemConverter = GetConverter(contract.ItemContract, null, contract, containerProperty);

        int? previousErrorIndex = null;

        bool finished = false;
        do
        {
            try
            {
                if (reader.ReadForType(contract.ItemContract, collectionItemConverter != null))
                {
                    switch (reader.TokenType)
                    {
                        case JsonToken.EndArray:
                            finished = true;
                            break;
                        case JsonToken.Comment:
                            break;
                        default:
                            object value;

                            if (collectionItemConverter != null && collectionItemConverter.CanRead)
                            {
                                value = DeserializeConvertable(collectionItemConverter, reader, contract.CollectionItemType, null);
                            }

It will not check for each individual array item's type to look for the corresponding converter.

So I will need to find a different solution than using a converter.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM