问题
Using Json.NET, I'd like to map a JObject
onto a .NET object with the following behavior:
For (non-null) array properties of the
JObject
, replace the entire collection on the target object.For (non-null) object properties of the
JObject
, reuse the target object's property if it's not null, and map just the provided properties onto it.
JsonSerializer.Populate seems to be what I want, as described in this answer. As for the behaviors I'm looking for, it seems I can achieve one or the other, but not both, via JsonSerializerSettings.ObjectCreationHandling. ObjectCreationHandling.Replace
does what I want with respect to requirement #1, while ObjectCreationHandling.Auto
does what I want with respect to requirement #2, but it appends array items onto the existing collection.
What is the recommended way to achieve both requirements here?
回答1:
Json.NET will automatically replace any arrays or read-only collections. To clear out read-write collections when deserializing, you could create a custom contract resolver that adds an OnDeserializingCallback to every modifiable collection that clears the collection when deserialization begins. Clearing the collection rather that replacing it outright handles cases where the collection is get-only, for instance:
public class RootObject
{
readonly HashSet<int> hashSet = new HashSet<int>();
public HashSet<int> HashSetValues { get { return this.hashSet; } }
}
The contract resolver is as follows:
public class CollectionClearingContractResolver : DefaultContractResolver
{
static void ClearGenericCollectionCallback<T>(object o, StreamingContext c)
{
var collection = o as ICollection<T>;
if (collection == null || collection is Array || collection.IsReadOnly)
return;
collection.Clear();
}
static SerializationCallback ClearListCallback = (o, c) =>
{
var collection = o as IList;
if (collection == null || collection is Array || collection.IsReadOnly)
return;
collection.Clear();
};
protected override JsonArrayContract CreateArrayContract(Type objectType)
{
var contract = base.CreateArrayContract(objectType);
if (!objectType.IsArray)
{
if (typeof(IList).IsAssignableFrom(objectType))
{
contract.OnDeserializingCallbacks.Add(ClearListCallback);
}
else if (objectType.GetCollectItemTypes().Count() == 1)
{
MethodInfo method = typeof(CollectionClearingContractResolver).GetMethod("ClearGenericCollectionCallback", BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public);
MethodInfo generic = method.MakeGenericMethod(contract.CollectionItemType);
contract.OnDeserializingCallbacks.Add((SerializationCallback)Delegate.CreateDelegate(typeof(SerializationCallback), generic));
}
}
return contract;
}
}
public static class TypeExtensions
{
public static IEnumerable<Type> GetInterfacesAndSelf(this Type type)
{
if (type == null)
throw new ArgumentNullException();
if (type.IsInterface)
return new[] { type }.Concat(type.GetInterfaces());
else
return type.GetInterfaces();
}
public static IEnumerable<Type> GetCollectItemTypes(this Type type)
{
foreach (Type intType in type.GetInterfacesAndSelf())
{
if (intType.IsGenericType
&& intType.GetGenericTypeDefinition() == typeof(ICollection<>))
{
yield return intType.GetGenericArguments()[0];
}
}
}
}
public static class JsonExtensions
{
public static void Populate<T>(this JToken value, T target) where T : class
{
value.Populate(target, null);
}
public static void Populate<T>(this JToken value, T target, JsonSerializerSettings settings) where T : class
{
using (var sr = value.CreateReader())
{
JsonSerializer.CreateDefault(settings).Populate(sr, target);
}
}
}
Then to use it, do:
var settings = new JsonSerializerSettings
{
ContractResolver = new CollectionClearingContractResolver(),
};
jObject.Populate(rootObject, settings);
Sample fiddle.
Such a contract resolver would also be useful when deserializing objects that populate collections in their default constructor, as in Deserialization causes copies of List-Entries.
回答2:
One fix is to use a custom JsonConverter
that effectively replaces collections by ignoring the existing value when a collection type is detected.
public class ReplaceArrayConverter : JsonConverter
{
public override bool CanConvert(Type objectType) {
// check for Array, IList, etc.
return objectType.IsCollection();
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) {
// ignore existingValue and just create a new collection
return JsonSerializer.CreateDefault().Deserialize(reader, objectType);
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) {
JsonSerializer.CreateDefault().Serialize(writer, value);
}
}
Used like so:
var ser = JsonSerializer.CreateDefault(new JsonSerializerSettings {
Converters = new[] { new ReplaceArrayConverter() }
});
using (var reader = jObj.CreateReader()) {
ser.Populate(reader, model);
}
来源:https://stackoverflow.com/questions/42165648/populate-object-where-objects-are-reused-and-arrays-are-replaced