Skip to content

Commit

Permalink
Add JsonIncludeAttribute & support for non-public property accessors
Browse files Browse the repository at this point in the history
  • Loading branch information
layomia committed Apr 10, 2020
1 parent 8e8090c commit ae62316
Show file tree
Hide file tree
Showing 16 changed files with 540 additions and 137 deletions.
5 changes: 5 additions & 0 deletions src/libraries/System.Text.Json/ref/System.Text.Json.cs
Original file line number Diff line number Diff line change
Expand Up @@ -507,6 +507,11 @@ public sealed partial class JsonIgnoreAttribute : System.Text.Json.Serialization
public JsonIgnoreAttribute() { }
public System.Text.Json.Serialization.JsonIgnoreCondition Condition { get { throw null; } set { } }
}
[System.AttributeUsageAttribute(System.AttributeTargets.Property, AllowMultiple = false)]
public sealed partial class JsonIncludeAttribute : System.Text.Json.Serialization.JsonAttribute
{
public JsonIncludeAttribute() { }
}
[System.AttributeUsageAttribute(System.AttributeTargets.Property, AllowMultiple=false)]
public sealed partial class JsonPropertyNameAttribute : System.Text.Json.Serialization.JsonAttribute
{
Expand Down
5 changes: 4 additions & 1 deletion src/libraries/System.Text.Json/src/Resources/Strings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -512,4 +512,7 @@
<data name="SerializeTypeInstanceNotSupported" xml:space="preserve">
<value>Serialization and deserialization of 'System.Type' instances are not supported and should be avoided since they can lead to security issues.</value>
</data>
</root>
<data name="JsonIncludeOnNonPublicInvalid" xml:space="preserve">
<value>The non-public property '{0}' on type '{1}' is annotated with 'JsonIncludeAttribute' which is invalid.</value>
</data>
</root>
1 change: 1 addition & 0 deletions src/libraries/System.Text.Json/src/System.Text.Json.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@
<Compile Include="System\Text\Json\Serialization\JsonPropertyInfo.cs" />
<Compile Include="System\Text\Json\Serialization\JsonPropertyInfoOfTTypeToConvert.cs" />
<Compile Include="System\Text\Json\Serialization\JsonPropertyNameAttribute.cs" />
<Compile Include="System\Text\Json\Serialization\JsonIncludeAttribute.cs" />
<Compile Include="System\Text\Json\Serialization\JsonResumableConverterOfT.cs" />
<Compile Include="System\Text\Json\Serialization\JsonSerializer.Read.HandleMetadata.cs" />
<Compile Include="System\Text\Json\Serialization\JsonSerializer.Read.HandlePropertyName.cs" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,14 +77,16 @@ public Dictionary<string, JsonParameterInfo> CreateParameterCache(int capacity,
}
}

public static JsonPropertyInfo AddProperty(Type propertyType, PropertyInfo propertyInfo, Type parentClassType, JsonSerializerOptions options)
public static JsonPropertyInfo AddProperty(PropertyInfo propertyInfo, Type parentClassType, JsonSerializerOptions options)
{
JsonIgnoreAttribute? ignoreAttribute = JsonPropertyInfo.GetAttribute<JsonIgnoreAttribute>(propertyInfo);
if (ignoreAttribute?.Condition == JsonIgnoreCondition.Always)
{
return JsonPropertyInfo.CreateIgnoredPropertyPlaceholder(propertyInfo, options);
}

Type propertyType = propertyInfo.PropertyType;

JsonConverter converter = GetConverter(
propertyType,
parentClassType,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,69 +95,10 @@ public JsonClassInfo(Type type, JsonSerializerOptions options)
{
case ClassType.Object:
{
// Create the policy property.
PropertyInfoForClassInfo = CreatePropertyInfoForClassInfo(type, runtimeType, converter!, options);

CreateObject = options.MemberAccessorStrategy.CreateConstructor(type);

PropertyInfo[] properties = type.GetProperties(BindingFlags.Instance | BindingFlags.Public);

Dictionary<string, JsonPropertyInfo> cache = CreatePropertyCache(properties.Length);

foreach (PropertyInfo propertyInfo in properties)
{
// Ignore indexers
if (propertyInfo.GetIndexParameters().Length > 0)
{
continue;
}

// For now we only support public getters\setters
if (propertyInfo.GetMethod?.IsPublic == true ||
propertyInfo.SetMethod?.IsPublic == true)
{
JsonPropertyInfo jsonPropertyInfo = AddProperty(propertyInfo.PropertyType, propertyInfo, type, options);
Debug.Assert(jsonPropertyInfo != null && jsonPropertyInfo.NameAsString != null);

// If the JsonPropertyNameAttribute or naming policy results in collisions, throw an exception.
if (!JsonHelpers.TryAdd(cache, jsonPropertyInfo.NameAsString, jsonPropertyInfo))
{
JsonPropertyInfo other = cache[jsonPropertyInfo.NameAsString];

if (other.ShouldDeserialize == false && other.ShouldSerialize == false)
{
// Overwrite the one just added since it has [JsonIgnore].
cache[jsonPropertyInfo.NameAsString] = jsonPropertyInfo;
}
else if (jsonPropertyInfo.ShouldDeserialize == true || jsonPropertyInfo.ShouldSerialize == true)
{
ThrowHelper.ThrowInvalidOperationException_SerializerPropertyNameConflict(Type, jsonPropertyInfo);
}
// else ignore jsonPropertyInfo since it has [JsonIgnore].
}
}
}

JsonPropertyInfo[] cacheArray;
if (DetermineExtensionDataProperty(cache))
{
// Remove from cache since it is handled independently.
cache.Remove(DataExtensionProperty!.NameAsString!);

cacheArray = new JsonPropertyInfo[cache.Count + 1];

// Set the last element to the extension property.
cacheArray[cache.Count] = DataExtensionProperty;
}
else
{
cacheArray = new JsonPropertyInfo[cache.Count];
}

// Set fields when finished to avoid concurrency issues.
PropertyCache = cache;
cache.Values.CopyTo(cacheArray, 0);
PropertyCacheArray = cacheArray;
HandlePublicProperties();
HandleNonPublicProperties();

if (converter.ConstructorIsParameterized)
{
Expand Down Expand Up @@ -189,6 +130,90 @@ public JsonClassInfo(Type type, JsonSerializerOptions options)
}
}

private void HandlePublicProperties()
{
PropertyInfo[] properties = Type.GetProperties(BindingFlags.Instance | BindingFlags.Public);

Dictionary<string, JsonPropertyInfo> cache = CreatePropertyCache(properties.Length);

foreach (PropertyInfo propertyInfo in properties)
{
// Ignore indexers
if (propertyInfo.GetIndexParameters().Length > 0)
{
continue;
}

// For now we only support public getters\setters
if (propertyInfo.GetMethod?.IsPublic == true ||
propertyInfo.SetMethod?.IsPublic == true)
{
JsonPropertyInfo jsonPropertyInfo = AddProperty(
propertyInfo,
Type,
Options);
Debug.Assert(jsonPropertyInfo != null && jsonPropertyInfo.NameAsString != null);

// If the JsonPropertyNameAttribute or naming policy results in collisions, throw an exception.
if (!JsonHelpers.TryAdd(cache, jsonPropertyInfo.NameAsString, jsonPropertyInfo))
{
JsonPropertyInfo other = cache[jsonPropertyInfo.NameAsString];

if (other.ShouldDeserialize == false && other.ShouldSerialize == false)
{
// Overwrite the one just added since it has [JsonIgnore].
cache[jsonPropertyInfo.NameAsString] = jsonPropertyInfo;
}
else if (jsonPropertyInfo.ShouldDeserialize == true || jsonPropertyInfo.ShouldSerialize == true)
{
ThrowHelper.ThrowInvalidOperationException_SerializerPropertyNameConflict(Type, jsonPropertyInfo);
}
// else ignore jsonPropertyInfo since it has [JsonIgnore].
}
}
}

JsonPropertyInfo[] cacheArray;
if (DetermineExtensionDataProperty(cache))
{
// Remove from cache since it is handled independently.
cache.Remove(DataExtensionProperty!.NameAsString!);

cacheArray = new JsonPropertyInfo[cache.Count + 1];

// Set the last element to the extension property.
cacheArray[cache.Count] = DataExtensionProperty;
}
else
{
cacheArray = new JsonPropertyInfo[cache.Count];
}

// Set fields when finished to avoid concurrency issues.
PropertyCache = cache;
cache.Values.CopyTo(cacheArray, 0);
PropertyCacheArray = cacheArray;
}

private void HandleNonPublicProperties()
{
PropertyInfo[] properties = Type.GetProperties(BindingFlags.Instance | BindingFlags.NonPublic);

foreach (PropertyInfo propertyInfo in properties)
{
// Ignore indexers
if (propertyInfo.GetIndexParameters().Length > 0)
{
continue;
}

if (JsonPropertyInfo.GetAttribute<JsonIncludeAttribute>(propertyInfo) != null)
{
ThrowHelper.ThrowInvalidOperationException_JsonIncludeOnNonPublicInvalid(propertyInfo, Type);
}
}
}

private void InitializeConstructorParameters(ConstructorInfo constructorInfo)
{
ParameterInfo[] parameters = constructorInfo!.GetParameters();
Expand Down Expand Up @@ -254,52 +279,6 @@ private void InitializeConstructorParameters(ConstructorInfo constructorInfo)
PropertyCache = propertyCache;
}

public bool DetermineExtensionDataProperty(Dictionary<string, JsonPropertyInfo> cache)
{
JsonPropertyInfo? jsonPropertyInfo = GetPropertyWithUniqueAttribute(Type, typeof(JsonExtensionDataAttribute), cache);
if (jsonPropertyInfo != null)
{
Type declaredPropertyType = jsonPropertyInfo.DeclaredPropertyType;
if (typeof(IDictionary<string, object>).IsAssignableFrom(declaredPropertyType) ||
typeof(IDictionary<string, JsonElement>).IsAssignableFrom(declaredPropertyType))
{
JsonConverter converter = Options.GetConverter(declaredPropertyType);
Debug.Assert(converter != null);
}
else
{
ThrowHelper.ThrowInvalidOperationException_SerializationDataExtensionPropertyInvalid(Type, jsonPropertyInfo);
}

DataExtensionProperty = jsonPropertyInfo;
return true;
}

return false;
}

private static JsonPropertyInfo? GetPropertyWithUniqueAttribute(Type classType, Type attributeType, Dictionary<string, JsonPropertyInfo> cache)
{
JsonPropertyInfo? property = null;

foreach (JsonPropertyInfo jsonPropertyInfo in cache.Values)
{
Debug.Assert(jsonPropertyInfo.PropertyInfo != null);
Attribute? attribute = jsonPropertyInfo.PropertyInfo.GetCustomAttribute(attributeType);
if (attribute != null)
{
if (property != null)
{
ThrowHelper.ThrowInvalidOperationException_SerializationDuplicateTypeAttribute(classType, attributeType);
}

property = jsonPropertyInfo;
}
}

return property;
}

private static JsonParameterInfo AddConstructorParameter(
ParameterInfo parameterInfo,
JsonPropertyInfo jsonPropertyInfo,
Expand Down Expand Up @@ -382,5 +361,51 @@ public static JsonConverter GetConverter(

return converter;
}

public bool DetermineExtensionDataProperty(Dictionary<string, JsonPropertyInfo> cache)
{
JsonPropertyInfo? jsonPropertyInfo = GetPropertyWithUniqueAttribute(Type, typeof(JsonExtensionDataAttribute), cache);
if (jsonPropertyInfo != null)
{
Type declaredPropertyType = jsonPropertyInfo.DeclaredPropertyType;
if (typeof(IDictionary<string, object>).IsAssignableFrom(declaredPropertyType) ||
typeof(IDictionary<string, JsonElement>).IsAssignableFrom(declaredPropertyType))
{
JsonConverter converter = Options.GetConverter(declaredPropertyType);
Debug.Assert(converter != null);
}
else
{
ThrowHelper.ThrowInvalidOperationException_SerializationDataExtensionPropertyInvalid(Type, jsonPropertyInfo);
}

DataExtensionProperty = jsonPropertyInfo;
return true;
}

return false;
}

private static JsonPropertyInfo? GetPropertyWithUniqueAttribute(Type classType, Type attributeType, Dictionary<string, JsonPropertyInfo> cache)
{
JsonPropertyInfo? property = null;

foreach (JsonPropertyInfo jsonPropertyInfo in cache.Values)
{
Debug.Assert(jsonPropertyInfo.PropertyInfo != null);
Attribute? attribute = jsonPropertyInfo.PropertyInfo.GetCustomAttribute(attributeType);
if (attribute != null)
{
if (property != null)
{
ThrowHelper.ThrowInvalidOperationException_SerializationDuplicateTypeAttribute(classType, attributeType);
}

property = jsonPropertyInfo;
}
}

return property;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

namespace System.Text.Json.Serialization
{
/// <summary>
/// Indicates that the member should be included for serialization and deserialization.
/// </summary>
/// <remarks>
/// When applied to a property, indicates that non-public getters and setters can be used for serialization and deserialization.
/// </remarks>
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public sealed class JsonIncludeAttribute : JsonAttribute
{
/// <summary>
/// Initializes a new instance of <see cref="JsonIncludeAttribute"/>.
/// </summary>
public JsonIncludeAttribute() { }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,17 @@ public override void Initialize(

if (propertyInfo != null)
{
if (propertyInfo.GetMethod?.IsPublic == true)
bool useNonPublicAccessors = GetAttribute<JsonIncludeAttribute>(propertyInfo) != null;

MethodInfo? getMethod = propertyInfo.GetMethod;
if (getMethod != null && (getMethod.IsPublic || useNonPublicAccessors))
{
HasGetter = true;
Get = options.MemberAccessorStrategy.CreatePropertyGetter<TTypeToConvert>(propertyInfo);
}

if (propertyInfo.SetMethod?.IsPublic == true)
MethodInfo? setMethod = propertyInfo.SetMethod;
if (setMethod != null && (setMethod.IsPublic || useNonPublicAccessors))
{
HasSetter = true;
Set = options.MemberAccessorStrategy.CreatePropertySetter<TTypeToConvert>(propertyInfo);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ internal static JsonPropertyInfo LookupProperty(
if (jsonPropertyInfo == JsonPropertyInfo.s_missingProperty)
{
JsonPropertyInfo? dataExtProperty = state.Current.JsonClassInfo.DataExtensionProperty;
if (dataExtProperty != null)
if (dataExtProperty != null && dataExtProperty.HasGetter && dataExtProperty.HasSetter)
{
state.Current.JsonPropertyNameAsString = JsonHelpers.Utf8GetString(unescapedPropertyName);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ public override Func<IEnumerable<KeyValuePair<string, TElement>>, TCollection> C

private static Delegate CreatePropertyGetter(PropertyInfo propertyInfo, Type classType, Type propertyType)
{
MethodInfo? realMethod = propertyInfo.GetGetMethod();
MethodInfo? realMethod = propertyInfo.GetMethod;
Type objectType = typeof(object);

Debug.Assert(realMethod != null);
Expand Down Expand Up @@ -268,7 +268,7 @@ private static Delegate CreatePropertyGetter(PropertyInfo propertyInfo, Type cla

private static Delegate CreatePropertySetter(PropertyInfo propertyInfo, Type classType, Type propertyType)
{
MethodInfo? realMethod = propertyInfo.GetSetMethod();
MethodInfo? realMethod = propertyInfo.SetMethod;
Type objectType = typeof(object);

Debug.Assert(realMethod != null);
Expand Down
Loading

0 comments on commit ae62316

Please sign in to comment.