Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Populating JSON properties support #83669

Merged
merged 9 commits into from
Apr 25, 2023
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 @@ -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 { } }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does the Preferred prefix refer to? Seems like this property won't win over the setting for properties and objects.

Copy link
Member Author

@krwq krwq Mar 28, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

correct:

  • preferred - means that if property's converter is capable of supporting populate and property didn't specify its settings then we will populate according to this setting
  • no preferred in name - we will use that or throw

now we will use setting in following order:

  • property: no prefer here, we use it when it's non-null. If set incorrectly we throw
  • type: prefer here: type properties will use this when they don't explicitly specify and their converter supports it
  • options: preferred in name, if not other preference defined and property didn't define we will use this setting. Note it's non-nullable and Replace by default. We also don't enable it when converter doesn't support it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

now we will use setting in following order:

Seems like the very same precedence considerations as with NumberHandling above, and that the behavior can be conveyed to users without the "Preferred" prefix

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for number handling it's implied you have number so it's always supported as long as it's a number. Here it's a bit more fuzzy and you might not get what you want (i.e. immutable collections users might think it will append or it won't populate for structs if it doesn't have a setter). We've discussed that during API review and prefix was added during it

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)]
eiriktsarpalis marked this conversation as resolved.
Show resolved Hide resolved
public sealed partial class JsonObjectCreationHandlingAttribute : System.Text.Json.Serialization.JsonAttribute
{
public JsonObjectCreationHandlingAttribute(System.Text.Json.Serialization.JsonObjectCreationHandling handling) { }
eiriktsarpalis marked this conversation as resolved.
Show resolved Hide resolved
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