Skip to content

Commit 870825a

Browse files
eiriktsarpaliskrwq
andauthored
Add support for JsonUnmappedMemberHandling (#79945)
* Add support for JsonUnmappedMemberHandling. * Address feedback * Ignore global UnmappedMemberHandling setting when a JsonExtensionDataAttribute is specified. * Fix VerifyOptionsEqual method. * Update src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.cs Co-authored-by: Krzysztof Wicher <mordotymoja@gmail.com> Co-authored-by: Krzysztof Wicher <mordotymoja@gmail.com>
1 parent 5493c13 commit 870825a

29 files changed

Lines changed: 638 additions & 57 deletions
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
namespace System.Text.Json.Serialization
5+
{
6+
/// <summary>
7+
/// Determines how <see cref="JsonSerializer"/> handles JSON properties that
8+
/// cannot be mapped to a specific .NET member when deserializing object types.
9+
/// </summary>
10+
#if BUILDING_SOURCE_GENERATOR
11+
internal
12+
#else
13+
public
14+
#endif
15+
enum JsonUnmappedMemberHandling
16+
{
17+
/// <summary>
18+
/// Silently skips any unmapped properties. This is the default behavior.
19+
/// </summary>
20+
Skip = 0,
21+
22+
/// <summary>
23+
/// Throws an exception when an unmapped property is encountered.
24+
/// </summary>
25+
Disallow = 1,
26+
}
27+
}

src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ private sealed partial class Emitter
3131
private const string PropertyInfoVarName = "propertyInfo";
3232
internal const string JsonContextVarName = "jsonContext";
3333
private const string NumberHandlingPropName = "NumberHandling";
34+
private const string UnmappedMemberHandlingPropName = "UnmappedMemberHandling";
3435
private const string ObjectCreatorPropName = "ObjectCreator";
3536
private const string OptionsInstanceVariableName = "Options";
3637
private const string JsonTypeInfoReturnValueLocalVariableName = "jsonTypeInfo";
@@ -64,6 +65,7 @@ private sealed partial class Emitter
6465
private const string JsonCollectionInfoValuesTypeRef = "global::System.Text.Json.Serialization.Metadata.JsonCollectionInfoValues";
6566
private const string JsonIgnoreConditionTypeRef = "global::System.Text.Json.Serialization.JsonIgnoreCondition";
6667
private const string JsonNumberHandlingTypeRef = "global::System.Text.Json.Serialization.JsonNumberHandling";
68+
private const string JsonUnmappedMemberHandlingTypeRef = "global::System.Text.Json.Serialization.JsonUnmappedMemberHandling";
6769
private const string JsonMetadataServicesTypeRef = "global::System.Text.Json.Serialization.Metadata.JsonMetadataServices";
6870
private const string JsonObjectInfoValuesTypeRef = "global::System.Text.Json.Serialization.Metadata.JsonObjectInfoValues";
6971
private const string JsonParameterInfoValuesTypeRef = "global::System.Text.Json.Serialization.Metadata.JsonParameterInfoValues";
@@ -646,6 +648,14 @@ private string GenerateForObject(TypeGenerationSpec typeMetadata)
646648
647649
{JsonTypeInfoReturnValueLocalVariableName} = {JsonMetadataServicesTypeRef}.CreateObjectInfo<{typeMetadata.TypeRef}>({OptionsLocalVariableName}, {ObjectInfoVarName});";
648650

651+
if (typeMetadata.UnmappedMemberHandling != null)
652+
{
653+
objectInfoInitSource += $"""
654+
655+
{JsonTypeInfoReturnValueLocalVariableName}.{UnmappedMemberHandlingPropName} = {GetUnmappedMemberHandlingAsStr(typeMetadata.UnmappedMemberHandling.Value)};
656+
""";
657+
}
658+
649659
string additionalSource = @$"{propMetadataInitFuncSource}{serializeFuncSource}{ctorParamMetadataInitFuncSource}";
650660

651661
return GenerateForType(typeMetadata, objectInfoInitSource, additionalSource);
@@ -1392,6 +1402,9 @@ private static string GetNumberHandlingAsStr(JsonNumberHandling? numberHandling)
13921402
? $"({JsonNumberHandlingTypeRef}){(int)numberHandling.Value}"
13931403
: "default";
13941404

1405+
private static string GetUnmappedMemberHandlingAsStr(JsonUnmappedMemberHandling unmappedMemberHandling) =>
1406+
$"({JsonUnmappedMemberHandlingTypeRef}){(int)unmappedMemberHandling}";
1407+
13951408
private static string GetCreateValueInfoMethodRef(string typeCompilableName) => $"{CreateValueInfoMethodName}<{typeCompilableName}>";
13961409

13971410
private static string FormatBool(bool value) => value ? "true" : "false";

src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ private sealed class Parser
3838
private const string JsonIgnoreConditionFullName = "System.Text.Json.Serialization.JsonIgnoreCondition";
3939
private const string JsonIncludeAttributeFullName = "System.Text.Json.Serialization.JsonIncludeAttribute";
4040
private const string JsonNumberHandlingAttributeFullName = "System.Text.Json.Serialization.JsonNumberHandlingAttribute";
41+
private const string JsonUnmappedMemberHandlingAttributeFullName = "System.Text.Json.Serialization.JsonUnmappedMemberHandlingAttribute";
4142
private const string JsonPropertyNameAttributeFullName = "System.Text.Json.Serialization.JsonPropertyNameAttribute";
4243
private const string JsonPropertyOrderAttributeFullName = "System.Text.Json.Serialization.JsonPropertyOrderAttribute";
4344
private const string JsonRequiredAttributeFullName = "System.Text.Json.Serialization.JsonRequiredAttribute";
@@ -706,6 +707,7 @@ private TypeGenerationSpec GetOrAddTypeGenerationSpec(Type type, JsonSourceGener
706707
List<PropertyInitializerGenerationSpec>? propertyInitializerSpecList = null;
707708
CollectionType collectionType = CollectionType.NotApplicable;
708709
JsonNumberHandling? numberHandling = null;
710+
JsonUnmappedMemberHandling? unmappedMemberHandling = null;
709711
bool foundDesignTimeCustomConverter = false;
710712
string? converterInstatiationLogic = null;
711713
bool implementsIJsonOnSerialized = false;
@@ -727,6 +729,12 @@ private TypeGenerationSpec GetOrAddTypeGenerationSpec(Type type, JsonSourceGener
727729
numberHandling = (JsonNumberHandling)ctorArgs[0].Value!;
728730
continue;
729731
}
732+
else if (attributeTypeFullName == JsonUnmappedMemberHandlingAttributeFullName)
733+
{
734+
IList<CustomAttributeTypedArgument> ctorArgs = attributeData.ConstructorArguments;
735+
unmappedMemberHandling = (JsonUnmappedMemberHandling)ctorArgs[0].Value!;
736+
continue;
737+
}
730738
else if (!foundDesignTimeCustomConverter && attributeType.GetCompatibleBaseClass(JsonConverterAttributeFullName) != null)
731739
{
732740
foundDesignTimeCustomConverter = true;
@@ -1130,6 +1138,7 @@ void CacheMemberHelper(Location memberLocation)
11301138
generationMode,
11311139
classType,
11321140
numberHandling,
1141+
unmappedMemberHandling,
11331142
propGenSpecList,
11341143
paramGenSpecArray,
11351144
propertyInitializerSpecList,

src/libraries/System.Text.Json/gen/System.Text.Json.SourceGeneration.targets

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
<Compile Include="..\Common\JsonSourceGenerationMode.cs" Link="Common\System\Text\Json\Serialization\JsonSourceGenerationMode.cs" />
4343
<Compile Include="..\Common\JsonSourceGenerationOptionsAttribute.cs" Link="Common\System\Text\Json\Serialization\JsonSourceGenerationOptionsAttribute.cs" />
4444
<Compile Include="..\Common\ReflectionExtensions.cs" Link="Common\System\Text\Json\Serialization\ReflectionExtensions.cs" />
45+
<Compile Include="..\Common\JsonUnmappedMemberHandling.cs" Link="Common\System\Text\Json\Serialization\JsonUnmappedMemberHandling.cs" />
4546
<Compile Include="$(CommonPath)\Roslyn\GetBestTypeByMetadataName.cs" Link="Common\Roslyn\GetBestTypeByMetadataName.cs" />
4647
<Compile Include="ClassType.cs" />
4748
<Compile Include="CollectionType.cs" />

src/libraries/System.Text.Json/gen/TypeGenerationSpec.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ public TypeGenerationSpec(Type type)
6262
public bool CanBeNull { get; private set; }
6363

6464
public JsonNumberHandling? NumberHandling { get; private set; }
65+
public JsonUnmappedMemberHandling? UnmappedMemberHandling { get; private set; }
6566

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

@@ -129,6 +130,7 @@ public void Initialize(
129130
JsonSourceGenerationMode generationMode,
130131
ClassType classType,
131132
JsonNumberHandling? numberHandling,
133+
JsonUnmappedMemberHandling? unmappedMemberHandling,
132134
List<PropertyGenerationSpec>? propertyGenSpecList,
133135
ParameterGenerationSpec[]? ctorParamGenSpecArray,
134136
List<PropertyInitializerGenerationSpec>? propertyInitializerSpecList,
@@ -153,6 +155,7 @@ public void Initialize(
153155
CanBeNull = !IsValueType || nullableUnderlyingTypeMetadata != null;
154156
IsPolymorphic = isPolymorphic;
155157
NumberHandling = numberHandling;
158+
UnmappedMemberHandling = unmappedMemberHandling;
156159
PropertyGenSpecList = propertyGenSpecList;
157160
PropertyInitializerSpecList = propertyInitializerSpecList;
158161
CtorParamGenSpecArray = ctorParamGenSpecArray;

src/libraries/System.Text.Json/ref/System.Text.Json.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -385,6 +385,7 @@ public JsonSerializerOptions(System.Text.Json.JsonSerializerOptions options) { }
385385
public System.Text.Json.Serialization.ReferenceHandler? ReferenceHandler { get { throw null; } set { } }
386386
public System.Text.Json.Serialization.Metadata.IJsonTypeInfoResolver? TypeInfoResolver { get { throw null; } set { } }
387387
public System.Text.Json.Serialization.JsonUnknownTypeHandling UnknownTypeHandling { get { throw null; } set { } }
388+
public System.Text.Json.Serialization.JsonUnmappedMemberHandling UnmappedMemberHandling { get { throw null; } set { } }
388389
public bool WriteIndented { get { throw null; } set { } }
389390
public void AddContext<TContext>() where TContext : System.Text.Json.Serialization.JsonSerializerContext, new() { }
390391
[System.Diagnostics.CodeAnalysis.RequiresDynamicCodeAttribute("Getting a converter for a type may require reflection which depends on runtime code generation.")]
@@ -1036,6 +1037,17 @@ public enum JsonUnknownTypeHandling
10361037
JsonElement = 0,
10371038
JsonNode = 1,
10381039
}
1040+
public enum JsonUnmappedMemberHandling
1041+
{
1042+
Skip = 0,
1043+
Disallow = 1,
1044+
}
1045+
[System.AttributeUsageAttribute(System.AttributeTargets.Class | System.AttributeTargets.Interface | System.AttributeTargets.Struct, AllowMultiple=false, Inherited=false)]
1046+
public partial class JsonUnmappedMemberHandlingAttribute : System.Text.Json.Serialization.JsonAttribute
1047+
{
1048+
public JsonUnmappedMemberHandlingAttribute(System.Text.Json.Serialization.JsonUnmappedMemberHandling unmappedMemberHandling) { }
1049+
public System.Text.Json.Serialization.JsonUnmappedMemberHandling UnmappedMemberHandling { get { throw null; } }
1050+
}
10391051
public abstract partial class ReferenceHandler
10401052
{
10411053
protected ReferenceHandler() { }
@@ -1235,6 +1247,7 @@ internal JsonTypeInfo() { }
12351247
public System.Text.Json.Serialization.Metadata.JsonPolymorphismOptions? PolymorphismOptions { get { throw null; } set { } }
12361248
public System.Collections.Generic.IList<System.Text.Json.Serialization.Metadata.JsonPropertyInfo> Properties { get { throw null; } }
12371249
public System.Type Type { get { throw null; } }
1250+
public System.Text.Json.Serialization.JsonUnmappedMemberHandling? UnmappedMemberHandling { get { throw null; } set { } }
12381251
[System.Diagnostics.CodeAnalysis.RequiresDynamicCodeAttribute("JSON serialization and deserialization might require types that cannot be statically analyzed and might need runtime code generation. Use System.Text.Json source generation for native AOT applications.")]
12391252
[System.Diagnostics.CodeAnalysis.RequiresUnreferencedCodeAttribute("JSON serialization and deserialization might require types that cannot be statically analyzed and might need runtime code generation. Use System.Text.Json source generation for native AOT applications.")]
12401253
public System.Text.Json.Serialization.Metadata.JsonPropertyInfo CreateJsonPropertyInfo(System.Type propertyType, string name) { throw null; }

src/libraries/System.Text.Json/src/Resources/Strings.resx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,9 @@
360360
<data name="SerializationDuplicateTypeAttribute" xml:space="preserve">
361361
<value>The type '{0}' cannot have more than one member that has the attribute '{1}'.</value>
362362
</data>
363+
<data name="ExtensionDataConflictsWithUnmappedMemberHandling" xml:space="preserve">
364+
<value>The type '{0}' is marked 'JsonUnmappedMemberHandling.Disallow' which conflicts with extension data property '{1}'.</value>
365+
</data>
363366
<data name="SerializationNotSupportedType" xml:space="preserve">
364367
<value>The type '{0}' is not supported.</value>
365368
</data>
@@ -479,6 +482,9 @@
479482
<data name="MetadataUnexpectedProperty" xml:space="preserve">
480483
<value>The metadata property is either not supported by the type or is not the first property in the deserialized JSON object.</value>
481484
</data>
485+
<data name="UnmappedJsonProperty" xml:space="preserve">
486+
<value>The JSON property '{0}' could not be mapped to any .NET member contained in type '{1}'.</value>
487+
</data>
482488
<data name="MetadataDuplicateTypeProperty" xml:space="preserve">
483489
<value>Deserialized object contains a duplicate type discriminator metadata property.</value>
484490
</data>

src/libraries/System.Text.Json/src/System.Text.Json.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ The System.Text.Json library is built-in as part of the shared framework in .NET
3333
<Compile Include="..\Common\JsonKebabCaseUpperNamingPolicy.cs" Link="Common\System\Text\Json\Serialization\JsonKebabCaseUpperNamingPolicy.cs" />
3434
<Compile Include="..\Common\JsonKnownNamingPolicy.cs" Link="Common\System\Text\Json\Serialization\JsonKnownNamingPolicy.cs" />
3535
<Compile Include="..\Common\JsonNumberHandling.cs" Link="Common\System\Text\Json\Serialization\JsonNumberHandling.cs" />
36+
<Compile Include="..\Common\JsonUnmappedMemberHandling.cs" Link="Common\System\Text\Json\Serialization\JsonUnmappedMemberHandling.cs" />
3637
<Compile Include="..\Common\JsonSeparatorNamingPolicy.cs" Link="Common\System\Text\Json\Serialization\JsonSeparatorNamingPolicy.cs" />
3738
<Compile Include="..\Common\JsonSerializableAttribute.cs" Link="Common\System\Text\Json\Serialization\JsonSerializableAttribute.cs" />
3839
<Compile Include="..\Common\JsonSnakeCaseLowerNamingPolicy.cs" Link="Common\System\Text\Json\Serialization\JsonSnakeCaseLowerNamingPolicy.cs" />
@@ -104,6 +105,7 @@ The System.Text.Json library is built-in as part of the shared framework in .NET
104105
<Compile Include="System\Text\Json\Serialization\Attributes\JsonPropertyNameAttribute.cs" />
105106
<Compile Include="System\Text\Json\Serialization\Attributes\JsonRequiredAttribute.cs" />
106107
<Compile Include="System\Text\Json\Serialization\Attributes\JsonPropertyOrderAttribute.cs" />
108+
<Compile Include="System\Text\Json\Serialization\Attributes\JsonUnmappedMemberHandlingAttribute.cs" />
107109
<Compile Include="System\Text\Json\Serialization\Converters\CastingConverter.cs" />
108110
<Compile Include="System\Text\Json\Serialization\Converters\Collection\ImmutableDictionaryOfTKeyTValueConverterWithReflection.cs" />
109111
<Compile Include="System\Text\Json\Serialization\Converters\Collection\ImmutableEnumerableOfTConverterWithReflection.cs" />
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
namespace System.Text.Json.Serialization
5+
{
6+
/// <summary>
7+
/// When placed on a type, determines the <see cref="JsonUnmappedMemberHandling"/> configuration
8+
/// for the specific type, overriding the global <see cref="JsonSerializerOptions.UnmappedMemberHandling"/> setting.
9+
/// </summary>
10+
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface | AttributeTargets.Struct,
11+
AllowMultiple = false, Inherited = false)]
12+
public class JsonUnmappedMemberHandlingAttribute : JsonAttribute
13+
{
14+
/// <summary>
15+
/// Initializes a new instance of <see cref="JsonUnmappedMemberHandlingAttribute"/>.
16+
/// </summary>
17+
public JsonUnmappedMemberHandlingAttribute(JsonUnmappedMemberHandling unmappedMemberHandling)
18+
{
19+
UnmappedMemberHandling = unmappedMemberHandling;
20+
}
21+
22+
/// <summary>
23+
/// Specifies the unmapped member handling setting for the attribute.
24+
/// </summary>
25+
public JsonUnmappedMemberHandling UnmappedMemberHandling { get; }
26+
}
27+
}

src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterOfT.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -638,7 +638,7 @@ internal sealed override void WriteAsPropertyNameCoreAsObject(Utf8JsonWriter wri
638638

639639
// For consistency do not return any default converters for options instances linked to a
640640
// JsonSerializerContext, even if the default converters might have been rooted.
641-
if (!IsInternalConverter && options.SerializerContext is null)
641+
if (!IsInternalConverter && options.TypeInfoResolver is not JsonSerializerContext)
642642
{
643643
result = _fallbackConverterForPropertyNameSerialization;
644644

0 commit comments

Comments
 (0)