c# - Sérialiser uniquement les propriétés d'interface en JSON avec Json.net




serialization (9)

Avec une classe / interface simple comme celle-ci

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

public class Thing : IThing
{
    public int Id { get; set; }
    public string Name { get; set; }
}

Comment puis-je obtenir la chaîne JSON avec uniquement la propriété "Name" (uniquement les propriétés de l'interface sous-jacente)?

En fait, quand je fais ça:

var serialized = JsonConvert.SerializeObject((IThing)theObjToSerialize, Formatting.Indented);
Console.WriteLine(serialized);

Je reçois l'objet complet en tant que JSON (Id + Name);


Answers

Vous pouvez utiliser la sérialisation conditionnelle. Jetez un oeil à ce link . De manière simple, vous devez implémenter l'interface IContractResolver , surcharger la méthode ShouldSerialize et transmettre votre résolveur au constructeur du sérialiseur Json.


La méthode que j'utilise,

public class InterfaceContractResolver : DefaultContractResolver
{
    private readonly Type _InterfaceType;
    public InterfaceContractResolver (Type InterfaceType)
    {
        _InterfaceType = InterfaceType;
    }

    protected override IList<JsonProperty> CreateProperties(Type type, MemberSerialization memberSerialization)
    {
        //IList<JsonProperty> properties = base.CreateProperties(type, memberSerialization);
        IList<JsonProperty> properties = base.CreateProperties(_InterfaceType, memberSerialization);
        return properties;
    }
}

// To serialize do this:
var settings = new JsonSerializerSettings() {
     ContractResolver = new InterfaceContractResolver (typeof(IThing))
});
string json = JsonConvert.SerializeObject(theObjToSerialize, settings);

Je voudrais partager ce que nous avons fini par faire face à cette tâche. Compte tenu de l'interface et de la classe de l'OP ...

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

public class Thing : IThing
{
   public int Id { get; set; }
   public string Name { get; set; }
}

... nous avons créé une classe qui est l'implémentation directe de l'interface ...

public class DirectThing : IThing
{
   public string Name { get; set; }
}

Ensuite, il suffit de sérialiser notre instance Thing , de la désérialiser en tant que DirectThing , puis de la sérialiser en tant que DirectThing :

var thing = new Thing();
JsonConvert.SerializeObject(
    JsonConvert.DeserializeObject<DirectThing>(JsonConvert.SerializeObject(thing)));

Cette approche peut fonctionner avec une longue chaîne d'héritage d'interface ... il suffit de créer une classe directe ( DirectThing dans cet exemple) au niveau qui vous intéresse. Pas besoin de s'inquiéter de la réflexion ou des attributs.

Du point de vue de la maintenance, la classe DirectThing est facile à gérer si vous ajoutez des membres à IThing car le compilateur IThing des erreurs si vous ne les avez pas également mises dans DirectThing . Cependant, si vous supprimez un membre X de IThing et que vous le placez dans Thing , vous devrez vous souvenir de le supprimer de DirectThing sinon X sera dans le résultat final.

Du point de vue de la performance, trois opérations de sérialisation sont effectuées ici au lieu d'une. Ainsi, en fonction de votre situation, vous pouvez évaluer la différence de performances des solutions basées sur les réflecteurs / attributs par rapport à cette solution. Dans mon cas, je ne faisais que cela à petite échelle, donc je n'étais pas préoccupé par les pertes potentielles de quelques micro / millisecondes.

J'espère que ça aide quelqu'un!



Version améliorée avec interfaces imbriquées + prise en charge des objets xsd.exe

Encore une autre variation ici. Le code provient de tomdupont.net/2015/09/how-to-only-serialize-interface.html avec les améliorations suivantes par rapport aux autres réponses ici

  • Gère la hiérarchie, donc si vous avez une Interface2[] dans une Interface1 elle sera sérialisée.
  • J'essayais de sérialiser un objet proxy WCF et le résultat JSON est apparu avec {} . Il s'est avéré que toutes les propriétés étaient définies sur Ignore=true . J'ai donc dû ajouter une boucle pour que toutes ne soient pas ignorées.

    public class InterfaceContractResolver : DefaultContractResolver
    {
        private readonly Type[] _interfaceTypes;
    
        private readonly ConcurrentDictionary<Type, Type> _typeToSerializeMap;
    
        public InterfaceContractResolver(params Type[] interfaceTypes)
        {
            _interfaceTypes = interfaceTypes;
    
            _typeToSerializeMap = new ConcurrentDictionary<Type, Type>();
        }
    
        protected override IList<JsonProperty> CreateProperties(
            Type type,
            MemberSerialization memberSerialization)
        {
            var typeToSerialize = _typeToSerializeMap.GetOrAdd(
                type,
                t => _interfaceTypes.FirstOrDefault(
                    it => it.IsAssignableFrom(t)) ?? t);
    
            var props = base.CreateProperties(typeToSerialize, memberSerialization);
    
            // mark all props as not ignored
            foreach (var prop in props)
            {
                prop.Ignored = false;
            }
    
            return props;
        }
    }
    

Une alternative à [JsonIgnore] est les [DataContract] et [DataMember] . Si votre classe est étiquetée avec [DataContract] le sérialiseur traitera uniquement les propriétés marquées avec l'attribut [DataMember] ( JsonIgnore est un modèle de " DataContract " lorsque DataContract est " DataContract ").

[DataContract]
public class Thing : IThing
{
    [DataMember]
    public int Id { get; set; }

    public string Name { get; set; }
}

La limitation des deux approches est qu'elles doivent être implémentées dans la classe, vous ne pouvez pas les ajouter à la définition de l'interface.


Inspiré par @ user3161686, voici une petite modification apportée à InterfaceContractResolver :

public class InterfaceContractResolver<TInterface> : DefaultContractResolver where TInterface : class
{
    protected override IList<JsonProperty> CreateProperties(Type type, MemberSerialization memberSerialization)
    {
        IList<JsonProperty> properties = base.CreateProperties(typeof(TInterface), memberSerialization);
        return properties;
    }
}

Enfin, j'ai obtenu quand cela ne fonctionnera pas ... Si vous voulez avoir à l'intérieur d'un autre objet complexe, il ne sera pas sérialisé correctement.

J'ai donc fait la version qui extraira uniquement les données stockées dans un assemblage spécifique et pour les types qui ont la même interface de base.

Il est donc fait en tant que .Net Core JsonContractResolver.

En plus de l'extraction de données, il résout:
a) la conversion camelCase avant d'envoyer des données au client
b) utilise l'interface la plus haute de la portée autorisée (par assembly) c) corrige l'ordre des champs: le champ de la plupart des classes de base sera répertorié en premier et l'objet imbriqué respectera également cette règle.

public class OutputJsonResolver : DefaultContractResolver
{
    #region Static Members
    private static readonly object syncTargets = new object();
    private static readonly Dictionary<Type, IList<JsonProperty>> Targets = new Dictionary<Type, IList<JsonProperty>>();

    private static readonly Assembly CommonAssembly = typeof(ICommon).Assembly;
    #endregion

    #region Override Members
    protected override IList<JsonProperty> CreateProperties(Type type, MemberSerialization memberSerialization)
    {
        if (type.Assembly != OutputJsonResolver.CommonAssembly)
            return base.CreateProperties(type, memberSerialization);

        IList<JsonProperty> properties;
        if (OutputJsonResolver.Targets.TryGetValue(type, out properties) == false)
        {
            lock (OutputJsonResolver.syncTargets)
            {
                if (OutputJsonResolver.Targets.ContainsKey(type) == false)
                {
                    properties = this.CreateCustomProperties(type, memberSerialization);

                    OutputJsonResolver.Targets[type] = properties;
                }
            }
        }

        return properties;
    }
    protected override string ResolvePropertyName(string propertyName)
    {
        return propertyName.ToCase(Casing.Camel);
    }
    #endregion

    #region Assistants
    private IList<JsonProperty> CreateCustomProperties(Type type, MemberSerialization memberSerialization)
    {
        // Hierarchy
        IReadOnlyList<Type> types = this.GetTypes(type);

        // Head
        Type head = types.OrderByDescending(item => item.GetInterfaces().Length).FirstOrDefault();

        // Sources
        IList<JsonProperty> sources = base.CreateProperties(head, memberSerialization);

        // Targets
        IList<JsonProperty> targets = new List<JsonProperty>(sources.Count);

        // Repository
        IReadOnlyDistribution<Type, JsonProperty> repository = sources.ToDistribution(item => item.DeclaringType);

        foreach (Type current in types.Reverse())
        {
            IReadOnlyPage<JsonProperty> page;
            if (repository.TryGetValue(current, out page) == true)
                targets.AddRange(page);
        }

        return targets;
    }
    private IReadOnlyList<Type> GetTypes(Type type)
    {
        List<Type> types = new List<Type>();

        if (type.IsInterface == true)
            types.Add(type);

        types.AddRange(type.GetInterfaces());

        return types;
    }
    #endregion
}

Si vous n'avez pas accès aux classes pour modifier les propriétés, ou si vous ne voulez pas toujours utiliser la même propriété de renommage, vous pouvez également renommer en créant un résolveur personnalisé.

Par exemple, si vous avez une classe appelée MyCustomObject , qui a une propriété appelée LongPropertyName , vous pouvez utiliser un résolveur personnalisé comme celui-ci ...

public class CustomDataContractResolver : DefaultContractResolver
{
  public static readonly CustomDataContractResolver Instance = new CustomDataContractResolver ();

  protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
  {
    var property = base.CreateProperty(member, memberSerialization);
    if (property.DeclaringType == typeof(MyCustomObject))
    {
      if (property.PropertyName.Equals("LongPropertyName", StringComparison.OrdinalIgnoreCase))
      {
        property.PropertyName = "Short";
      }
    }
    return property;
  }
}

Appelez ensuite pour la sérialisation et fournissez le résolveur

 var result = JsonConvert.SerializeObject(myCustomObjectInstance,
                new JsonSerializerSettings { ContractResolver = CustomDataContractResolver.Instance });

Et le résultat sera raccourci à {"Short": "prop value"} au lieu de {"LongPropertyName": "prop value"}

Plus d'informations sur les résolveurs personnalisés here





c# serialization json.net