Skip to content

Commit

Permalink
Populating JSON properties support (#83669)
Browse files Browse the repository at this point in the history
* Populating JSON properties support

* apply review feedback

* Attempt to fix build by reducing stack frame

* Fix docs

* Remove unnecessary properties from ReadStackFrame.

* Move source gen tests to the source gen test project.

* Improve test helper method name.

* Add argument validation to JsonPropertyInfo.ObjectCreationHandling & extend unit testing for the new metadata APIs.

* Add test coverage for invalid attribute annotations.

---------

Co-authored-by: Eirik Tsarpalis <eirik.tsarpalis@gmail.com>
  • Loading branch information
krwq and eiriktsarpalis committed Apr 25, 2023
1 parent 32ecd85 commit ed9fbe3
Show file tree
Hide file tree
Showing 63 changed files with 7,523 additions and 107 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ namespace System.Text.Json.Serialization
#else
public
#endif
enum JsonNumberHandling
enum JsonNumberHandling
{
/// <summary>
/// Numbers will only be read from <see cref="JsonTokenType.Number"/> tokens and will only be written as JSON numbers (without quotes).
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// 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>
/// Determines how deserialization will handle object creation for fields or properties.
/// </summary>
#if BUILDING_SOURCE_GENERATOR
internal
#else
public
#endif
enum JsonObjectCreationHandling
{
/// <summary>
/// A new instance will always be created when deserializing a field or property.
/// </summary>
Replace = 0,

/// <summary>
/// Attempt to populate any instances already found on a deserialized field or property.
/// </summary>
Populate = 1,
}
}
34 changes: 30 additions & 4 deletions src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ private sealed partial class Emitter
internal const string JsonContextVarName = "jsonContext";
private const string NumberHandlingPropName = "NumberHandling";
private const string UnmappedMemberHandlingPropName = "UnmappedMemberHandling";
private const string PreferredPropertyObjectCreationHandlingPropName = "PreferredPropertyObjectCreationHandling";
private const string ObjectCreatorPropName = "ObjectCreator";
private const string OptionsInstanceVariableName = "Options";
private const string JsonTypeInfoReturnValueLocalVariableName = "jsonTypeInfo";
Expand Down Expand Up @@ -65,6 +66,7 @@ private sealed partial class Emitter
private const string JsonCollectionInfoValuesTypeRef = "global::System.Text.Json.Serialization.Metadata.JsonCollectionInfoValues";
private const string JsonIgnoreConditionTypeRef = "global::System.Text.Json.Serialization.JsonIgnoreCondition";
private const string JsonNumberHandlingTypeRef = "global::System.Text.Json.Serialization.JsonNumberHandling";
private const string JsonObjectCreationHandlingTypeRef = "global::System.Text.Json.Serialization.JsonObjectCreationHandling";
private const string JsonUnmappedMemberHandlingTypeRef = "global::System.Text.Json.Serialization.JsonUnmappedMemberHandling";
private const string JsonMetadataServicesTypeRef = "global::System.Text.Json.Serialization.Metadata.JsonMetadataServices";
private const string JsonObjectInfoValuesTypeRef = "global::System.Text.Json.Serialization.Metadata.JsonObjectInfoValues";
Expand Down Expand Up @@ -657,6 +659,14 @@ private string GenerateForObject(TypeGenerationSpec typeMetadata)
""";
}

if (typeMetadata.PreferredPropertyObjectCreationHandling != null)
{
objectInfoInitSource += $"""

{JsonTypeInfoReturnValueLocalVariableName}.{PreferredPropertyObjectCreationHandlingPropName} = {GetObjectCreationHandlingAsStr(typeMetadata.PreferredPropertyObjectCreationHandling.Value)};
""";
}

string additionalSource = @$"{propMetadataInitFuncSource}{serializeFuncSource}{ctorParamMetadataInitFuncSource}";

return GenerateForType(typeMetadata, objectInfoInitSource, additionalSource);
Expand Down Expand Up @@ -762,6 +772,12 @@ private static string GeneratePropMetadataInitFunc(TypeGenerationSpec typeGenera
{propertyInfoVarName}.IsRequired = true;");
}

if (memberMetadata.ObjectCreationHandling != null)
{
sb.Append($@"
{propertyInfoVarName}.ObjectCreationHandling = {GetObjectCreationHandlingAsStr(memberMetadata.ObjectCreationHandling.Value)};");
}

sb.Append($@"
{PropVarName}[{i}] = {propertyInfoVarName};
");
Expand Down Expand Up @@ -1386,12 +1402,22 @@ private static string IndentSource(string source, int numIndentations)
}

private static string GetNumberHandlingAsStr(JsonNumberHandling? numberHandling) =>
numberHandling.HasValue
? $"({JsonNumberHandlingTypeRef}){(int)numberHandling.Value}"
: "default";
numberHandling switch
{
null => "default",
>= 0 => $"({JsonNumberHandlingTypeRef}){(int)numberHandling.Value}",
< 0 => $"({JsonNumberHandlingTypeRef})({(int)numberHandling.Value})"
};

private static string GetObjectCreationHandlingAsStr(JsonObjectCreationHandling creationHandling) =>
creationHandling >= 0
? $"({JsonObjectCreationHandlingTypeRef}){(int)creationHandling}"
: $"({JsonObjectCreationHandlingTypeRef})({(int)creationHandling})";

private static string GetUnmappedMemberHandlingAsStr(JsonUnmappedMemberHandling unmappedMemberHandling) =>
$"({JsonUnmappedMemberHandlingTypeRef}){(int)unmappedMemberHandling}";
unmappedMemberHandling >= 0
? $"({JsonUnmappedMemberHandlingTypeRef}){(int)unmappedMemberHandling}"
: $"({JsonUnmappedMemberHandlingTypeRef})({(int)unmappedMemberHandling})";

private static string GetCreateValueInfoMethodRef(string typeCompilableName) => $"{CreateValueInfoMethodName}<{typeCompilableName}>";

Expand Down
19 changes: 19 additions & 0 deletions src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ private sealed class Parser
private const string JsonIgnoreConditionFullName = "System.Text.Json.Serialization.JsonIgnoreCondition";
private const string JsonIncludeAttributeFullName = "System.Text.Json.Serialization.JsonIncludeAttribute";
private const string JsonNumberHandlingAttributeFullName = "System.Text.Json.Serialization.JsonNumberHandlingAttribute";
private const string JsonObjectCreationHandlingAttributeFullName = "System.Text.Json.Serialization.JsonObjectCreationHandlingAttribute";
private const string JsonUnmappedMemberHandlingAttributeFullName = "System.Text.Json.Serialization.JsonUnmappedMemberHandlingAttribute";
private const string JsonPropertyNameAttributeFullName = "System.Text.Json.Serialization.JsonPropertyNameAttribute";
private const string JsonPropertyOrderAttributeFullName = "System.Text.Json.Serialization.JsonPropertyOrderAttribute";
Expand Down Expand Up @@ -708,6 +709,7 @@ private TypeGenerationSpec GetOrAddTypeGenerationSpec(Type type, JsonSourceGener
CollectionType collectionType = CollectionType.NotApplicable;
JsonNumberHandling? numberHandling = null;
JsonUnmappedMemberHandling? unmappedMemberHandling = null;
JsonObjectCreationHandling? preferredPropertyObjectCreationHandling = null;
bool foundDesignTimeCustomConverter = false;
string? converterInstatiationLogic = null;
bool implementsIJsonOnSerialized = false;
Expand Down Expand Up @@ -735,6 +737,12 @@ private TypeGenerationSpec GetOrAddTypeGenerationSpec(Type type, JsonSourceGener
unmappedMemberHandling = (JsonUnmappedMemberHandling)ctorArgs[0].Value!;
continue;
}
else if (attributeTypeFullName == JsonObjectCreationHandlingAttributeFullName)
{
IList<CustomAttributeTypedArgument> ctorArgs = attributeData.ConstructorArguments;
preferredPropertyObjectCreationHandling = (JsonObjectCreationHandling)ctorArgs[0].Value!;
continue;
}
else if (!foundDesignTimeCustomConverter && attributeType.GetCompatibleBaseClass(JsonConverterAttributeFullName) != null)
{
foundDesignTimeCustomConverter = true;
Expand Down Expand Up @@ -1139,6 +1147,7 @@ void CacheMemberHelper(Location memberLocation)
classType,
numberHandling,
unmappedMemberHandling,
preferredPropertyObjectCreationHandling,
propGenSpecList,
paramGenSpecArray,
propertyInitializerSpecList,
Expand Down Expand Up @@ -1238,6 +1247,7 @@ private PropertyGenerationSpec GetPropertyGenerationSpec(
out string? jsonPropertyName,
out JsonIgnoreCondition? ignoreCondition,
out JsonNumberHandling? numberHandling,
out JsonObjectCreationHandling? objectCreationHandling,
out string? converterInstantiationLogic,
out int order,
out bool hasFactoryConverter,
Expand Down Expand Up @@ -1287,6 +1297,7 @@ private PropertyGenerationSpec GetPropertyGenerationSpec(
SetterIsVirtual = setterIsVirtual,
DefaultIgnoreCondition = ignoreCondition,
NumberHandling = numberHandling,
ObjectCreationHandling = objectCreationHandling,
Order = order,
HasJsonInclude = hasJsonInclude,
IsExtensionData = isExtensionData,
Expand Down Expand Up @@ -1320,6 +1331,7 @@ private void ProcessMemberCustomAttributes(
out string? jsonPropertyName,
out JsonIgnoreCondition? ignoreCondition,
out JsonNumberHandling? numberHandling,
out JsonObjectCreationHandling? objectCreationHandling,
out string? converterInstantiationLogic,
out int order,
out bool hasFactoryConverter,
Expand All @@ -1330,6 +1342,7 @@ private void ProcessMemberCustomAttributes(
jsonPropertyName = null;
ignoreCondition = default;
numberHandling = default;
objectCreationHandling = default;
converterInstantiationLogic = null;
order = 0;
isExtensionData = false;
Expand Down Expand Up @@ -1382,6 +1395,12 @@ private void ProcessMemberCustomAttributes(
numberHandling = (JsonNumberHandling)ctorArgs[0].Value!;
}
break;
case JsonObjectCreationHandlingAttributeFullName:
{
IList<CustomAttributeTypedArgument> ctorArgs = attributeData.ConstructorArguments;
objectCreationHandling = (JsonObjectCreationHandling)ctorArgs[0].Value!;
}
break;
case JsonPropertyNameAttributeFullName:
{
IList<CustomAttributeTypedArgument> ctorArgs = attributeData.ConstructorArguments;
Expand Down
5 changes: 5 additions & 0 deletions src/libraries/System.Text.Json/gen/PropertyGenerationSpec.cs
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,11 @@ internal sealed class PropertyGenerationSpec
/// </summary>
public JsonNumberHandling? NumberHandling { get; init; }

/// <summary>
/// The <see cref="JsonObjectCreationHandling"/> for the property.
/// </summary>
public JsonObjectCreationHandling? ObjectCreationHandling { get; init; }

/// <summary>
/// The serialization order of the property.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
<Compile Include="..\Common\JsonKebabCaseUpperNamingPolicy.cs" Link="Common\System\Text\Json\Serialization\JsonKebabCaseUpperNamingPolicy.cs" />
<Compile Include="..\Common\JsonKnownNamingPolicy.cs" Link="Common\System\Text\Json\Serialization\JsonKnownNamingPolicy.cs" />
<Compile Include="..\Common\JsonNumberHandling.cs" Link="Common\System\Text\Json\Serialization\JsonNumberHandling.cs" />
<Compile Include="..\Common\JsonObjectCreationHandling.cs" Link="Common\System\Text\Json\Serialization\JsonObjectCreationHandling.cs" />
<Compile Include="..\Common\JsonSeparatorNamingPolicy.cs" Link="Common\System\Text\Json\Serialization\JsonSeparatorNamingPolicy.cs" />
<Compile Include="..\Common\JsonSerializableAttribute.cs" Link="Common\System\Text\Json\Serialization\JsonSerializableAttribute.cs" />
<Compile Include="..\Common\JsonSnakeCaseLowerNamingPolicy.cs" Link="Common\System\Text\Json\Serialization\JsonSnakeCaseLowerNamingPolicy.cs" />
Expand Down
3 changes: 3 additions & 0 deletions src/libraries/System.Text.Json/gen/TypeGenerationSpec.cs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ public TypeGenerationSpec(Type type)

public JsonNumberHandling? NumberHandling { get; private set; }
public JsonUnmappedMemberHandling? UnmappedMemberHandling { get; private set; }
public JsonObjectCreationHandling? PreferredPropertyObjectCreationHandling { get; private set; }

public List<PropertyGenerationSpec>? PropertyGenSpecList { get; private set; }

Expand Down Expand Up @@ -131,6 +132,7 @@ public void Initialize(
ClassType classType,
JsonNumberHandling? numberHandling,
JsonUnmappedMemberHandling? unmappedMemberHandling,
JsonObjectCreationHandling? preferredPropertyObjectCreationHandling,
List<PropertyGenerationSpec>? propertyGenSpecList,
ParameterGenerationSpec[]? ctorParamGenSpecArray,
List<PropertyInitializerGenerationSpec>? propertyInitializerSpecList,
Expand All @@ -156,6 +158,7 @@ public void Initialize(
IsPolymorphic = isPolymorphic;
NumberHandling = numberHandling;
UnmappedMemberHandling = unmappedMemberHandling;
PreferredPropertyObjectCreationHandling = preferredPropertyObjectCreationHandling;
PropertyGenSpecList = propertyGenSpecList;
PropertyInitializerSpecList = propertyInitializerSpecList;
CtorParamGenSpecArray = ctorParamGenSpecArray;
Expand Down
14 changes: 14 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 @@ -384,6 +384,7 @@ public JsonSerializerOptions(System.Text.Json.JsonSerializerOptions options) { }
public bool IsReadOnly { get { throw null; } }
public int MaxDepth { get { throw null; } set { } }
public System.Text.Json.Serialization.JsonNumberHandling NumberHandling { get { throw null; } set { } }
public System.Text.Json.Serialization.JsonObjectCreationHandling PreferredObjectCreationHandling { get { throw null; } set { } }
public bool PropertyNameCaseInsensitive { get { throw null; } set { } }
public System.Text.Json.JsonNamingPolicy? PropertyNamingPolicy { get { throw null; } set { } }
public System.Text.Json.JsonCommentHandling ReadCommentHandling { get { throw null; } set { } }
Expand Down Expand Up @@ -969,6 +970,17 @@ public sealed partial class JsonNumberHandlingAttribute : System.Text.Json.Seria
public JsonNumberHandlingAttribute(System.Text.Json.Serialization.JsonNumberHandling handling) { }
public System.Text.Json.Serialization.JsonNumberHandling Handling { get { throw null; } }
}
public enum JsonObjectCreationHandling
{
Replace = 0,
Populate = 1,
}
[System.AttributeUsageAttribute(System.AttributeTargets.Field | System.AttributeTargets.Property | AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Interface, AllowMultiple = false)]
public sealed partial class JsonObjectCreationHandlingAttribute : System.Text.Json.Serialization.JsonAttribute
{
public JsonObjectCreationHandlingAttribute(System.Text.Json.Serialization.JsonObjectCreationHandling handling) { }
public System.Text.Json.Serialization.JsonObjectCreationHandling Handling { get { throw null; } }
}
[System.AttributeUsageAttribute(System.AttributeTargets.Class | System.AttributeTargets.Interface, AllowMultiple=false, Inherited=false)]
public sealed partial class JsonPolymorphicAttribute : System.Text.Json.Serialization.JsonAttribute
{
Expand Down Expand Up @@ -1210,6 +1222,7 @@ public abstract partial class JsonPropertyInfo
{
internal JsonPropertyInfo() { }
public System.Reflection.ICustomAttributeProvider? AttributeProvider { get { throw null; } set { } }
public System.Text.Json.Serialization.JsonObjectCreationHandling? ObjectCreationHandling { get { throw null; } set { } }
public System.Text.Json.Serialization.JsonConverter? CustomConverter { get { throw null; } set { } }
public System.Func<object, object?>? Get { get { throw null; } set { } }
public bool IsExtensionData { get { throw null; } set { } }
Expand Down Expand Up @@ -1249,6 +1262,7 @@ internal JsonTypeInfo() { }
public bool IsReadOnly { get { throw null; } }
public System.Text.Json.Serialization.Metadata.JsonTypeInfoKind Kind { get { throw null; } }
public System.Text.Json.Serialization.JsonNumberHandling? NumberHandling { get { throw null; } set { } }
public System.Text.Json.Serialization.JsonObjectCreationHandling? PreferredPropertyObjectCreationHandling { get { throw null; } set { } }
public System.Action<object>? OnDeserialized { get { throw null; } set { } }
public System.Action<object>? OnDeserializing { get { throw null; } set { } }
public System.Action<object>? OnSerialized { get { throw null; } set { } }
Expand Down
Loading

0 comments on commit ed9fbe3

Please sign in to comment.