Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,9 @@ public KnownTypeSymbols(Compilation compilation)
public INamedTypeSymbol? JsonNumberHandlingAttributeType => GetOrResolveType("System.Text.Json.Serialization.JsonNumberHandlingAttribute", ref _JsonNumberHandlingAttributeType);
private Option<INamedTypeSymbol?> _JsonNumberHandlingAttributeType;

public INamedTypeSymbol? JsonNamingPolicyAttributeType => GetOrResolveType("System.Text.Json.Serialization.JsonNamingPolicyAttribute", ref _JsonNamingPolicyAttributeType);
private Option<INamedTypeSymbol?> _JsonNamingPolicyAttributeType;

public INamedTypeSymbol? JsonObjectCreationHandlingAttributeType => GetOrResolveType("System.Text.Json.Serialization.JsonObjectCreationHandlingAttribute", ref _JsonObjectCreationHandlingAttributeType);
private Option<INamedTypeSymbol?> _JsonObjectCreationHandlingAttributeType;

Expand Down
71 changes: 64 additions & 7 deletions src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -587,6 +587,7 @@ private TypeGenerationSpec ParseTypeGenerationSpec(in TypeToGenerate typeToGener
out JsonNumberHandling? numberHandling,
out JsonUnmappedMemberHandling? unmappedMemberHandling,
out JsonObjectCreationHandling? preferredPropertyObjectCreationHandling,
out JsonKnownNamingPolicy? typeNamingPolicy,
out JsonIgnoreCondition? typeIgnoreCondition,
out bool foundJsonConverterAttribute,
out TypeRef? customConverterType,
Expand Down Expand Up @@ -692,7 +693,7 @@ private TypeGenerationSpec ParseTypeGenerationSpec(in TypeToGenerate typeToGener
implementsIJsonOnSerialized = _knownSymbols.IJsonOnSerializedType.IsAssignableFrom(type);

ctorParamSpecs = ParseConstructorParameters(typeToGenerate, constructor, out constructionStrategy, out constructorSetsRequiredMembers);
propertySpecs = ParsePropertyGenerationSpecs(contextType, typeToGenerate, typeIgnoreCondition, options, out hasExtensionDataProperty, out fastPathPropertyIndices);
propertySpecs = ParsePropertyGenerationSpecs(contextType, typeToGenerate, typeIgnoreCondition, options, typeNamingPolicy, out hasExtensionDataProperty, out fastPathPropertyIndices);
propertyInitializerSpecs = ParsePropertyInitializers(ctorParamSpecs, propertySpecs, constructorSetsRequiredMembers, ref constructionStrategy);
}

Expand Down Expand Up @@ -749,6 +750,7 @@ private void ProcessTypeCustomAttributes(
out JsonNumberHandling? numberHandling,
out JsonUnmappedMemberHandling? unmappedMemberHandling,
out JsonObjectCreationHandling? objectCreationHandling,
out JsonKnownNamingPolicy? namingPolicy,
out JsonIgnoreCondition? typeIgnoreCondition,
out bool foundJsonConverterAttribute,
out TypeRef? customConverterType,
Expand All @@ -757,6 +759,7 @@ private void ProcessTypeCustomAttributes(
numberHandling = null;
unmappedMemberHandling = null;
objectCreationHandling = null;
namingPolicy = null;
typeIgnoreCondition = null;
customConverterType = null;
foundJsonConverterAttribute = false;
Expand All @@ -781,6 +784,22 @@ private void ProcessTypeCustomAttributes(
objectCreationHandling = (JsonObjectCreationHandling)attributeData.ConstructorArguments[0].Value!;
continue;
}
else if (_knownSymbols.JsonNamingPolicyAttributeType?.IsAssignableFrom(attributeType) == true)
{
if (attributeData.ConstructorArguments.Length == 1 &&
attributeData.ConstructorArguments[0].Value is int knownPolicyValue)
{
namingPolicy = (JsonKnownNamingPolicy)knownPolicyValue;
}
else
{
// The attribute uses a custom naming policy that can't be resolved at compile time.
// Use Unspecified to prevent the global naming policy from incorrectly applying.
namingPolicy = JsonKnownNamingPolicy.Unspecified;
}

continue;
}
else if (!foundJsonConverterAttribute && _knownSymbols.JsonConverterAttributeType.IsAssignableFrom(attributeType))
{
customConverterType = GetConverterTypeFromJsonConverterAttribute(contextType, typeToGenerate.Type, attributeData);
Expand Down Expand Up @@ -1004,6 +1023,7 @@ private List<PropertyGenerationSpec> ParsePropertyGenerationSpecs(
in TypeToGenerate typeToGenerate,
JsonIgnoreCondition? typeIgnoreCondition,
SourceGenerationOptionsSpec? options,
JsonKnownNamingPolicy? typeNamingPolicy,
out bool hasExtensionDataProperty,
out List<int>? fastPathPropertyIndices)
{
Expand Down Expand Up @@ -1088,7 +1108,8 @@ void AddMember(
typeIgnoreCondition,
ref hasExtensionDataProperty,
generationMode,
options);
options,
typeNamingPolicy);

if (propertySpec is null)
{
Expand Down Expand Up @@ -1237,7 +1258,8 @@ private bool IsValidDataExtensionPropertyType(ITypeSymbol type)
JsonIgnoreCondition? typeIgnoreCondition,
ref bool typeHasExtensionDataProperty,
JsonSourceGenerationMode? generationMode,
SourceGenerationOptionsSpec? options)
SourceGenerationOptionsSpec? options,
JsonKnownNamingPolicy? typeNamingPolicy)
{
Debug.Assert(memberInfo is IFieldSymbol or IPropertySymbol);

Expand All @@ -1250,6 +1272,7 @@ private bool IsValidDataExtensionPropertyType(ITypeSymbol type)
out JsonIgnoreCondition? ignoreCondition,
out JsonNumberHandling? numberHandling,
out JsonObjectCreationHandling? objectCreationHandling,
out JsonKnownNamingPolicy? memberNamingPolicy,
out TypeRef? converterType,
out int order,
out bool isExtensionData,
Expand Down Expand Up @@ -1319,9 +1342,18 @@ private bool IsValidDataExtensionPropertyType(ITypeSymbol type)
return null;
}

string effectiveJsonPropertyName = DetermineEffectiveJsonPropertyName(memberInfo.Name, jsonPropertyName, options);
string effectiveJsonPropertyName = DetermineEffectiveJsonPropertyName(memberInfo.Name, jsonPropertyName, memberNamingPolicy, typeNamingPolicy, options);
string propertyNameFieldName = DeterminePropertyNameFieldName(effectiveJsonPropertyName);

// For metadata-based serialization, embed the effective property name when a naming policy
// attribute is present so that the runtime DeterminePropertyName uses it instead of
// falling back to the global PropertyNamingPolicy. JsonPropertyNameAttribute values are
// already captured in jsonPropertyName and take highest precedence.
string? metadataJsonPropertyName = jsonPropertyName
?? (memberNamingPolicy is not null || typeNamingPolicy is not null
? effectiveJsonPropertyName
: null);

// Enqueue the property type for generation, unless the member is ignored.
TypeRef propertyTypeRef = ignoreCondition != JsonIgnoreCondition.Always
? EnqueueType(memberType, generationMode)
Expand All @@ -1334,7 +1366,7 @@ private bool IsValidDataExtensionPropertyType(ITypeSymbol type)
IsProperty = memberInfo is IPropertySymbol,
IsPublic = isAccessible,
IsVirtual = memberInfo.IsVirtual(),
JsonPropertyName = jsonPropertyName,
JsonPropertyName = metadataJsonPropertyName,
EffectiveJsonPropertyName = effectiveJsonPropertyName,
PropertyNameFieldName = propertyNameFieldName,
IsReadOnly = isReadOnly,
Expand Down Expand Up @@ -1366,6 +1398,7 @@ private void ProcessMemberCustomAttributes(
out JsonIgnoreCondition? ignoreCondition,
out JsonNumberHandling? numberHandling,
out JsonObjectCreationHandling? objectCreationHandling,
out JsonKnownNamingPolicy? memberNamingPolicy,
out TypeRef? converterType,
out int order,
out bool isExtensionData,
Expand All @@ -1378,6 +1411,7 @@ private void ProcessMemberCustomAttributes(
ignoreCondition = default;
numberHandling = default;
objectCreationHandling = default;
memberNamingPolicy = default;
converterType = null;
order = 0;
isExtensionData = false;
Expand All @@ -1396,6 +1430,20 @@ private void ProcessMemberCustomAttributes(
{
converterType = GetConverterTypeFromJsonConverterAttribute(contextType, memberInfo, attributeData, memberType);
}
else if (memberNamingPolicy is null && _knownSymbols.JsonNamingPolicyAttributeType?.IsAssignableFrom(attributeType) == true)
{
if (attributeData.ConstructorArguments.Length == 1 &&
attributeData.ConstructorArguments[0].Value is int knownPolicyValue)
{
memberNamingPolicy = (JsonKnownNamingPolicy)knownPolicyValue;
}
else
{
// The attribute uses a custom naming policy that can't be resolved at compile time.
// Use Unspecified to prevent the global naming policy from incorrectly applying.
memberNamingPolicy = JsonKnownNamingPolicy.Unspecified;
}
}
else if (attributeType.ContainingAssembly.Name == SystemTextJsonNamespace)
{
switch (attributeType.ToDisplayString())
Expand Down Expand Up @@ -1843,14 +1891,23 @@ private static int GetTotalTypeParameterCount(INamedTypeSymbol unboundType)
return constructedContainingType;
}

private static string DetermineEffectiveJsonPropertyName(string propertyName, string? jsonPropertyName, SourceGenerationOptionsSpec? options)
private static string DetermineEffectiveJsonPropertyName(
string propertyName,
string? jsonPropertyName,
JsonKnownNamingPolicy? memberNamingPolicy,
JsonKnownNamingPolicy? typeNamingPolicy,
SourceGenerationOptionsSpec? options)
{
if (jsonPropertyName != null)
{
return jsonPropertyName;
}

JsonNamingPolicy? instance = options?.GetEffectivePropertyNamingPolicy() switch
JsonKnownNamingPolicy? effectiveKnownPolicy = memberNamingPolicy
?? typeNamingPolicy
?? options?.GetEffectivePropertyNamingPolicy();

JsonNamingPolicy? instance = effectiveKnownPolicy switch
{
JsonKnownNamingPolicy.CamelCase => JsonNamingPolicy.CamelCase,
JsonKnownNamingPolicy.SnakeCaseLower => JsonNamingPolicy.SnakeCaseLower,
Expand Down
7 changes: 7 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 @@ -1079,6 +1079,13 @@ public enum JsonKnownNamingPolicy
KebabCaseLower = 4,
KebabCaseUpper = 5,
}
[System.AttributeUsageAttribute(System.AttributeTargets.Class | System.AttributeTargets.Field | System.AttributeTargets.Interface | System.AttributeTargets.Property | System.AttributeTargets.Struct, AllowMultiple=false)]
public partial class JsonNamingPolicyAttribute : System.Text.Json.Serialization.JsonAttribute
{
public JsonNamingPolicyAttribute(System.Text.Json.Serialization.JsonKnownNamingPolicy namingPolicy) { }
protected JsonNamingPolicyAttribute(System.Text.Json.JsonNamingPolicy namingPolicy) { }
public System.Text.Json.JsonNamingPolicy NamingPolicy { get { throw null; } }
}
public enum JsonKnownReferenceHandler
{
Unspecified = 0,
Expand Down
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 @@ -116,6 +116,7 @@ The System.Text.Json library is built-in as part of the shared framework in .NET
<Compile Include="System\Text\Json\Serialization\Attributes\JsonIgnoreAttribute.cs" />
<Compile Include="System\Text\Json\Serialization\Attributes\JsonIncludeAttribute.cs" />
<Compile Include="System\Text\Json\Serialization\Attributes\JsonNumberHandlingAttribute.cs" />
<Compile Include="System\Text\Json\Serialization\Attributes\JsonNamingPolicyAttribute.cs" />
<Compile Include="System\Text\Json\Serialization\Attributes\JsonPolymorphicAttribute.cs" />
<Compile Include="System\Text\Json\Serialization\Attributes\JsonPropertyNameAttribute.cs" />
<Compile Include="System\Text\Json\Serialization\Attributes\JsonRequiredAttribute.cs" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace System.Text.Json.Serialization
{
/// <summary>
/// When placed on a type, property, or field, indicates what <see cref="JsonNamingPolicy"/>
/// should be used to convert property names.
/// </summary>
/// <remarks>
/// When placed on a property or field, the naming policy specified by this attribute
/// takes precedence over the type-level attribute and the <see cref="JsonSerializerOptions.PropertyNamingPolicy"/>.
/// When placed on a type, the naming policy specified by this attribute takes precedence
/// over the <see cref="JsonSerializerOptions.PropertyNamingPolicy"/>.
/// The <see cref="JsonPropertyNameAttribute"/> takes precedence over this attribute.
/// </remarks>
[AttributeUsage(
AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Interface |
AttributeTargets.Property | AttributeTargets.Field,
AllowMultiple = false)]
public class JsonNamingPolicyAttribute : JsonAttribute
{
/// <summary>
/// Initializes a new instance of <see cref="JsonNamingPolicyAttribute"/>
/// with the specified known naming policy.
/// </summary>
/// <param name="namingPolicy">The known naming policy to use for name conversion.</param>
/// <exception cref="ArgumentOutOfRangeException">
/// The specified <paramref name="namingPolicy"/> is not a valid known naming policy value.
/// </exception>
public JsonNamingPolicyAttribute(JsonKnownNamingPolicy namingPolicy)
{
NamingPolicy = ResolveNamingPolicy(namingPolicy);
}

/// <summary>
/// Initializes a new instance of <see cref="JsonNamingPolicyAttribute"/>
/// with a custom naming policy.
/// </summary>
/// <param name="namingPolicy">The naming policy to use for name conversion.</param>
/// <exception cref="ArgumentNullException">
/// <paramref name="namingPolicy"/> is <see langword="null"/>.
/// </exception>
protected JsonNamingPolicyAttribute(JsonNamingPolicy namingPolicy)
{
if (namingPolicy is null)
{
ThrowHelper.ThrowArgumentNullException(nameof(namingPolicy));
}

NamingPolicy = namingPolicy;
}

/// <summary>
/// Gets the naming policy to use for name conversion.
/// </summary>
public JsonNamingPolicy NamingPolicy { get; }

internal static JsonNamingPolicy ResolveNamingPolicy(JsonKnownNamingPolicy namingPolicy)
{
return namingPolicy switch
{
JsonKnownNamingPolicy.CamelCase => JsonNamingPolicy.CamelCase,
JsonKnownNamingPolicy.SnakeCaseLower => JsonNamingPolicy.SnakeCaseLower,
JsonKnownNamingPolicy.SnakeCaseUpper => JsonNamingPolicy.SnakeCaseUpper,
JsonKnownNamingPolicy.KebabCaseLower => JsonNamingPolicy.KebabCaseLower,
JsonKnownNamingPolicy.KebabCaseUpper => JsonNamingPolicy.KebabCaseUpper,
_ => throw new ArgumentOutOfRangeException(nameof(namingPolicy)),
};
}
}
}
Loading
Loading