Utilisation de convertisseurs Json.NET pour désérialiser les propriétés

88

J'ai une définition de classe qui contient une propriété qui renvoie une interface.

public class Foo
{ 
    public int Number { get; set; }

    public ISomething Thing { get; set; }
}

Tenter de sérialiser la classe Foo à l'aide de Json.NET me donne un message d'erreur du type "Impossible de créer une instance de type" ISomething ". ISomething peut être une interface ou une classe abstraite."

Existe-t-il un attribut ou un convertisseur Json.NET qui me permettrait de spécifier une Somethingclasse concrète à utiliser lors de la désérialisation?

dthrasher
la source
Je crois que vous devez spécifier un nom de propriété qui obtient / définit ISomething
ram
J'ai. J'utilise le raccourci pour les propriétés implémentées automatiquement introduites dans C # 3.5. msdn.microsoft.com/en-us/library/bb384054.aspx
dthrasher
4
N'est-ce pas quelque chose du genre. Je pense que ram a raison, vous avez toujours besoin d'un nom de propriété. Je sais que cela n'est pas lié à votre problème, mais votre commentaire ci-dessus m'a fait penser qu'il me manquait une nouvelle fonctionnalité dans .NET qui vous permettait de spécifier une propriété sans nom.
Mr Moose

Réponses:

92

L'une des choses que vous pouvez faire avec Json.NET est:

var settings = new JsonSerializerSettings();
settings.TypeNameHandling = TypeNameHandling.Objects;

JsonConvert.SerializeObject(entity, Formatting.Indented, settings);

L' TypeNameHandlingindicateur ajoutera une $typepropriété au JSON, ce qui permet à Json.NET de savoir dans quel type concret il a besoin pour désérialiser l'objet. Cela vous permet de désérialiser un objet tout en remplissant une interface ou une classe de base abstraite.

L'inconvénient, cependant, est que cela est très spécifique à Json.NET. le$type sera un type complet, donc si vous le sérialisez avec des informations de type, le désérialiseur doit également être en mesure de le comprendre.

Documentation: Paramètres de sérialisation avec Json.NET

Daniel T.
la source
Intéressant. Je vais devoir jouer avec ça. Bon conseil!
dthrasher
2
Pour Newtonsoft.Json, cela fonctionne de manière similaire, mais la propriété est "$ type"
Jaap
C'était trop facile!
Shimmy Weitzhandler
1
Faites attention aux éventuels problèmes de sécurité lors de l'utilisation TypeNameHandling. Voir la mise en garde TypeNameHandling dans Newtonsoft Json pour plus de détails.
dbc
J'ai lutté comme un fou avec les convertisseurs hier, et c'était bien mieux et mieux compréhensible, merci !!!
Horothenic le
52

Vous pouvez y parvenir grâce à l'utilisation de la classe JsonConverter. Supposons que vous ayez une classe avec une propriété d'interface;

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

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

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

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

Votre JsonConverter est responsable de la sérialisation et de la désérialisation de la propriété sous-jacente;

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

  public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
  {
    return serializer.Deserialize<Tycoon>(reader);
  }

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

Lorsque vous travaillez avec une organisation désérialisée via Json.Net, l'adresse IPerson sous-jacente de la propriété Owner sera de type Tycoon.

MrMDavidson
la source
Très agréable. Je vais devoir essayer le convertisseur.
dthrasher
4
La balise "[JsonConverter (typeof (TycoonConverter))]" fonctionnerait-elle toujours si elle figurait sur une liste de l'interface?
Zwik
40

Au lieu de passer un objet JsonSerializerSettings personnalisé à JsonConvert.SerializeObject () avec l'option TypeNameHandling.Objects, comme mentionné précédemment, vous pouvez simplement marquer cette propriété d'interface spécifique avec un attribut afin que le JSON généré ne soit pas gonflé avec les propriétés "$ type" sur CHAQUE objet:

public class Foo
{
    public int Number { get; set; }

    // Add "$type" property containing type info of concrete class.
    [JsonProperty( TypeNameHandling = TypeNameHandling.Objects )]
    public ISomething { get; set; }
}
Erhhung
la source
Brillant. Merci :)
Darren Young
5
Pour les collections d'interfaces ou de classes abstraites, la propriété est "ItemTypeNameHandling". Par exemple: [JsonProperty (ItemTypeNameHandling = TypeNameHandling.Auto)]
Anthony F
Merci pour ça!
brudert
23

Dans la version la plus récente du convertisseur Newtonsoft Json tiers, vous pouvez définir un constructeur avec un type concret relatif à la propriété interfacée.

public class Foo
{ 
    public int Number { get; private set; }

    public ISomething IsSomething { get; private set; }

    public Foo(int number, Something concreteType)
    {
        Number = number;
        IsSomething = concreteType;
    }
}

Tant que Something implémente ISomething, cela devrait fonctionner. Ne placez pas non plus de constructeur vide par défaut au cas où le convertisseur JSon tente de l'utiliser, vous devez le forcer à utiliser le constructeur contenant le type concret.

PS. cela vous permet également de rendre vos setters privés.

SamuelDavis
la source
6
Cela devrait être crié sur les toits! Certes, cela ajoute des contraintes sur la mise en œuvre concrète, mais c'est tellement plus simple que les autres approches pour les situations où il peut être utilisé.
Mark Meuer
3
Et si nous avons plus d'un constructeur avec plusieurs types de béton, le saura-t-il toujours?
Teoman shipahi
1
Cette réponse est si élégante comparée à toutes les absurdités alambiquées que vous auriez à faire autrement. Cela devrait être la réponse acceptée. Une mise en garde dans mon cas, cependant, était que je devais ajouter [JsonConstructor] avant le constructeur pour que cela fonctionne .... Je soupçonne que l'utilisation de ceci sur UN SEUL de vos constructeurs concrets résoudrait votre problème (4 ans) @Teomanshipahi
nacitar sevaht
@nacitarsevaht Je peux revenir en arrière et résoudre mon problème maintenant :) de toute façon je ne me souviens même pas de ce que c'était, mais quand je repense, c'est une bonne solution pour certains cas.
Teoman shipahi
nous l'utilisons aussi mais je préfère la conversion dans la plupart des cas, car le couplage du type concret au constructeur va à l'encontre de l'objectif d'utiliser une interface pour la propriété en premier lieu!
gabe le
19

J'ai eu le même problème, alors j'ai créé mon propre convertisseur qui utilise l'argument des types connus.

public class JsonKnownTypeConverter : JsonConverter
{
    public IEnumerable<Type> KnownTypes { get; set; }

    public JsonKnownTypeConverter(IEnumerable<Type> knownTypes)
    {
        KnownTypes = knownTypes;
    }

    protected object Create(Type objectType, JObject jObject)
    {
        if (jObject["$type"] != null)
        {
            string typeName = jObject["$type"].ToString();
            return Activator.CreateInstance(KnownTypes.First(x =>typeName.Contains("."+x.Name+",")));
        }

        throw new InvalidOperationException("No supported type");
    }

    public override bool CanConvert(Type objectType)
    {
        if (KnownTypes == null)
            return false;

        return (objectType.IsInterface || objectType.IsAbstract) && KnownTypes.Any(objectType.IsAssignableFrom);
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        // Load JObject from stream
        JObject jObject = JObject.Load(reader);
        // Create target object based on JObject
        var target = Create(objectType, jObject);
        // Populate the object properties
        serializer.Populate(jObject.CreateReader(), target);
        return target;
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        throw new NotImplementedException();
    }
}

J'ai défini deux méthodes d'extension pour la désérialisation et la sérialisation:

public static class AltiJsonSerializer
{
    public static T DeserializeJson<T>(this string jsonString, IEnumerable<Type> knownTypes = null)
    {
        if (string.IsNullOrEmpty(jsonString))
            return default(T);

        return JsonConvert.DeserializeObject<T>(jsonString,
                new JsonSerializerSettings
                {
                    TypeNameHandling = TypeNameHandling.Auto, 
                    Converters = new List<JsonConverter>
                        (
                            new JsonConverter[]
                            {
                                new JsonKnownTypeConverter(knownTypes)
                            }
                        )
                }
            );
    }

    public static string SerializeJson(this object objectToSerialize)
    {
        return JsonConvert.SerializeObject(objectToSerialize, Formatting.Indented,
        new JsonSerializerSettings {TypeNameHandling = TypeNameHandling.Auto});
    }
}

Vous pouvez définir votre propre façon de comparer et d'identifier les types dans les convertis, j'utilise uniquement le nom de la classe.

Bruno Altinet
la source
1
Ce JsonConverter est génial, je l'ai utilisé mais j'ai rencontré quelques problèmes que j'ai résolus de cette façon: - En utilisant JsonSerializer.CreateDefault () à la place pour Populate, car mon objet avait une hiérarchie plus profonde. - Utilisation de la réflexion pour récupérer le constructeur et l'instancier dans la méthode Create ()
Aurel
3

Normalement, j'ai toujours utilisé la solution avec TypeNameHandlingcomme suggéré par DanielT, mais dans les cas ici, je n'ai pas eu de contrôle sur le JSON entrant (et je ne peux donc pas m'assurer qu'il inclut une $typepropriété), j'ai écrit un convertisseur personnalisé qui vous permet simplement de spécifier explicitement le type de béton:

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

Cela utilise simplement l'implémentation du sérialiseur par défaut de Json.Net tout en spécifiant explicitement le type concret.

Le code source et un aperçu sont disponibles sur ce billet de blog .

Steve Greatrex
la source
1
C'est une excellente solution. À votre santé.
JohnMetta
2

Je voulais juste compléter l'exemple que @Daniel T. nous a montré ci-dessus:

Si vous utilisez ce code pour sérialiser votre objet:

var settings = new JsonSerializerSettings();
settings.TypeNameHandling = TypeNameHandling.Objects;
JsonConvert.SerializeObject(entity, Formatting.Indented, settings);

Le code pour désérialiser le json devrait ressembler à ceci:

var settings = new JsonSerializerSettings(); 
settings.TypeNameHandling = TypeNameHandling.Objects;
var entity = JsonConvert.DeserializeObject<EntityType>(json, settings);

Voici comment un json se conforme lors de l'utilisation de l' TypeNameHandlingindicateur:entrez la description de l'image ici

Luis Armando
la source
-5

Je me suis demandé la même chose, mais j'ai bien peur que cela ne puisse pas être fait.

Regardons les choses de cette façon. Vous remettez à JSon.net une chaîne de données et un type dans lequel désérialiser. Que doit faire JSON.net quand il atteint cet ISomething? Il ne peut pas créer un nouveau type de ISomething car ISomething n'est pas un objet. Il ne peut pas non plus créer un objet qui implémente ISomething, car il ne sait pas lequel des nombreux objets qui peuvent hériter ISomething il devrait utiliser. Les interfaces sont quelque chose qui peut être automatiquement sérialisé, mais pas automatiquement désérialisé.

Ce que je ferais serait de chercher à remplacer ISomething par une classe de base. En utilisant cela, vous pourrez peut-être obtenir l'effet que vous recherchez.

Timothy Baldridge
la source
1
Je me rends compte que cela ne fonctionnera pas «hors de la boîte». Mais je me demandais s'il y avait un attribut comme "[JsonProperty (typeof (SomethingBase))]" que je pourrais utiliser pour fournir une classe concrète.
dthrasher
Alors pourquoi ne pas utiliser SomethingBase au lieu de ISomething dans le code ci-dessus? On pourrait soutenir que nous regardons également cela dans le mauvais sens car les interfaces ne devraient pas être utilisées dans la sérialisation, puisqu'elles définissent simplement "l'interface" de communication avec une classe donnée. La sérialisation d'une interface est techniquement absurde, tout comme la sérialisation d'une classe abstraite. Donc, même si cela «pourrait être fait», je dirais que cela «ne devrait pas être fait».
Timothy Baldridge
Avez-vous examiné l'une des classes de l'espace de noms Newtonsoft.Json.Serialization? en particulier la classe JsonObjectContract?
johnny
-9

Voici une référence à un article écrit par ScottGu

Sur cette base, j'ai écrit un code qui, je pense, pourrait être utile

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

}

public class School : IEducationalInstitute
{
    private string name;
    #region IEducationalInstitute Members

    public string Name
    {
        get { return name; }
        set { name = value; }
    }

    #endregion
}

public class Student 
{
    public IEducationalInstitute LocalSchool { get; set; }

    public int ID { get; set; }
}

public static class JSONHelper
{
    public static string ToJSON(this object obj)
    {
        JavaScriptSerializer serializer = new JavaScriptSerializer();
        return serializer.Serialize(obj);
    }
    public  static string ToJSON(this object obj, int depth)
    {
        JavaScriptSerializer serializer = new JavaScriptSerializer();
        serializer.RecursionLimit = depth;
        return serializer.Serialize(obj);
    }
}

Et c'est comme ça que tu appellerais ça

School myFavSchool = new School() { Name = "JFK High School" };
Student sam = new Student()
{
    ID = 1,
    LocalSchool = myFavSchool
};
string jSONstring = sam.ToJSON();

Console.WriteLine(jSONstring);
//Result {"LocalSchool":{"Name":"JFK High School"},"ID":1}

Si je comprends bien, je ne pense pas que vous ayez besoin de spécifier une classe concrète qui implémente l'interface pour la sérialisation JSON.

RAM
la source
1
Votre exemple utilise JavaScriptSerializer, une classe du .NET Framework. J'utilise Json.NET comme sérialiseur. codeplex.com/Json
dthrasher
3
Ne fait pas référence à la question d'origine, Json.NET y était explicitement mentionné.
Oliver