web-dev-qa-db-fra.com

Désérialiser json avec des champs connus et inconnus

Étant donné le résultat JSON suivant: Le résultat JSON par défaut comporte un ensemble de champs connu:

{
    "id": "7908",
    "name": "product name"
}

Mais peut être étendu avec des champs supplémentaires (dans cet exemple, _unknown_field_name_1 et _unknown_field_name_2) dont les noms ne sont pas connus lors de la demande du résultat.

{
    "id": "7908",
    "name": "product name",
    "_unknown_field_name_1": "some value",
    "_unknown_field_name_2": "some value"
}

Je voudrais que le résultat JSON soit sérialisé et désérialisé vers et depuis une classe avec des propriétés pour les champs connus et mappe les champs inconnus (pour lesquels il n'y a pas de propriétés) à une propriété (ou à plusieurs propriétés) comme un dictionnaire afin qu'ils puissent être accédé et modifié.

public class Product
{
    public string id { get; set; }
    public string name { get; set; }
    public Dictionary<string, string> fields { get; set; }
}

Je pense que j'ai besoin d'un moyen de brancher un sérialiseur JSON et de mapper moi-même les membres manquants (à la fois pour la sérialisation et la désérialisation) ... J'ai examiné diverses possibilités:

  • json.net et résolveurs de contrat personnalisés (vous ne savez pas comment le faire)
  • sérialiseur datacontract (ne peut remplacer que onserialized, onserializing)
  • sérialiser en dynamique et faire un mappage personnalisé (cela peut fonctionner, mais semble beaucoup de travail)
  • laisser le produit héritant de DynamicObject (les sérialiseurs fonctionnent avec la réflexion et n'invoquent pas les méthodes trygetmember et trysetmember)

J'utilise restsharp, mais n'importe quel sérialiseur peut être branché.

Oh, et je ne peux pas changer le résultat JSON, et ceci ou ceci ne m'a pas aidé non plus.

Mise à jour: Cela lui ressemble davantage: http://geekswithblogs.net/DavidHoerster/archive/2011/07/26/json.net-custom-convertersndasha-quick-tour.aspx

45
nickvane

Une option encore plus simple pour résoudre ce problème serait d’utiliser le JsonExtensionDataAttribute de JSON .NET

public class MyClass
{
   // known field
   public decimal TaxRate { get; set; }

   // extra fields
   [JsonExtensionData]
   private IDictionary<string, JToken> _extraStuff;
}

Vous en trouverez un exemple sur le blog du projet ici

UPDATE Veuillez noter que cela nécessite JSON .NET version 5 et ultérieure

58
cecilphillip

Voir https://Gist.github.com/LodewijkSioen/5101814

Ce que vous cherchiez était une coutume JsonConverter

10
Lodewijk

C'est une façon de le résoudre, même si je ne l'aime pas trop. Je l'ai résolu en utilisant Newton/JSON.Net. Je suppose que vous pourriez aussi utiliser le JsonConverter pour la désérialisation. 

private const string Json = "{\"id\":7908,\"name\":\"product name\",\"_unknown_field_name_1\":\"some value\",\"_unknown_field_name_2\":\"some value\"}";

    [TestMethod]
    public void TestDeserializeUnknownMembers()
    {
        var @object = JObject.Parse(Json);

        var serializer = new Newtonsoft.Json.JsonSerializer();
        serializer.MissingMemberHandling = Newtonsoft.Json.MissingMemberHandling.Error;
        serializer.Error += (sender, eventArgs) =>
            {
                var contract = eventArgs.CurrentObject as Contract ?? new Contract();
                contract.UnknownValues.Add(eventArgs.ErrorContext.Member.ToString(), @object[eventArgs.ErrorContext.Member.ToString()].Value<string>());
                eventArgs.ErrorContext.Handled = true;
            };

        using (MemoryStream memoryStream = new MemoryStream(Encoding.UTF8.GetBytes(Json)))
        using (StreamReader streamReader = new StreamReader(memoryStream))
        using (JsonReader jsonReader = new JsonTextReader(streamReader))
        {
            var result = serializer.Deserialize<Contract>(jsonReader);
            Assert.IsTrue(result.UnknownValues.ContainsKey("_unknown_field_name_1"));
            Assert.IsTrue(result.UnknownValues.ContainsKey("_unknown_field_name_2"));
        }
    }

    [TestMethod]
    public void TestSerializeUnknownMembers()
    {
        var deserializedObject = new Contract
        {
            id = 7908,
            name = "product name",
            UnknownValues = new Dictionary<string, string>
        {
            {"_unknown_field_name_1", "some value"},
            {"_unknown_field_name_2", "some value"}
        }
        };

        var json = JsonConvert.SerializeObject(deserializedObject, new DictionaryConverter());
        Console.WriteLine(Json);
        Console.WriteLine(json);
        Assert.AreEqual(Json, json);
    }
}

class DictionaryConverter : JsonConverter
{
    public DictionaryConverter()
    {

    }

    public override bool CanConvert(Type objectType)
    {
        return objectType == typeof(Contract);
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        throw new NotImplementedException();
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        var contract = value as Contract;
        var json = JsonConvert.SerializeObject(value);
        var dictArray = String.Join(",", contract.UnknownValues.Select(pair => "\"" + pair.Key + "\":\"" + pair.Value + "\""));

        json = json.Substring(0, json.Length - 1) + "," + dictArray + "}";
        writer.WriteRaw(json);
    }
}

class Contract
{
    public Contract()
    {
        UnknownValues = new Dictionary<string, string>();
    }

    public int id { get; set; }
    public string name { get; set; }

    [JsonIgnore]
    public Dictionary<string, string> UnknownValues { get; set; }
}

}

4
Jordy Langen

Je cherchais un problème similaire et ai trouvé ce post.

Voici une façon de le faire en utilisant la réflexion.

Pour le rendre plus générique, il convient de vérifier le type de la propriété au lieu d'utiliser simplement ToString () dans propertyInfo.SetValue, sauf si OFC toutes les propriétés réelles sont des chaînes.

De plus, les noms de propriété en minuscule ne sont pas standard en C #, mais étant donné que GetProperty est sensible à la casse, il y a peu d'autres options.

public class Product
{
    private Type _type;

    public Product()
    {
        fields = new Dictionary<string, object>();
        _type = GetType();
    }

    public string id { get; set; }
    public string name { get; set; }
    public Dictionary<string, object> fields { get; set; }

    public void SetProperty(string key, object value)
    {
        var propertyInfo = _type.GetProperty(key);
        if (null == propertyInfo)
        {
            fields.Add(key,value);
            return;
        }
        propertyInfo.SetValue(this, value.ToString());
    }
}
...
private const string JsonTest = "{\"id\":7908,\"name\":\"product name\",\"_unknown_field_name_1\":\"some value\",\"_unknown_field_name_2\":\"some value\"}";

var product = new Product();
var data = JObject.Parse(JsonTest);
foreach (var item in data)
{
    product.SetProperty(item.Key, item.Value);
}
0
pasx

Je pensais jeter mon chapeau dans le ring depuis que j'ai eu un problème similaire récemment. Voici un exemple du JSON que je voulais désérialiser:

{
    "agencyId": "agency1",
    "overrides": {
        "assumption.discount.rates": "value: 0.07",
        ".plan": {
            "plan1": {
                "assumption.payroll.growth": "value: 0.03",
                "provision.eeContrib.rate": "value: 0.35"
            },
            "plan2": {
                ".classAndTier": {
                    "misc:tier1": {
                        "provision.eeContrib.rate": "value: 0.4"
                    },
                    "misc:tier2": {
                        "provision.eeContrib.rate": "value: 0.375"
                    }
                }
            }
        }
    }
}

Cela concerne un système où les substitutions s'appliquent à différents niveaux et sont héritées dans l'arborescence. En tout état de cause, le modèle de données que je souhaitais me permettait de disposer d'un sac de propriété contenant également ces règles d'héritage spéciales.

Ce que j'ai fini par être le suivant:

public class TestDataModel
{
    public string AgencyId;
    public int Years;
    public PropertyBagModel Overrides;
}

public class ParticipantFilterModel
{
    public string[] ClassAndTier;
    public string[] BargainingUnit;
    public string[] Department;
}

public class PropertyBagModel
{
    [JsonExtensionData]
    private readonly Dictionary<string, JToken> _extensionData = new Dictionary<string, JToken>();

    [JsonIgnore]
    public readonly Dictionary<string, string> Values = new Dictionary<string, string>();

    [JsonProperty(".plan", NullValueHandling = NullValueHandling.Ignore)]
    public Dictionary<string, PropertyBagModel> ByPlan;

    [JsonProperty(".classAndTier", NullValueHandling = NullValueHandling.Ignore)]
    public Dictionary<string, PropertyBagModel> ByClassAndTier;

    [JsonProperty(".bargainingUnit", NullValueHandling = NullValueHandling.Ignore)]
    public Dictionary<string, PropertyBagModel> ByBarginingUnit;

    [OnSerializing]
    private void OnSerializing(StreamingContext context)
    {
        foreach (var kvp in Values)
            _extensionData.Add(kvp.Key, kvp.Value);
    }

    [OnSerialized]
    private void OnSerialized(StreamingContext context)
    {
        _extensionData.Clear();
    }

    [OnDeserialized]
    private void OnDeserialized(StreamingContext context)
    {
        Values.Clear();
        foreach (var kvp in _extensionData.Where(x => x.Value.Type == JTokenType.String))
            Values.Add(kvp.Key, kvp.Value.Value<string>());
        _extensionData.Clear();
    }
}

L'idée de base est la suivante:

  1. Le PropertyBagModel sur la désérialisation par JSON.NET contient les champs ByPlan, ByClassAndTier, etc., ainsi que le champ privé _extensionData.
  2. Ensuite, JSON.NET appelle la méthode privée OnDeserialized () et cela déplacera les données de _extensionData vers Values ​​selon les besoins (ou les déposera au sol sinon, vous pourrez probablement vous connecter si c'était quelque chose que vous vouliez savoir). Nous supprimons ensuite le surplus supplémentaire de _extensionData afin qu’il ne consomme pas de mémoire.
  3. Lors de la sérialisation, la méthode OnSerializing reçoit les appels où nous déplaçons des éléments dans _extensionData afin de les enregistrer.
  4. Lorsque la sérialisation est terminée, OnSerialized est appelée et nous supprimons les éléments supplémentaires de _extensionData.

Nous pourrions en outre supprimer et recréer le dictionnaire _extensionData en cas de besoin, mais je n’y voyais aucune valeur réelle, car je n’utilisais pas des tonnes de ces objets. Pour ce faire, nous créerions simplement sur OnSerializing et supprimerions sur OnSerialized. Sur OnDeserializing, au lieu d’effacer, nous pourrions le libérer.

0
Kelly L