Interfaces de coulée pour la désérialisation dans JSON.NET

je suis en train d'essayer de mettre en place un lecteur qui va prendre dans les objets JSON de divers sites Web (penser le raclage de l'information) et les traduire en C# objets. J'utilise JSON.NET pour le processus de désérialisation. Le problème que je rencontre est qu'il ne sait pas comment gérer les propriétés au niveau de l'interface dans une classe. Ainsi quelque chose de la nature:

public IThingy Thing

produira l'erreur:

ne pouvait pas créer un instance de type IThingy. Type est une interface ou une classe abstraite et ne peut pas être instanciée.

il est relativement important que ce soit une IThingy plutôt qu'un truc puisque le code sur lequel je travaille est considéré comme sensible et que les tests unitaires sont très importants. Se moquer des objets pour des scripts de test atomique n'est pas possible avec des objets à part entière comme Thingy. Ils doivent être une interface.

JSON.NET's de la documentation pour un certain temps maintenant, et les questions que j'ai pu trouver sur ce site ce sont tous de plus d'un an. Toute aide?

aussi, si cela importe, mon application est écrite en .net 4.0.

93
demandé sur Ray Hayes 2011-04-25 21:10:53

15 réponses

@SamualDavis a fourni une excellente solution dans une question connexe , que je vais résumer ici.

si vous devez desérialiser un jet de JSON dans une classe de béton qui a des propriétés d'interface, vous pouvez inclure les classes de béton comme paramètres à un constructeur pour la classe! le Newtonsoft deserializer est assez intelligent pour comprendre qu'il a besoin d'utiliser ces classes de béton pour desérialiser les propriétés.

voici un exemple:

public class Visit : IVisit
{
    /// <summary>
    /// This constructor is required for the JSON deserializer to be able
    /// to identify concrete classes to use when deserializing the interface properties.
    /// </summary>
    public Visit(MyLocation location, Guest guest)
    {
        Location = location;
        Guest = guest;
    }
    public long VisitId { get; set; }
    public ILocation Location { get;  set; }
    public DateTime VisitDate { get; set; }
    public IGuest Guest { get; set; }
}
88
répondu Mark Meuer 2017-05-23 12:02:56

(copié de this question )

dans les cas où je n'ai pas eu le contrôle sur le JSON entrant (et ne peux donc pas s'assurer qu'il inclut une propriété $type) j'ai écrit un convertisseur personnalisé qui vous permet juste de spécifier explicitement le type de béton:

public class Model
{
    [JsonConverter(typeof(ConcreteTypeConverter<Something>))]
    public ISomething TheThing { get; set; }
}

cela utilise juste l'implémentation par défaut de serializer de Json.Net tout en spécifiant explicitement le type de béton.

An vue d'ensemble sont disponibles sur ce billet de blog . Le code Source est le suivant:

public class ConcreteTypeConverter<TConcrete> : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        //assume we can convert to anything for now
        return true;
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        //explicitly specify the concrete type we want to create
        return serializer.Deserialize<TConcrete>(reader);
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        //use the default serialization - it works fine
        serializer.Serialize(writer, value);
    }
}
41
répondu Steve Greatrex 2017-05-23 11:55:10

pour permettre la desérialisation de multiples implémentations d'interfaces, vous pouvez utiliser JsonConverter, mais pas à travers un attribut:

Newtonsoft.Json.JsonSerializer serializer = new Newtonsoft.Json.JsonSerializer();
serializer.Converters.Add(new DTOJsonConverter());
Interfaces.IEntity entity = serializer.Deserialize(jsonReader);

dtojsonconverter cartographie chaque interface avec une mise en œuvre concrète:

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);
    }
}
Le DTOJsonConverter

n'est nécessaire que pour le désérialiseur. Le processus de sérialisation est inchangé. L'objet Json n'a pas besoin d'intégrer des noms de types concrets.

Ce DONC post offre la même solution un pas plus loin avec un JsonConverter Générique.

34
répondu Eric Boumendil 2017-05-23 12:18:24

Pourquoi utiliser un convertisseur? Il y a une fonctionnalité native dans Newtonsoft.Json pour résoudre ce problème exact:

Set TypeNameHandling dans le JsonSerializerSettings à TypeNameHandling.Auto

JsonConvert.SerializeObject(
        toSerialize,
        new JsonSerializerSettings()
        {
          TypeNameHandling = TypeNameHandling.Auto
        });

Cela mettra chaque type dans le json, qui n'est pas considéré comme un béton instance d'un type, mais comme une interface ou une classe abstraite.

je l'ai testé et il fonctionne comme un charme, même avec des listes.

Source et an mise en œuvre manuelle alternative: Code Inside Blog

28
répondu Mafii 2017-10-16 08:36:30

j'ai trouvé ça utile. Vous pourriez aussi.

Exemple D'Usage

public class Parent
{
    [JsonConverter(typeof(InterfaceConverter<IChildModel, ChildModel>))]
    IChildModel Child { get; set; }
}

Création Personnalisée Converter

public class InterfaceConverter<TInterface, TConcrete> : CustomCreationConverter<TInterface>
    where TConcrete : TInterface, new()
{
    public override TInterface Create(Type objectType)
    {
        return new TConcrete();
    }
}

Json.NET documentation

8
répondu smiggleworth 2016-03-21 18:01:37

deux choses que vous pourriez essayer:

mettre en Œuvre un try/parse modèle:

public class Organisation {
  public string Name { get; set; }

  [JsonConverter(typeof(RichDudeConverter))]
  public IPerson Owner { get; set; }
}

public interface IPerson {
  string Name { get; set; }
}

public class Tycoon : IPerson {
  public string Name { get; set; }
}

public class Magnate : IPerson {
  public string Name { get; set; }
  public string IndustryName { get; set; }
}

public class Heir: IPerson {
  public string Name { get; set; }
  public IPerson Benefactor { get; set; }
}

public class RichDudeConverter : JsonConverter
{
  public override bool CanConvert(Type objectType)
  {
    return (objectType == typeof(IPerson));
  }

  public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
  {
    // pseudo-code
    object richDude = serializer.Deserialize<Heir>(reader);

    if (richDude == null)
    {
        richDude = serializer.Deserialize<Magnate>(reader);
    }

    if (richDude == null)
    {
        richDude = serializer.Deserialize<Tycoon>(reader);
    }

    return richDude;
  }

  public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
  {
    // Left as an exercise to the reader :)
    throw new NotImplementedException();
  }
}

ou, si vous pouvez le faire dans votre modèle d'objet, mettez en œuvre une classe de base concrète entre IPerson et vos objets leaf, et désérialisez à elle.

le premier peut potentiellement échouer à l'exécution, le second nécessite des modifications à votre modèle d'objet et homogénéise la sortie au plus petit dénominateur commun.

5
répondu mcw0933 2011-04-25 18:10:01

pour ceux qui pourraient être curieux au sujet du ConcreteListTypeConverter qui a été référencé par Oliver, voici ma tentative:

public class ConcreteListTypeConverter<TInterface, TImplementation> : JsonConverter where TImplementation : TInterface 
{
    public override bool CanConvert(Type objectType)
    {
        return true;
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        var res = serializer.Deserialize<List<TImplementation>>(reader);
        return res.ConvertAll(x => (TInterface) x);
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        serializer.Serialize(writer, value);
    }
}
4
répondu Matt M 2015-06-10 19:27:28

supposons un réglage automatique comme celui-ci:

public class AutofacContractResolver : DefaultContractResolver
{
    private readonly IContainer _container;

    public AutofacContractResolver(IContainer container)
    {
        _container = container;
    }

    protected override JsonObjectContract CreateObjectContract(Type objectType)
    {
        JsonObjectContract contract = base.CreateObjectContract(objectType);

        // use Autofac to create types that have been registered with it
        if (_container.IsRegistered(objectType))
        {
           contract.DefaultCreator = () => _container.Resolve(objectType);
        }  

        return contract;
    }
}

alors, supposons que votre classe soit comme ceci:

public class TaskController
{
    private readonly ITaskRepository _repository;
    private readonly ILogger _logger;

    public TaskController(ITaskRepository repository, ILogger logger)
    {
        _repository = repository;
        _logger = logger;
    }

    public ITaskRepository Repository
    {
        get { return _repository; }
    }

    public ILogger Logger
    {
        get { return _logger; }
    }
}

par conséquent, l'utilisation du résolveur dans la desérialisation pourrait être comme:

ContainerBuilder builder = new ContainerBuilder();
builder.RegisterType<TaskRepository>().As<ITaskRepository>();
builder.RegisterType<TaskController>();
builder.Register(c => new LogService(new DateTime(2000, 12, 12))).As<ILogger>();

IContainer container = builder.Build();

AutofacContractResolver contractResolver = new AutofacContractResolver(container);

string json = @"{
      'Logger': {
        'Level':'Debug'
      }
}";

// ITaskRespository and ILogger constructor parameters are injected by Autofac 
TaskController controller = JsonConvert.DeserializeObject<TaskController>(json, new JsonSerializerSettings
{
    ContractResolver = contractResolver
});

Console.WriteLine(controller.Repository.GetType().Name);

vous pouvez voir plus de détails dans http://www.newtonsoft.com/json/help/html/DeserializeWithDependencyInjection.htm

4
répondu OmG 2017-11-23 15:47:30

utiliser cette classe, pour cartographier le type abstrait au type réel:

public class AbstractConverter<TReal, TAbstract> : JsonConverter
{
    public override Boolean CanConvert(Type objectType) 
        => objectType == typeof(TAbstract);

    public override Object ReadJson(JsonReader reader, Type type, Object value, JsonSerializer jser) 
        => jser.Deserialize<TReal>(reader);

    public override void WriteJson(JsonWriter writer, Object value, JsonSerializer jser) 
        => jser.Serialize(writer, value);
}

...et quand désérialiser:

        var settings = new JsonSerializerSettings
        {
            Converters = {
                new AbstractConverter<Thing, IThingy>(),
                new AbstractConverter<Thing2, IThingy2>()
            },
        };

        JsonConvert.DeserializeObject(json, type, settings);
3
répondu Gildor 2018-02-22 09:04:08

pour ce que ça vaut, j'ai fini par devoir gérer ça moi-même pour la plupart. Chaque objet a une méthode Deserialize(string jsonStream) . Quelques extraits:

JObject parsedJson = this.ParseJson(jsonStream);
object thingyObjectJson = (object)parsedJson["thing"];
this.Thing = new Thingy(Convert.ToString(thingyObjectJson));

Dans ce cas, nouveau "Truc" (string) est un constructeur qui va appeler le Deserialize(string jsonStream) méthode du type de béton. Ce régime continuera à baisser jusqu'à ce que vous obtenez aux points de base que json.NET je peux gérer.

this.Name = (string)parsedJson["name"];
this.CreatedTime = DateTime.Parse((string)parsedJson["created_time"]);

et ainsi de suite. Cette installation m'a permis de donner json.NET setups il peut manipuler sans avoir à remanier une grande partie de la bibliothèque elle-même ou en utilisant des modèles d'essai/parse lourds qui auraient embourbé notre bibliothèque entière en raison du nombre d'objets impliqués. Cela signifie également que je peux gérer efficacement toute modification json sur un objet spécifique, et je n'ai pas besoin de me soucier de tout ce qui objet touche. Ce n'est pas la solution idéale, mais elle fonctionne très bien à partir de nos tests d'intégration et d'unité.

2
répondu tmesser 2011-05-19 14:35:22

Plusieurs années et j'ai eu un problème similaire. Dans mon cas, il y avait des interfaces très imbriquées et une préférence pour la génération de classes concrètes à l'exécution de sorte qu'elle fonctionnerait avec une classe générique.

j'ai décidé de créer une classe proxy à l'exécution qui enveloppe l'objet retourné par Newtonsoft.

L'avantage de cette approche est qu'il ne nécessite pas une mise en œuvre concrète de la classe et peut gérer n'importe quelle profondeur d'imbrication interfaces automatiques. Vous pouvez en voir plus sur mon blog .

using Castle.DynamicProxy;
using Newtonsoft.Json.Linq;
using System;
using System.Reflection;

namespace LL.Utilities.Std.Json
{
    public static class JObjectExtension
    {
        private static ProxyGenerator _generator = new ProxyGenerator();

        public static dynamic toProxy(this JObject targetObject, Type interfaceType) 
        {
            return _generator.CreateInterfaceProxyWithoutTarget(interfaceType, new JObjectInterceptor(targetObject));
        }

        public static InterfaceType toProxy<InterfaceType>(this JObject targetObject)
        {

            return toProxy(targetObject, typeof(InterfaceType));
        }
    }

    [Serializable]
    public class JObjectInterceptor : IInterceptor
    {
        private JObject _target;

        public JObjectInterceptor(JObject target)
        {
            _target = target;
        }
        public void Intercept(IInvocation invocation)
        {

            var methodName = invocation.Method.Name;
            if(invocation.Method.IsSpecialName && methodName.StartsWith("get_"))
            {
                var returnType = invocation.Method.ReturnType;
                methodName = methodName.Substring(4);

                if (_target == null || _target[methodName] == null)
                {
                    if (returnType.GetTypeInfo().IsPrimitive || returnType.Equals(typeof(string)))
                    {

                        invocation.ReturnValue = null;
                        return;
                    }

                }

                if (returnType.GetTypeInfo().IsPrimitive || returnType.Equals(typeof(string)))
                {
                    invocation.ReturnValue = _target[methodName].ToObject(returnType);
                }
                else
                {
                    invocation.ReturnValue = ((JObject)_target[methodName]).toProxy(returnType);
                }
            }
            else
            {
                throw new NotImplementedException("Only get accessors are implemented in proxy");
            }

        }
    }



}

Utilisation:

var jObj = JObject.Parse(input);
InterfaceType proxyObject = jObj.toProxy<InterfaceType>();
2
répondu Sudsy 2017-08-12 11:19:43

Nicholas Westby a fourni une excellente solution dans un impressionnant article .

si vous voulez désérialiser JSON vers l'une des nombreuses classes possibles qui implémentent une interface comme celle-ci:

public class Person
{
    public IProfession Profession { get; set; }
}

public interface IProfession
{
    string JobTitle { get; }
}

public class Programming : IProfession
{
    public string JobTitle => "Software Developer";
    public string FavoriteLanguage { get; set; }
}

public class Writing : IProfession
{
    public string JobTitle => "Copywriter";
    public string FavoriteWord { get; set; }
}

public class Samples
{
    public static Person GetProgrammer()
    {
        return new Person()
        {
            Profession = new Programming()
            {
                FavoriteLanguage = "C#"
            }
        };
    }
}

vous pouvez utiliser un convertisseur JSON personnalisé:

public class ProfessionConverter : JsonConverter
{
    public override bool CanWrite => false;
    public override bool CanRead => true;
    public override bool CanConvert(Type objectType)
    {
        return objectType == typeof(IProfession);
    }
    public override void WriteJson(JsonWriter writer,
        object value, JsonSerializer serializer)
    {
        throw new InvalidOperationException("Use default serialization.");
    }

    public override object ReadJson(JsonReader reader,
        Type objectType, object existingValue,
        JsonSerializer serializer)
    {
        var jsonObject = JObject.Load(reader);
        var profession = default(IProfession);
        switch (jsonObject["JobTitle"].Value())
        {
            case "Software Developer":
                profession = new Programming();
                break;
            case "Copywriter":
                profession = new Writing();
                break;
        }
        serializer.Populate(jsonObject.CreateReader(), profession);
        return profession;
    }
}

et vous aurez besoin de décorer la propriété "Profession" avec un attribut JsonConverter pour lui faire savoir d'utiliser votre convertisseur personnalisé:

    public class Person
    {
        [JsonConverter(typeof(ProfessionConverter))]
        public IProfession Profession { get; set; }
    }

Et ensuite, vous pouvez lancer votre classe avec une Interface:

Person person = JsonConvert.DeserializeObject<Person>(jsonString);
2
répondu A. Morel 2018-02-24 11:59:16

ma solution à celui-ci, que j'aime parce qu'il est bien général, est comme suit:

/// <summary>
/// Automagically convert known interfaces to (specific) concrete classes on deserialisation
/// </summary>
public class WithMocksJsonConverter : JsonConverter
{
    /// <summary>
    /// The interfaces I know how to instantiate mapped to the classes with which I shall instantiate them, as a Dictionary.
    /// </summary>
    private readonly Dictionary<Type,Type> conversions = new Dictionary<Type,Type>() { 
        { typeof(IOne), typeof(MockOne) },
        { typeof(ITwo), typeof(MockTwo) },
        { typeof(IThree), typeof(MockThree) },
        { typeof(IFour), typeof(MockFour) }
    };

    /// <summary>
    /// Can I convert an object of this type?
    /// </summary>
    /// <param name="objectType">The type under consideration</param>
    /// <returns>True if I can convert the type under consideration, else false.</returns>
    public override bool CanConvert(Type objectType)
    {
        return conversions.Keys.Contains(objectType);
    }

    /// <summary>
    /// Attempt to read an object of the specified type from this reader.
    /// </summary>
    /// <param name="reader">The reader from which I read.</param>
    /// <param name="objectType">The type of object I'm trying to read, anticipated to be one I can convert.</param>
    /// <param name="existingValue">The existing value of the object being read.</param>
    /// <param name="serializer">The serializer invoking this request.</param>
    /// <returns>An object of the type into which I convert the specified objectType.</returns>
    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        try
        {
            return serializer.Deserialize(reader, this.conversions[objectType]);
        }
        catch (Exception)
        {
            throw new NotSupportedException(string.Format("Type {0} unexpected.", objectType));
        }
    }

    /// <summary>
    /// Not yet implemented.
    /// </summary>
    /// <param name="writer">The writer to which I would write.</param>
    /// <param name="value">The value I am attempting to write.</param>
    /// <param name="serializer">the serializer invoking this request.</param>
    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        throw new NotImplementedException();
    }
}

}

vous pourriez évidemment et trivialement le convertir en un convertisseur encore plus général en ajoutant un constructeur qui a pris un argument de dictionnaire de type avec lequel instancier la variable d'instance de conversions.

1
répondu Simon Brooke 2016-01-21 18:06:35

Sans objet jamais être un IThingy que les interfaces sont toutes, par définition, abstrait.

l'objet que vous avez qui a été sérialisé pour la première fois était d'un certain béton type, en mettant en œuvre l'interface abstract . Vous avez besoin de ce même béton classe relancer les données sérialisées.

L'objet résultant sera de certains types que met en œuvre le abstract interface que vous recherchez.

De la documentation il s'ensuit que vous pouvez utiliser

(Thingy)JsonConvert.DeserializeObject(jsonString, typeof(Thingy));

lors de la désérialisation pour informer JSON.NET sur le béton.

0
répondu Sean Kinsey 2011-04-25 18:01:26

ma solution a été ajoutée les éléments d'interface dans le constructeur.

public class Customer: ICustomer{
     public Customer(Details details){
          Details = details;
     }

     [JsonProperty("Details",NullValueHnadling = NullValueHandling.Ignore)]
     public IDetails Details {get; set;}
}
0
répondu Jorge Santos Neill 2017-10-18 18:19:21