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í .