Json.NET y el misterio de ImageFormat

¡Hola!
El otro día me ocurrió que, deserializando con Json.NET un objeto que contenía una propiedad ImageFormat, de forma cuasi mágica se serializaba/deserializaba correctamente. En concreto necesitaba hacer esto para una pequeña aplicación donde se generan iconos de diferentes tamaños y, cuando ya estaba pensando en hacer un JsonConverter, me di cuenta de que no era necesario.

Tenía una clase que podía ser algo parecido a lo siguiente:

1
2
3
4
5
6
    public class ImageProperties
    {
        public ImageFormat ImageFormat { get; set; }
        public string Path { get; set; }
        public bool OverwriteFile { get; set; }        
    }

Y había preparado un test antes de ponerme a hacer el JsonConverter, parecido al siguiente:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
    [Fact]
    public void SerializeAndDeserializeImageFormatTest()
    {
        ImageProperties imageProperties = new ImageProperties()
        {
            ImageFormat = ImageFormat.Png,
            OverwriteFile = true,
            Path = "/images"
        };
        string imagePropertiesJson = JsonConvert.SerializeObject(imageProperties);
 
        ImageProperties deserializedImageProperties = JsonConvert.DeserializeObject<ImageProperties>(imagePropertiesJson);
 
        Assert.Equal(ImageFormat.Png, deserializedImageProperties.ImageFormat);
    }

El test no era exactamente así, pero queda más claro. Lo que estamos haciendo es, a partir de la propiedad estática Png (de ImageFormat), la convertimos en un JSON, y ese JSON lo serializamos de nuevo. Como iba diciendo, antes de implementar el JsonConverter tenía esto, voy a comprobar que el test falle y…

¡Premio!, el test se pone en verde. Me quedé un poco sorprendido así que me puse a analizarlo un poco.
En el primer paso, se genera un JSON como este:

1
2
3
4
5
    {  
        "ImageFormat":"Png",        "Path":"images",
        "OverwriteFile":true
    }

Vemos que convierte nuestro ImageFormat.Png a «Png»… ya me parecía algo curioso, pero el siguiente paso mucho más. Consigue transformar el «Png» a ImageFormat.Png:

¿Qué está pasando? En un primer momento hasta pensé si Json.NET hacía un comprobación específica para ImageFormat. Después pensé si estaba buscando propiedades con el mismo nombre que el texto «Png» del Json, algo así:

1
2
    ImageFormat imageFormatPng = (ImageFormat)typeof(ImageFormat)
                .GetProperty("Png", BindingFlags.Public | BindingFlags.Static).GetValue(null);

Obviamente esto lo haría de forma genérica (y seguramente mejor), donde pone «Png» sería el valor de la propiedad del JSON y en ningún momento estaría especificado ImageFormat, claro. Una vez sabida la solución, puedo decir que no es esta, pero en algún punto sí que se hace algo parecido (y no lo hace Json.NET).
El siguiente paso fue descargarme el código de Json.NET, crear un test parecido al anterior y empezar a depurar. Puedo asegurar que entre mi viejo portátil, Visual Studio 2017 y Resharper no fue una tarea demasiado agradable 🙂 Después de un rato de sufrimiento depurar, encontré el primer paso, la serialización:

1
2
3
4
5
6
7
8
    internal static bool TryConvertToString(object value, Type type, out string s)
    {
        if (JsonTypeReflector.CanTypeDescriptorConvertString(type, out TypeConverter converter))
        {
            s = converter.ConvertToInvariantString(value);
            return true;
        }
    }

Aunque fuera de contexto y algo simplificado, el método era así. Vamos al siguiente paso (deserialización) y luego explicamos qué es lo que está pasando:

1
2
3
4
5
6
7
8
9
    private static ConvertResult TryConvertInternal(object initialValue, CultureInfo culture, Type targetType, out object value)
    {
        TypeConverter fromConverter = TypeDescriptor.GetConverter(targetType);
        if (fromConverter != null && fromConverter.CanConvertFrom(initialType))
        {
            value = fromConverter.ConvertFrom(null, culture, initialValue);
            return ConvertResult.Success;
        }
    }

Igual que antes, no es el método entero, sólo es la parte que nos interesa.
¿Qué es entonces lo que está haciendo? Pues aprovecharse de la clase TypeConverter. Ignorante de mí, no conocía esta funcionalidad (dentro de System.ComponentModel), aunque seguramente soy el único al que no le sonaba esto 🙂 Echando un vistazo a ImageFormat, podemos ver que está decorada con el siguiente atributo:

1
    [TypeConverterAttribute(typeof(ImageFormatConverter))]

Con esto está diciendo que ImageFormat tiene un convertidor de tipos. La funcionalidad de ese convertidor de tipos reside en ImageFormatConverter, que hereda de TypeConverter. Si echamos un vistazo a TypeConverter vemos que tiene varios métodos que podemos sobreescribir, los que más nos interesan de ImageFormatConverter son CanConvertFrom/ConvertFrom y CanConvertTo/ConvertTo. Estos métodos tienen la siguiente signatura en ImageFormatConverter:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
    public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) 
    {
    }
 
    public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
    {
    }
 
    public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
    {
    }
 
    public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType)
    {
    }

En la clase base, TypeConverter, el override sería un virtual, claro.
CanConvertFrom: comprobará si se puede convertir desde el tipo de origen al de destino. En el caso concreto ImageFormatConverter, comprobará si el tipo de origen es string, en cuyo caso devolverá true, puesto que como vimos, desde el string «Png» ha podido convertir a ImageFormat.
CanConvertTo: comprobará si se puede convertir desde el tipo de destino al de origen. Para ImageFormatConverter, comprobará si el tipo de destino es string, en cuyo caso devolverá true. Como vimos, sí pudo convertir un ImageFormat.Png al string «Png».
ConvertFrom: convierte un object (el parámetro value) al tipo de destino. En el caso de ImageFormatConverter, lo que convierte es un string a ImageFormat. Si recordáis, un poco más arriba comenté por dónde iban los tiros de la solución. Y es que aquí lo que hace es, en caso de que el object recibido sea un string, lo comparara con el nombre de las propiedades (de ImageFormat) que sean públicas y estáticas y, si coincide, devuelve el valor de esa propiedad, que en este caso es ImageFormat.Png.
ConvertTo: convierte un object (el parámetro value) al tipo de origen. En el caso de ImageFormatConverter, lo que convierte es un objeto de tipo ImageFormat a un string. Para ello comprueba si hay alguna propiedad pública y estática dentro de ImageFormat que sea igual al objeto value recibido. En caso afirmativo, es decir, recibamos ImageFormat.Png, ImageFormat.Jpeg, etc., devuelve el nombre del miembro, que sería por ejemplo «Png».
Un último detalle. Si revisamos de nuevo el código de Json.NET que vimos antes, en la parte de obtener el texto asociado a nuestro ImageFormat, utiliza el método del ConvertToInvariantString del converter. Este método es el que transforma ImageFormat.Png a «Png», y este método realmente está usando el método ConvertTo que acabamos de ver.

Al final lo que parecía magia simplemente era ignorancia. Lo que utiliza Json.NET, entre otras muchas cosas, es comprobar si la clase tiene un TypeConverter y utilizarlo. Como comentaba, es la primera vez que veía esta característica y no tengo muy claro si es algo que se utiliza habitualmente o no, tocará echar un vistazo. Me hubiera gustado poner código de ImageFormatConverter pero era código decompilado y no tengo muy claro si debía/podía enseñarlo 🙂

¡Un saludo!

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *