Json.NET: serialisation et deserialisation d’interfaces avec JsonConverter

Ce court billet illustre l’utilisation de la librairie Json.NET pour la sérialisation et la désérialisation d’interfaces. En fait, seule la désérialisation est intéressante puisque Json.NET est capable de sérialiser n’importe quel type d’instance.

Json.NET propose plusieurs scenarii pour contrôler la désérialisation d’interface. Ici, je m’intéresse à un cas précis pour lequel j’utilise un JsonConverter fourni à la classe chargée de la désérialisation:

  • Une entité est transférée entre deux projets indépendants, sous la forme d’un objet Json.
  • Les deux projets ne partagent pas la même implémentation des interfaces utilisées.
  • Les interfaces peuvent avoir de multiples implémentations.

Dans l’exemple ci-dessous, le projet Server génère un objet Json enregistré dans le fichier « foo.json ». Le projet Client lit ce fichier pour désérialiser l’objet. Le nom et le rôle des projets Server et Client n’ont guère d’importance dans ce scenario (le client pourrait très bien sérialiser un objet pour le serveur).

Le serveur sérialise une instance de IEntity dont l’implémentation n’est pas partagée avec le client. La sérialisation d’interfaces ne pose aucun problème particulier.
Le projet Client, lui, a besoin de savoir comment construire l’instance d’une interface. C’est le rôle de la classe abstraite JsonConverter dont une instance est fournie à la classe chargée de désérialiser l’objet Json.

Voici le code complet…

Interfaces (totalement inintéressant):


using System;

namespace Interfaces
{
    public interface IEntity
    {
        string Name { get; set; }

        IScalar Scalar { get; set; }
    }
    
    public interface IScalar
    {
        int Value { get; set; }
    }
}

Serialisation (rien de très intéressant):


using System;
using System.IO;

namespace Server.DTO
{
    class ServerEntity : Interfaces.IEntity
    {
        public ServerEntity(string name, int value)
        {
            Name = name;
            Scalar = new ServerScalar(value);
        }

        public string Name
        {
            get;
            set;
        }

        public Interfaces.IScalar Scalar
        {
            get;
            set;
        }
    }

    class ServerScalar : Interfaces.IScalar
    {
        public ServerScalar(int value)
        {
            Value = value;
        }

        public int Value
        {
            get;
            set;
        }
    }
}

namespace Server
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var entity = new DTO.ServerEntity("foo", 10);
            const string FILENAME = "foo.json";
            using (var writer = new StreamWriter(FILENAME))
            {
                Newtonsoft.Json.JsonSerializer serializer = new Newtonsoft.Json.JsonSerializer();
                serializer.Serialize(writer, entity);
            }
            Console.WriteLine("Entity serialized in " + FILENAME);
            Console.ReadKey();
        }
    }
}

Deserialisation:

Les parties intéressantes sont:

  • la classe DTOJsonConverter
  • Program.Main: serializer.Converters.Add(new DTOJsonConverter())

using System;
using System.IO;

namespace Client
{
    public class Program
    {
        public static void Main(string[] args)
        {
            const string FILENAME = "foo.json";
            try
            {
                if (!File.Exists(FILENAME))
                {
                    Console.WriteLine("File not found: " + FILENAME);
                    return;
                }

                Newtonsoft.Json.JsonSerializer serializer = new Newtonsoft.Json.JsonSerializer();
                serializer.Converters.Add(new DTOJsonConverter());

                Interfaces.IEntity entity = null;
                using (var reader = new Newtonsoft.Json.JsonTextReader(new StreamReader(FILENAME)))
                {
                    entity = serializer.Deserialize(reader);
                }

                Console.WriteLine(string.Format("Entity deserialized: {0}={1}", entity.Name, entity.Scalar.Value));

            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.ToString());
            }
            finally
            {
                Console.ReadKey();
            }
        }
    }

    class DTOJsonConverter : Newtonsoft.Json.JsonConverter
    {
        private static readonly string ISCALAR_FULLNAME = typeof(Interfaces.IScalar).FullName;
        private static readonly string IENTITY_FULLNAME = typeof(Interfaces.IEntity).FullName;


        public override bool CanConvert(Type objectType)
        {
            if (objectType.FullName == ISCALAR_FULLNAME
                || objectType.FullName == IENTITY_FULLNAME)
            {
                return true;
            }
            return false;
        }

        public override object ReadJson(Newtonsoft.Json.JsonReader reader, Type objectType, object existingValue, Newtonsoft.Json.JsonSerializer serializer)
        {
            if (objectType.FullName == ISCALAR_FULLNAME)
                return serializer.Deserialize(reader, typeof(DTO.ClientScalar));
            else if (objectType.FullName == IENTITY_FULLNAME)
                return serializer.Deserialize(reader, typeof(DTO.ClientEntity));

            throw new NotSupportedException(string.Format("Type {0} unexpected.", objectType));
        }

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

namespace Client.DTO
{
    class ClientEntity : Interfaces.IEntity
    {
        public string Name
        {
            get;
            set;
        }

        public Interfaces.IScalar Scalar
        {
            get;
            set;
        }
    }

    class ClientScalar : Interfaces.IScalar
    {
        public int Value
        {
            get;
            set;
        }
    }
}

L’objet Json échangé est le suivant:


{ 
  "Name" : "foo",
  "Scalar" : { "Value" : 10 }
}

Pour aller plus loin…

Cette solution peut être poussée un cran plus loin avec un JsonConverter générique. Voir la réponse sur le forum: http://json.codeplex.com/discussions/56031 (réponse du 18 février 2011).