2 votes

Json.net - Comment préserver les références aux valeurs du dictionnaire lors de l'alimentation d'un dictionnaire ?

Je voudrais alimenter les objets contenus dans un dictionnaire à partir d'un fichier JSON tout en préservant les références des objets eux-mêmes.

La documentation de Json.net sur PreserveReferencesHandling indique clairement qu'elle ne fonctionnera pas dans le cas où un type implémente System.Runtime.Serialization.ISerializable :

Spécifie les options de traitement de la référence pour le Newtonsoft.Json.JsonSerializer. Notez que les références ne peuvent pas être préservées lorsqu'une valeur est définie via un constructeur non par défaut, comme les les types qui implémentent System.Runtime.Serialization.ISerializable.

Voici mon code défaillant :

class Model
{
   public int Val { get; set; } = 123;
}

...

    var model = new Model();
    var to_serialize = new Dictionary<int, Model> { { 0, model } }; // works ok with list<Model>

    // serialize
    var jsonString = JsonConvert.SerializeObject(to_serialize, Formatting.Indented);

    var jsonSerializerSettings = new JsonSerializerSettings();
    jsonSerializerSettings.MissingMemberHandling = MissingMemberHandling.Ignore;
    jsonSerializerSettings.PreserveReferencesHandling = PreserveReferencesHandling.All; // does not work for ISerializable

    Assert.AreSame(to_serialize[0], model); // ok!

    JsonConvert.PopulateObject(
        value: jsonString,
        target: to_serialize,
        settings: jsonSerializerSettings
    );

    Assert.AreSame(to_serialize[0], model); // not ok... works ok with list<Model>

Ma principale exigence est que lors de l'appel de PopulateObject(), le constructeur de la classe Model ne soit pas invoqué. Au lieu de cela, seul son champ interne sera mis à jour avec la valeur du JSON. Dans mon cas réel, la classe Model contient d'autres valeurs qui ne sont pas dans le JSON et que je ne veux pas perdre :

[JsonObject(MemberSerialization.OptIn)]
class Model
{
   [JsonProperty(PropertyName = "val_prop")]
   public int Val { get; set; } = 123;

   // not in the json file, would like this field to maintain the value
   // it had prior to PopulateObject()
   public int OtherVal { get; set; } = 456;
}

Y a-t-il un moyen de faire en sorte que cela fonctionne ?

3voto

dbc Points 3449

Votre problème est similaire à celui de JsonSerializer.CreateDefault().Populate(..) réinitialise mes valeurs. : vous souhaitez alimenter une collection préexistante, plus précisément une Dictionary<int, T> pour certains T et de remplir les valeurs préexistantes. Malheureusement, dans le cas d'un dictionnaire, Json.NET va remplacer les valeurs plutôt que de les remplir, comme on peut le voir dans JsonSerializerInternalReader.PopulateDictionary() qui désérialise simplement la valeur au type approprié, et la place dans le dictionnaire.

Pour contourner cette limitation, vous pouvez créer un fichier personnalisé JsonConverter para Dictionary<TKey, TValue> quand TKey est un type primitif et TValue est un type complexe qui fusionne les paires clé/valeur JSON entrantes avec le dictionnaire préexistant. Le convertisseur suivant fait l'affaire :

public class DictionaryMergeConverter : JsonConverter
{
    static readonly IContractResolver defaultResolver = JsonSerializer.CreateDefault().ContractResolver;
    readonly IContractResolver resolver = defaultResolver;

    public override bool CanConvert(Type objectType)
    {
        var keyValueTypes = objectType.GetDictionaryKeyValueType();
        if (keyValueTypes == null)
            return false;
        var keyContract = resolver.ResolveContract(keyValueTypes[0]);
        if (!(keyContract is JsonPrimitiveContract))
            return false;
        var contract = resolver.ResolveContract(keyValueTypes[1]);
        return contract is JsonContainerContract;
        // Also possibly check whether keyValueTypes[1] is a read-only collection or dictionary.
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        if (reader.MoveToContentAndAssert().TokenType == JsonToken.Null)
            return null;
        if (reader.TokenType != JsonToken.StartObject)
            throw new JsonSerializationException(string.Format("Unexpected token {0}", reader.TokenType));
        IDictionary dictionary = existingValue as IDictionary ?? (IDictionary)serializer.ContractResolver.ResolveContract(objectType).DefaultCreator();
        var keyValueTypes = objectType.GetDictionaryKeyValueType();
        while (reader.ReadToContentAndAssert().TokenType != JsonToken.EndObject)
        {
            switch (reader.TokenType)
            {
                case JsonToken.PropertyName:
                    var name = (string)reader.Value;
                    reader.ReadToContentAndAssert();

                    // TODO: DateTime keys and enums with overridden names.
                    var key = (keyValueTypes[0] == typeof(string) ? (object)name : Convert.ChangeType(name, keyValueTypes[0], serializer.Culture));
                    var value = dictionary.Contains(key) ? dictionary[key] : null;

                    // TODO:
                    //  - JsonConverter active for valueType, either in contract or in serializer.Converters
                    //  - NullValueHandling, ObjectCreationHandling, PreserveReferencesHandling, 

                    if (value == null)
                    {
                        value = serializer.Deserialize(reader, keyValueTypes[1]);
                    }
                    else
                    {
                        serializer.Populate(reader, value);
                    }
                    dictionary[key] = value;
                    break;

                default:
                    throw new JsonSerializationException(string.Format("Unexpected token {0}", reader.TokenType));
            }
        }

        return dictionary;
    }

    public override bool CanWrite { get { return false; } }

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

public static partial class JsonExtensions
{
    public static JsonReader ReadToContentAndAssert(this JsonReader reader)
    {
        return reader.ReadAndAssert().MoveToContentAndAssert();
    }

    public static JsonReader MoveToContentAndAssert(this JsonReader reader)
    {
        if (reader == null)
            throw new ArgumentNullException();
        if (reader.TokenType == JsonToken.None)       // Skip past beginning of stream.
            reader.ReadAndAssert();
        while (reader.TokenType == JsonToken.Comment) // Skip past comments.
            reader.ReadAndAssert();
        return reader;
    }

    public static JsonReader ReadAndAssert(this JsonReader reader)
    {
        if (reader == null)
            throw new ArgumentNullException();
        if (!reader.Read())
            throw new JsonReaderException("Unexpected end of JSON stream.");
        return reader;
    }
}

public static class TypeExtensions
{
    public static IEnumerable<Type> BaseTypesAndSelf(this Type type)
    {
        while (type != null)
        {
            yield return type;
            type = type.BaseType;
        }
    }

    public static Type[] GetDictionaryKeyValueType(this Type type)
    {
        return type.BaseTypesAndSelf().Where(t => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(Dictionary<,>)).Select(t => t.GetGenericArguments()).FirstOrDefault();
    }
}

Après avoir fait cela, vous rencontrerez un problème secondaire : Json.NET n'utilisera jamais un convertisseur personnalisé pour remplir les champs de type Objet racine . Pour contourner ce problème, vous devrez appeler JsonConverter.ReadJson() directement, à partir d'une méthode utilitaire :

public static partial class JsonExtensions
{
    public static void PopulateObjectWithConverter(string value, object target, JsonSerializerSettings settings)
    {
        if (target == null || value == null)
            throw new ArgumentNullException();
        var serializer = JsonSerializer.CreateDefault(settings);
        var converter = serializer.Converters.Where(c => c.CanConvert(target.GetType()) && c.CanRead).FirstOrDefault() ?? serializer.ContractResolver.ResolveContract(target.GetType()).Converter;
        using (var jsonReader = new JsonTextReader(new StringReader(value)))
        {
            if (converter == null)
                serializer.Populate(jsonReader, target);
            else
            {
                jsonReader.MoveToContentAndAssert();
                var newtarget = converter.ReadJson(jsonReader, target.GetType(), target, serializer);
                if (newtarget != target)
                    throw new JsonException(string.Format("Converter {0} allocated a new object rather than populating the existing object {1}.", converter, value));
            }
        }
    }
}

Vous allez maintenant pouvoir remplir votre dictionnaire comme suit :

var jsonString = JsonConvert.SerializeObject(to_serialize, Formatting.Indented);

var settings = new JsonSerializerSettings
{
    Converters = { new DictionaryMergeConverter() },
};
JsonExtensions.PopulateObjectWithConverter(jsonString, to_serialize, settings);

Notes :

  • PreserveReferencesHandling n'a aucun impact sur le fait que les valeurs du dictionnaire soient remplies ou remplacées. Au lieu de cela, ce paramètre contrôle si un graphe de sérialisation avec plusieurs références au même objet maintiendra sa topologie de référence lors de l'aller-retour.

  • Dans votre question, vous avez écrit // works ok with list<Model> mais en fait, ce n'est pas correct. Lorsqu'un List<T> est remplie, les nouvelles valeurs sont en annexe à la liste, donc Assert.AreSame(to_serialize[0], model); passe purement par hasard. Si vous aviez en plus affirmé Assert.AreSame(1, to_serialize.Count) il aurait échoué.

  • Bien que le convertisseur fonctionne pour les clés primitives telles que string y int elle peut ne pas fonctionner pour les types de clés qui nécessitent une conversion spécifique à JSON, comme par exemple enum ou DateTime .

  • Le convertisseur n'est actuellement mis en œuvre que pour Dictionary<TKey, TValue> et tire parti du fait que ce type implémente la fonction non générique IDictionary l'interface. Elle pourrait être étendue à d'autres types de dictionnaires tels que SortedDictionary<TKey,TValue> si nécessaire.

Violon de démonstration aquí .

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X