-Added JsonDataExtensionAttribute

This commit is contained in:
James Newton-King
2013-04-30 00:13:20 +12:00
parent a7d47558d8
commit 64da81d386
17 changed files with 325 additions and 24 deletions
@@ -75,4 +75,4 @@ using System.Security;
// You can specify all the values or you can default the Revision and Build Numbers
// by using the '*' as shown below:
[assembly: AssemblyVersion("4.5.0.0")]
[assembly: AssemblyFileVersion("5.0.4.16028")]
[assembly: AssemblyFileVersion("5.0.4.16030")]
@@ -134,6 +134,173 @@ namespace Newtonsoft.Json.Tests.Serialization
}
public class ExtensionDataTestClass
{
public string Name { get; set; }
[JsonProperty("custom_name")]
public string CustomName { get; set; }
[JsonIgnore]
public IList<int> Ignored { get; set; }
public bool GetPrivate { get; internal set; }
public bool GetOnly
{
get { return true; }
}
public readonly string Readonly = "Readonly";
public IList<int> Ints { get; set; }
[JsonExtensionData]
internal IDictionary<string, JToken> ExtensionData { get; set; }
public ExtensionDataTestClass()
{
Ints = new List<int> { 0 };
}
}
[Test]
public void ExtensionDataTest()
{
string json = @"{
""Ints"": [1,2,3],
""Ignored"": [1,2,3],
""Readonly"": ""Readonly"",
""Name"": ""Actually set!"",
""CustomName"": ""Wrong name!"",
""GetPrivate"": true,
""GetOnly"": true,
""NewValueSimple"": true,
""NewValueComplex"": [1,2,3]
}";
ExtensionDataTestClass c = JsonConvert.DeserializeObject<ExtensionDataTestClass>(json);
Assert.AreEqual("Actually set!", c.Name);
Assert.AreEqual(4, c.Ints.Count);
Assert.AreEqual("Readonly", (string)c.ExtensionData["Readonly"]);
Assert.AreEqual("Wrong name!", (string)c.ExtensionData["CustomName"]);
Assert.AreEqual(true, (bool)c.ExtensionData["GetPrivate"]);
Assert.AreEqual(true, (bool)c.ExtensionData["GetOnly"]);
Assert.AreEqual(true, (bool)c.ExtensionData["NewValueSimple"]);
Assert.IsTrue(JToken.DeepEquals(new JArray(1, 2, 3), c.ExtensionData["NewValueComplex"]));
Assert.IsTrue(JToken.DeepEquals(new JArray(1, 2, 3), c.ExtensionData["Ignored"]));
Assert.AreEqual(7, c.ExtensionData.Count);
}
public class MultipleExtensionDataAttributesTestClass
{
public string Name { get; set; }
[JsonExtensionData]
internal IDictionary<string, JToken> ExtensionData1 { get; set; }
[JsonExtensionData]
internal IDictionary<string, JToken> ExtensionData2 { get; set; }
}
public class ExtensionDataAttributesInheritanceTestClass : MultipleExtensionDataAttributesTestClass
{
[JsonExtensionData]
internal IDictionary<string, JToken> ExtensionData0 { get; set; }
}
public class FieldExtensionDataAttributeTestClass
{
[JsonExtensionData]
internal IDictionary<object, object> ExtensionData;
}
public class PublicExtensionDataAttributeTestClass
{
public string Name { get; set; }
[JsonExtensionData]
public IDictionary<object, object> ExtensionData;
}
[Test]
public void SerializePublicExtensionData()
{
string json = JsonConvert.SerializeObject(new PublicExtensionDataAttributeTestClass
{
Name = "Name!",
ExtensionData = new Dictionary<object, object>
{
{ "Test", 1 }
}
});
Assert.AreEqual(@"{""Name"":""Name!""}", json);
}
[Test]
public void DeserializePublicExtensionData()
{
string json = @"{
'Name':'Name!',
'NoMatch':'NoMatch!',
'ExtensionData':{'HAI':true}
}";
var c = JsonConvert.DeserializeObject<PublicExtensionDataAttributeTestClass>(json);
Assert.AreEqual("Name!", c.Name);
Assert.AreEqual(2, c.ExtensionData.Count);
Assert.AreEqual("NoMatch!", (string)(JValue)c.ExtensionData["NoMatch"]);
// the ExtensionData property is put into the extension data
// inception
var o = (JObject)c.ExtensionData["ExtensionData"];
Assert.AreEqual(1, o.Count);
Assert.IsTrue(JToken.DeepEquals(new JObject { { "HAI", true } }, o));
}
[Test]
public void FieldExtensionDataAttributeTest_Serialize()
{
FieldExtensionDataAttributeTestClass c = new FieldExtensionDataAttributeTestClass
{
ExtensionData = new Dictionary<object, object>()
};
string json = JsonConvert.SerializeObject(c);
Assert.AreEqual("{}", json);
}
[Test]
public void FieldExtensionDataAttributeTest_Deserialize()
{
var c = JsonConvert.DeserializeObject<FieldExtensionDataAttributeTestClass>("{'first':1,'second':2}");
Assert.AreEqual(2, c.ExtensionData.Count);
Assert.AreEqual(1, (int)(JToken)c.ExtensionData["first"]);
Assert.AreEqual(2, (int)(JToken)c.ExtensionData["second"]);
}
[Test]
public void MultipleExtensionDataAttributesTest()
{
var c = JsonConvert.DeserializeObject<MultipleExtensionDataAttributesTestClass>("{'first':1,'second':2}");
Assert.AreEqual(null, c.ExtensionData1);
Assert.AreEqual(2, c.ExtensionData2.Count);
Assert.AreEqual(1, (int)c.ExtensionData2["first"]);
Assert.AreEqual(2, (int)c.ExtensionData2["second"]);
}
[Test]
public void ExtensionDataAttributesInheritanceTest()
{
var c = JsonConvert.DeserializeObject<ExtensionDataAttributesInheritanceTestClass>("{'first':1,'second':2}");
Assert.AreEqual(null, c.ExtensionData1);
Assert.AreEqual(null, c.ExtensionData2);
Assert.AreEqual(2, c.ExtensionData0.Count);
Assert.AreEqual(1, (int)c.ExtensionData0["first"]);
Assert.AreEqual(2, (int)c.ExtensionData0["second"]);
}
[Test]
public void GenericCollectionInheritance()
{
@@ -0,0 +1,12 @@
using System;
namespace Newtonsoft.Json
{
/// <summary>
/// Instructs the <see cref="JsonSerializer"/> to populate properties with no matching class member onto the specified collection.
/// </summary>
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false)]
public class JsonExtensionDataAttribute : Attribute
{
}
}
+1
View File
@@ -484,6 +484,7 @@ namespace Newtonsoft.Json
/// </returns>
public static JsonSerializer CreateDefault()
{
// copy static to local variable to avoid concurrency issues
Func<JsonSerializerSettings> defaultSettingsCreator = JsonConvert.DefaultSettings;
JsonSerializerSettings defaultSettings = (defaultSettingsCreator != null) ? defaultSettingsCreator() : null;
@@ -103,6 +103,7 @@
<Compile Include="JsonConstructorAttribute.cs" />
<Compile Include="JsonDictionaryAttribute.cs" />
<Compile Include="JsonException.cs" />
<Compile Include="JsonExtensionDataAttribute.cs" />
<Compile Include="JsonPosition.cs" />
<Compile Include="Linq\JPropertyKeyedCollection.cs" />
<Compile Include="SerializationBinder.cs" />
@@ -114,6 +114,7 @@
<Compile Include="JsonConstructorAttribute.cs" />
<Compile Include="JsonDictionaryAttribute.cs" />
<Compile Include="JsonException.cs" />
<Compile Include="JsonExtensionDataAttribute.cs" />
<Compile Include="JsonPosition.cs" />
<Compile Include="Linq\JPropertyDescriptor.cs" />
<Compile Include="Linq\JPropertyKeyedCollection.cs" />
@@ -116,6 +116,7 @@
<Compile Include="DateTimeZoneHandling.cs" />
<Compile Include="Formatting.cs" />
<Compile Include="JsonConstructorAttribute.cs" />
<Compile Include="JsonExtensionDataAttribute.cs" />
<Compile Include="JsonPosition.cs" />
<Compile Include="Linq\JPropertyKeyedCollection.cs" />
<Compile Include="Serialization\DiagnosticsTraceWriter.cs" />
@@ -87,6 +87,7 @@
<Compile Include="JsonConverterCollection.cs" />
<Compile Include="JsonDictionaryAttribute.cs" />
<Compile Include="JsonException.cs" />
<Compile Include="JsonExtensionDataAttribute.cs" />
<Compile Include="JsonIgnoreAttribute.cs" />
<Compile Include="JsonObjectAttribute.cs" />
<Compile Include="JsonPosition.cs" />
@@ -87,6 +87,7 @@
<Compile Include="JsonConverterCollection.cs" />
<Compile Include="JsonDictionaryAttribute.cs" />
<Compile Include="JsonException.cs" />
<Compile Include="JsonExtensionDataAttribute.cs" />
<Compile Include="JsonIgnoreAttribute.cs" />
<Compile Include="JsonObjectAttribute.cs" />
<Compile Include="JsonPosition.cs" />
@@ -88,6 +88,7 @@
<Compile Include="JsonConverterCollection.cs" />
<Compile Include="JsonDictionaryAttribute.cs" />
<Compile Include="JsonException.cs" />
<Compile Include="JsonExtensionDataAttribute.cs" />
<Compile Include="JsonIgnoreAttribute.cs" />
<Compile Include="JsonObjectAttribute.cs" />
<Compile Include="JsonPosition.cs" />
@@ -92,6 +92,6 @@ using System.Security;
// You can specify all the values or you can default the Revision and Build Numbers
// by using the '*' as shown below:
[assembly: AssemblyVersion("4.5.0.0")]
[assembly: AssemblyFileVersion("5.0.4.16028")]
[assembly: AssemblyFileVersion("5.0.4.16030")]
[assembly: CLSCompliant(true)]
@@ -389,9 +389,75 @@ namespace Newtonsoft.Json.Serialization
contract.ConstructorParameters.AddRange(CreateConstructorParameters(constructor, contract.Properties));
}
}
contract.ExtensionDataSetter = GetExtensionDataForType(contract.NonNullableUnderlyingType);
return contract;
}
private ExtensionDataSetter GetExtensionDataForType(Type type)
{
ExtensionDataSetter extensionDataSetter = null;
foreach (Type baseType in GetClassHierarchyForType(type))
{
IList<MemberInfo> members = new List<MemberInfo>();
members.AddRange(baseType.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly));
members.AddRange(baseType.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly));
foreach (MemberInfo member in members)
{
MemberTypes memberType = member.MemberType();
if (memberType != MemberTypes.Property && memberType != MemberTypes.Field)
continue;
// last instance of attribute wins on type if there are multiple
if (!member.IsDefined(typeof(JsonExtensionDataAttribute), false))
continue;
Type t = ReflectionUtils.GetMemberUnderlyingType(member);
Type dictionaryType;
if (ReflectionUtils.ImplementsGenericDefinition(t, typeof(IDictionary<,>), out dictionaryType))
{
Type keyType = dictionaryType.GetGenericArguments()[0];
Type valueType = dictionaryType.GetGenericArguments()[1];
// change type to a class if it is the base interface so it can be instantiated if needed
if (ReflectionUtils.IsGenericDefinition(t, typeof(IDictionary<,>)))
t = typeof(Dictionary<,>).MakeGenericType(keyType, valueType);
if (keyType.IsAssignableFrom(typeof(string)) && valueType.IsAssignableFrom(typeof(JToken)))
{
MethodInfo addMethod = t.GetMethod("Add", new [] { keyType, valueType });
Func<object, object> getExtensionDataDictionary = JsonTypeReflector.ReflectionDelegateFactory.CreateGet<object>(member);
Action<object, object> setExtensionDataDictionary = JsonTypeReflector.ReflectionDelegateFactory.CreateSet<object>(member);
Func<object> createExtensionDataDictionary = JsonTypeReflector.ReflectionDelegateFactory.CreateDefaultConstructor<object>(t);
MethodCall<object, object> setExtensionDataDictionaryValue = JsonTypeReflector.ReflectionDelegateFactory.CreateMethodCall<object>(addMethod);
extensionDataSetter = (o, key, value) =>
{
object dictionary = getExtensionDataDictionary(o);
if (dictionary == null)
{
dictionary = createExtensionDataDictionary();
setExtensionDataDictionary(o, dictionary);
}
setExtensionDataDictionaryValue(dictionary, key, value);
};
continue;
}
}
throw new JsonException("Invalid extension data attribute on '{0}'. Member '{1}' type must implement IDictionary<string, JToken>.".FormatWith(CultureInfo.InvariantCulture, GetClrTypeFullName(member.DeclaringType), member.Name));
}
}
return extensionDataSetter;
}
private ConstructorInfo GetAttributeConstructor(Type objectType)
{
IList<ConstructorInfo> markedConstructors = objectType.GetConstructors(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic).Where(c => c.IsDefined(typeof(JsonConstructorAttribute), true)).ToList();
@@ -593,8 +659,7 @@ namespace Newtonsoft.Json.Serialization
foreach (Type baseType in GetClassHierarchyForType(type))
{
// while we allow more than one OnSerialized total, only one can be defined
// per class
// while we allow more than one OnSerialized total, only one can be defined per class
MethodInfo currentOnSerializing = null;
MethodInfo currentOnSerialized = null;
MethodInfo currentOnDeserializing = null;
@@ -1020,6 +1085,8 @@ namespace Newtonsoft.Json.Serialization
bool hasJsonIgnoreAttribute =
JsonTypeReflector.GetAttribute<JsonIgnoreAttribute>(attributeProvider) != null
// automatically ignore extension data dictionary property if it is public
|| JsonTypeReflector.GetAttribute<JsonExtensionDataAttribute>(attributeProvider) != null
#if !(SILVERLIGHT || NETFX_CORE || PORTABLE40 || PORTABLE)
|| JsonTypeReflector.GetAttribute<NonSerializedAttribute>(attributeProvider) != null
#endif
@@ -27,6 +27,7 @@ using System;
using System.Collections.Generic;
using System.Reflection;
using System.Runtime.Serialization;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Utilities;
namespace Newtonsoft.Json.Serialization
@@ -63,6 +64,14 @@ namespace Newtonsoft.Json.Serialization
/// <param name="errorContext">The error context.</param>
public delegate void SerializationErrorCallback(object o, StreamingContext context, ErrorContext errorContext);
/// <summary>
/// Sets extension data for an object during deserialization.
/// </summary>
/// <param name="o">The object to set extension data on.</param>
/// <param name="key">The extension data key.</param>
/// <param name="value">The extension data value.</param>
public delegate void ExtensionDataSetter(object o, string key, JToken value);
/// <summary>
/// Contract details for a <see cref="Type"/> used by the <see cref="JsonSerializer"/>.
/// </summary>
@@ -76,6 +76,11 @@ namespace Newtonsoft.Json.Serialization
/// <value>The parametrized constructor.</value>
public ConstructorInfo ParametrizedConstructor { get; set; }
/// <summary>
/// Gets or sets the extension data setter.
/// </summary>
public ExtensionDataSetter ExtensionDataSetter { get; set; }
private bool? _hasRequiredOrDefaultValueProperties;
internal bool HasRequiredOrDefaultValueProperties
{
@@ -719,7 +719,7 @@ To fix this error either change the JSON to a {1} or change the deserialized typ
return value;
}
private void SetPropertyValue(JsonProperty property, JsonConverter propertyConverter, JsonContainerContract containerContract, JsonProperty containerProperty, JsonReader reader, object target)
private bool SetPropertyValue(JsonProperty property, JsonConverter propertyConverter, JsonContainerContract containerContract, JsonProperty containerProperty, JsonReader reader, object target)
{
object currentValue;
bool useExistingValue;
@@ -727,7 +727,7 @@ To fix this error either change the JSON to a {1} or change the deserialized typ
bool gottenCurrentValue;
if (CalculatePropertyDetails(property, ref propertyConverter, containerContract, containerProperty, reader, target, out useExistingValue, out currentValue, out propertyContract, out gottenCurrentValue))
return;
return false;
object value;
@@ -758,7 +758,12 @@ To fix this error either change the JSON to a {1} or change the deserialized typ
property.SetIsSpecified(target, true);
}
return true;
}
// the value wasn't set be JSON was populated onto the existing value
return useExistingValue;
}
private bool CalculatePropertyDetails(JsonProperty property, ref JsonConverter propertyConverter, JsonContainerContract containerContract, JsonProperty containerProperty, JsonReader reader, object target, out bool useExistingValue, out object currentValue, out JsonContract propertyContract, out bool gottenCurrentValue)
@@ -769,10 +774,7 @@ To fix this error either change the JSON to a {1} or change the deserialized typ
gottenCurrentValue = false;
if (property.Ignored)
{
reader.Skip();
return true;
}
JsonToken tokenType = reader.TokenType;
@@ -798,26 +800,17 @@ To fix this error either change the JSON to a {1} or change the deserialized typ
}
if (!property.Writable && !useExistingValue)
{
reader.Skip();
return true;
}
// test tokentype here because null might not be convertable to some types, e.g. ignoring null when applied to DateTime
if (property.NullValueHandling.GetValueOrDefault(Serializer._nullValueHandling) == NullValueHandling.Ignore && tokenType == JsonToken.Null)
{
reader.Skip();
return true;
}
// test tokentype here because default value might not be convertable to actual type, e.g. default of "" for DateTime
if (HasFlag(property.DefaultValueHandling.GetValueOrDefault(Serializer._defaultValueHandling), DefaultValueHandling.Ignore)
&& JsonReader.IsPrimitiveToken(tokenType)
&& MiscellaneousUtils.ValueEquals(reader.Value, property.GetResolvedDefaultValue()))
{
reader.Skip();
return true;
}
if (currentValue == null)
{
@@ -1367,7 +1360,8 @@ To fix this error either change the environment to be fully trusted, change the
JsonConverter propertyConverter = GetConverter(property.PropertyContract, property.MemberConverter, null, null);
SetPropertyValue(property, propertyConverter, null, member, reader, newObject);
if (!SetPropertyValue(property, propertyConverter, null, member, reader, newObject))
reader.Skip();
}
else
{
@@ -1700,7 +1694,10 @@ To fix this error either change the environment to be fully trusted, change the
if (Serializer._missingMemberHandling == MissingMemberHandling.Error)
throw JsonSerializationException.Create(reader, "Could not find member '{0}' on object of type '{1}'".FormatWith(CultureInfo.InvariantCulture, memberName, contract.UnderlyingType.Name));
reader.Skip();
if (!reader.Read())
break;
SetExtensionData(contract, reader, memberName, newObject);
continue;
}
@@ -1714,7 +1711,9 @@ To fix this error either change the environment to be fully trusted, change the
SetPropertyPresence(reader, property, propertiesPresence);
SetPropertyValue(property, propertyConverter, contract, member, reader, newObject);
// set extension data if property is ignored or readonly
if (!SetPropertyValue(property, propertyConverter, contract, member, reader, newObject))
SetExtensionData(contract, reader, memberName, newObject);
}
catch (Exception ex)
{
@@ -1745,6 +1744,27 @@ To fix this error either change the environment to be fully trusted, change the
return newObject;
}
private void SetExtensionData(JsonObjectContract contract, JsonReader reader, string memberName, object o)
{
if (contract.ExtensionDataSetter != null)
{
try
{
JToken extensionDataValue = JToken.ReadFrom(reader);
contract.ExtensionDataSetter(o, memberName, extensionDataValue);
}
catch (Exception ex)
{
throw JsonSerializationException.Create(reader, "Error setting value in extension data for type '{0}'.".FormatWith(CultureInfo.InvariantCulture, contract.UnderlyingType), ex);
}
}
else
{
reader.Skip();
}
}
private void EndObject(object newObject, JsonReader reader, JsonObjectContract contract, int initialDepth, Dictionary<JsonProperty, PropertyPresence> propertiesPresence)
{
if (propertiesPresence != null)
@@ -80,7 +80,13 @@ namespace Newtonsoft.Json.Utilities
}
if (method is MethodInfo)
callExpression = EnsureCastExpression(callExpression, type);
{
MethodInfo m = (MethodInfo)method;
if (m.ReturnType != typeof(void))
callExpression = EnsureCastExpression(callExpression, type);
else
callExpression = Expression.Block(callExpression, Expression.Constant(null));
}
LambdaExpression lambdaExpression = Expression.Lambda(typeof(MethodCall<T, object>), callExpression, targetParameterExpression, argsParameterExpression);
@@ -348,7 +348,11 @@ namespace Newtonsoft.Json.Utilities
public static IEnumerable<PropertyInfo> GetProperties(this Type type, BindingFlags bindingFlags)
{
return type.GetTypeInfo().GetPropertiesRecursive().Where(p => TestAccessibility(p, bindingFlags));
IList<PropertyInfo> properties = (bindingFlags.HasFlag(BindingFlags.DeclaredOnly))
? type.GetTypeInfo().DeclaredProperties.ToList()
: type.GetTypeInfo().GetPropertiesRecursive();
return properties.Where(p => TestAccessibility(p, bindingFlags));
}
private static IList<MemberInfo> GetMembersRecursive(this TypeInfo type)
@@ -424,7 +428,11 @@ namespace Newtonsoft.Json.Utilities
public static IEnumerable<FieldInfo> GetFields(this Type type, BindingFlags bindingFlags)
{
return type.GetTypeInfo().GetFieldsRecursive().Where(f => TestAccessibility(f, bindingFlags)).ToList();
IList<FieldInfo> fields = (bindingFlags.HasFlag(BindingFlags.DeclaredOnly))
? type.GetTypeInfo().DeclaredFields.ToList()
: type.GetTypeInfo().GetFieldsRecursive();
return fields.Where(f => TestAccessibility(f, bindingFlags)).ToList();
}
private static bool TestAccessibility(PropertyInfo member, BindingFlags bindingFlags)