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

Add support for JsonUnmappedMemberHandling #79945

Merged
Merged
Show file tree
Hide file tree
Changes from 3 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
@@ -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 <see cref="JsonSerializer"/> handles JSON properties that
/// cannot be mapped to a specific .NET member when deserializing object types.
/// </summary>
#if BUILDING_SOURCE_GENERATOR
internal
#else
public
#endif
enum JsonUnmappedMemberHandling
{
/// <summary>
/// Silently skips any unmapped properties. This is the default behavior.
/// </summary>
Skip = 0,

/// <summary>
/// Throws an exception when an unmapped property is encountered.
/// </summary>
Disallow = 1,
}
}
13 changes: 13 additions & 0 deletions src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ private sealed partial class Emitter
private const string PropertyInfoVarName = "propertyInfo";
internal const string JsonContextVarName = "jsonContext";
private const string NumberHandlingPropName = "NumberHandling";
private const string UnmappedMemberHandlingPropName = "UnmappedMemberHandling";
private const string ObjectCreatorPropName = "ObjectCreator";
private const string OptionsInstanceVariableName = "Options";
private const string JsonTypeInfoReturnValueLocalVariableName = "jsonTypeInfo";
Expand Down Expand Up @@ -64,6 +65,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 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";
private const string JsonParameterInfoValuesTypeRef = "global::System.Text.Json.Serialization.Metadata.JsonParameterInfoValues";
Expand Down Expand Up @@ -646,6 +648,14 @@ private string GenerateForObject(TypeGenerationSpec typeMetadata)

{JsonTypeInfoReturnValueLocalVariableName} = {JsonMetadataServicesTypeRef}.CreateObjectInfo<{typeMetadata.TypeRef}>({OptionsLocalVariableName}, {ObjectInfoVarName});";

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

{JsonTypeInfoReturnValueLocalVariableName}.{UnmappedMemberHandlingPropName} = {GetUnmappedMemberHandlingAsStr(typeMetadata.UnmappedMemberHandling.Value)};
""";
}

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

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

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

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

private static string FormatBool(bool value) => value ? "true" : "false";
Expand Down
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 JsonUnmappedMemberHandlingAttributeFullName = "System.Text.Json.Serialization.JsonUnmappedMemberHandlingAttribute";
private const string JsonPropertyNameAttributeFullName = "System.Text.Json.Serialization.JsonPropertyNameAttribute";
private const string JsonPropertyOrderAttributeFullName = "System.Text.Json.Serialization.JsonPropertyOrderAttribute";
private const string JsonRequiredAttributeFullName = "System.Text.Json.Serialization.JsonRequiredAttribute";
Expand Down Expand Up @@ -706,6 +707,7 @@ private TypeGenerationSpec GetOrAddTypeGenerationSpec(Type type, JsonSourceGener
List<PropertyInitializerGenerationSpec>? propertyInitializerSpecList = null;
CollectionType collectionType = CollectionType.NotApplicable;
JsonNumberHandling? numberHandling = null;
JsonUnmappedMemberHandling? unmappedMemberHandling = null;
bool foundDesignTimeCustomConverter = false;
string? converterInstatiationLogic = null;
bool implementsIJsonOnSerialized = false;
Expand All @@ -727,6 +729,12 @@ private TypeGenerationSpec GetOrAddTypeGenerationSpec(Type type, JsonSourceGener
numberHandling = (JsonNumberHandling)ctorArgs[0].Value!;
continue;
}
else if (attributeTypeFullName == JsonUnmappedMemberHandlingAttributeFullName)
{
IList<CustomAttributeTypedArgument> ctorArgs = attributeData.ConstructorArguments;
unmappedMemberHandling = (JsonUnmappedMemberHandling)ctorArgs[0].Value!;
continue;
eiriktsarpalis marked this conversation as resolved.
Show resolved Hide resolved
}
else if (!foundDesignTimeCustomConverter && attributeType.GetCompatibleBaseClass(JsonConverterAttributeFullName) != null)
{
foundDesignTimeCustomConverter = true;
Expand Down Expand Up @@ -1130,6 +1138,7 @@ void CacheMemberHelper(Location memberLocation)
generationMode,
classType,
numberHandling,
unmappedMemberHandling,
propGenSpecList,
paramGenSpecArray,
propertyInitializerSpecList,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
<Compile Include="..\Common\JsonSourceGenerationMode.cs" Link="Common\System\Text\Json\Serialization\JsonSourceGenerationMode.cs" />
<Compile Include="..\Common\JsonSourceGenerationOptionsAttribute.cs" Link="Common\System\Text\Json\Serialization\JsonSourceGenerationOptionsAttribute.cs" />
<Compile Include="..\Common\ReflectionExtensions.cs" Link="Common\System\Text\Json\Serialization\ReflectionExtensions.cs" />
<Compile Include="..\Common\JsonUnmappedMemberHandling.cs" Link="Common\System\Text\Json\Serialization\JsonUnmappedMemberHandling.cs" />
<Compile Include="$(CommonPath)\Roslyn\GetBestTypeByMetadataName.cs" Link="Common\Roslyn\GetBestTypeByMetadataName.cs" />
<Compile Include="ClassType.cs" />
<Compile Include="CollectionType.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 @@ -62,6 +62,7 @@ public TypeGenerationSpec(Type type)
public bool CanBeNull { get; private set; }

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

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

Expand Down Expand Up @@ -129,6 +130,7 @@ public void Initialize(
JsonSourceGenerationMode generationMode,
ClassType classType,
JsonNumberHandling? numberHandling,
JsonUnmappedMemberHandling? unmappedMemberHandling,
List<PropertyGenerationSpec>? propertyGenSpecList,
ParameterGenerationSpec[]? ctorParamGenSpecArray,
List<PropertyInitializerGenerationSpec>? propertyInitializerSpecList,
Expand All @@ -153,6 +155,7 @@ public void Initialize(
CanBeNull = !IsValueType || nullableUnderlyingTypeMetadata != null;
IsPolymorphic = isPolymorphic;
NumberHandling = numberHandling;
UnmappedMemberHandling = unmappedMemberHandling;
PropertyGenSpecList = propertyGenSpecList;
PropertyInitializerSpecList = propertyInitializerSpecList;
CtorParamGenSpecArray = ctorParamGenSpecArray;
Expand Down
13 changes: 13 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 @@ -385,6 +385,7 @@ public JsonSerializerOptions(System.Text.Json.JsonSerializerOptions options) { }
public System.Text.Json.Serialization.ReferenceHandler? ReferenceHandler { get { throw null; } set { } }
public System.Text.Json.Serialization.Metadata.IJsonTypeInfoResolver? TypeInfoResolver { get { throw null; } set { } }
public System.Text.Json.Serialization.JsonUnknownTypeHandling UnknownTypeHandling { get { throw null; } set { } }
public System.Text.Json.Serialization.JsonUnmappedMemberHandling UnmappedMemberHandling { get { throw null; } set { } }
public bool WriteIndented { get { throw null; } set { } }
public void AddContext<TContext>() where TContext : System.Text.Json.Serialization.JsonSerializerContext, new() { }
[System.Diagnostics.CodeAnalysis.RequiresDynamicCodeAttribute("Getting a converter for a type may require reflection which depends on runtime code generation.")]
Expand Down Expand Up @@ -1036,6 +1037,17 @@ public enum JsonUnknownTypeHandling
JsonElement = 0,
JsonNode = 1,
}
public enum JsonUnmappedMemberHandling
{
Skip = 0,
Disallow = 1,
}
[System.AttributeUsageAttribute(System.AttributeTargets.Class | System.AttributeTargets.Interface | System.AttributeTargets.Struct, AllowMultiple=false, Inherited=false)]
public partial class JsonUnmappedMemberHandlingAttribute : System.Text.Json.Serialization.JsonAttribute
{
public JsonUnmappedMemberHandlingAttribute(System.Text.Json.Serialization.JsonUnmappedMemberHandling unmappedMemberHandling) { }
public System.Text.Json.Serialization.JsonUnmappedMemberHandling UnmappedMemberHandling { get { throw null; } }
}
public abstract partial class ReferenceHandler
{
protected ReferenceHandler() { }
Expand Down Expand Up @@ -1235,6 +1247,7 @@ internal JsonTypeInfo() { }
public System.Text.Json.Serialization.Metadata.JsonPolymorphismOptions? PolymorphismOptions { get { throw null; } set { } }
public System.Collections.Generic.IList<System.Text.Json.Serialization.Metadata.JsonPropertyInfo> Properties { get { throw null; } }
public System.Type Type { get { throw null; } }
public System.Text.Json.Serialization.JsonUnmappedMemberHandling? UnmappedMemberHandling { get { throw null; } set { } }
[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.")]
[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.")]
public System.Text.Json.Serialization.Metadata.JsonPropertyInfo CreateJsonPropertyInfo(System.Type propertyType, string name) { throw null; }
Expand Down
6 changes: 6 additions & 0 deletions src/libraries/System.Text.Json/src/Resources/Strings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,9 @@
<data name="SerializationDuplicateTypeAttribute" xml:space="preserve">
<value>The type '{0}' cannot have more than one member that has the attribute '{1}'.</value>
</data>
<data name="ExtensionDataConflictsWithUnmappedMemberHandling" xml:space="preserve">
<value>The type '{0}' is marked 'JsonUnmappedMemberHandling.Disallow' which conflicts with extension data property '{1}'.</value>
</data>
<data name="SerializationNotSupportedType" xml:space="preserve">
<value>The type '{0}' is not supported.</value>
</data>
Expand Down Expand Up @@ -479,6 +482,9 @@
<data name="MetadataUnexpectedProperty" xml:space="preserve">
<value>The metadata property is either not supported by the type or is not the first property in the deserialized JSON object.</value>
</data>
<data name="UnmappedJsonProperty" xml:space="preserve">
<value>The JSON property '{0}' could not be mapped to any .NET member contained in type '{1}'.</value>
</data>
<data name="MetadataDuplicateTypeProperty" xml:space="preserve">
<value>Deserialized object contains a duplicate type discriminator metadata property.</value>
</data>
Expand Down
2 changes: 2 additions & 0 deletions src/libraries/System.Text.Json/src/System.Text.Json.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ The System.Text.Json library is built-in as part of the shared framework in .NET
<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\JsonUnmappedMemberHandling.cs" Link="Common\System\Text\Json\Serialization\JsonUnmappedMemberHandling.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 Expand Up @@ -104,6 +105,7 @@ The System.Text.Json library is built-in as part of the shared framework in .NET
<Compile Include="System\Text\Json\Serialization\Attributes\JsonPropertyNameAttribute.cs" />
<Compile Include="System\Text\Json\Serialization\Attributes\JsonRequiredAttribute.cs" />
<Compile Include="System\Text\Json\Serialization\Attributes\JsonPropertyOrderAttribute.cs" />
<Compile Include="System\Text\Json\Serialization\Attributes\JsonUnmappedMemberHandlingAttribute.cs" />
<Compile Include="System\Text\Json\Serialization\Converters\CastingConverter.cs" />
<Compile Include="System\Text\Json\Serialization\Converters\Collection\ImmutableDictionaryOfTKeyTValueConverterWithReflection.cs" />
<Compile Include="System\Text\Json\Serialization\Converters\Collection\ImmutableEnumerableOfTConverterWithReflection.cs" />
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>
/// When placed on a type, determines the <see cref="JsonUnmappedMemberHandling"/> configuration
/// for the specific type, overriding the global <see cref="JsonSerializerOptions.UnmappedMemberHandling"/> setting.
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface | AttributeTargets.Struct,
AllowMultiple = false, Inherited = false)]
public class JsonUnmappedMemberHandlingAttribute : JsonAttribute
{
/// <summary>
/// Initializes a new instance of <see cref="JsonUnmappedMemberHandlingAttribute"/>.
/// </summary>
public JsonUnmappedMemberHandlingAttribute(JsonUnmappedMemberHandling unmappedMemberHandling)
{
UnmappedMemberHandling = unmappedMemberHandling;
}

/// <summary>
/// Specifies the unmapped member handling setting for the attribute.
/// </summary>
public JsonUnmappedMemberHandling UnmappedMemberHandling { get; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -638,7 +638,7 @@ internal sealed override void WriteAsPropertyNameCoreAsObject(Utf8JsonWriter wri

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,5 +78,8 @@ internal static bool IsValidNumberHandlingValue(JsonNumberHandling handling) =>
JsonNumberHandling.AllowReadingFromString |
JsonNumberHandling.WriteAsString |
JsonNumberHandling.AllowNamedFloatingPointLiterals));

internal static bool IsValidUnmappedMemberHandlingValue(JsonUnmappedMemberHandling handling) =>
handling is JsonUnmappedMemberHandling.Skip or JsonUnmappedMemberHandling.Disallow;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,17 +24,18 @@ internal static JsonPropertyInfo LookupProperty(
out bool useExtensionProperty,
bool createExtensionProperty = true)
{
JsonTypeInfo jsonTypeInfo = state.Current.JsonTypeInfo;
#if DEBUG
if (state.Current.JsonTypeInfo.Kind != JsonTypeInfoKind.Object)
if (jsonTypeInfo.Kind != JsonTypeInfoKind.Object)
{
string objTypeName = obj?.GetType().FullName ?? "<null>";
Debug.Fail($"obj.GetType() => {objTypeName}; {state.Current.JsonTypeInfo.GetPropertyDebugInfo(unescapedPropertyName)}");
Debug.Fail($"obj.GetType() => {objTypeName}; {jsonTypeInfo.GetPropertyDebugInfo(unescapedPropertyName)}");
}
#endif

useExtensionProperty = false;

JsonPropertyInfo jsonPropertyInfo = state.Current.JsonTypeInfo.GetProperty(
JsonPropertyInfo jsonPropertyInfo = jsonTypeInfo.GetProperty(
unescapedPropertyName,
ref state.Current,
out byte[] utf8PropertyName);
Expand All @@ -45,11 +46,18 @@ internal static JsonPropertyInfo LookupProperty(
// For case insensitive and missing property support of JsonPath, remember the value on the temporary stack.
state.Current.JsonPropertyName = utf8PropertyName;

// Determine if we should use the extension property.
// Handle missing properties
if (jsonPropertyInfo == JsonPropertyInfo.s_missingProperty)
{
JsonPropertyInfo? dataExtProperty = state.Current.JsonTypeInfo.ExtensionDataProperty;
if (dataExtProperty != null && dataExtProperty.HasGetter && dataExtProperty.HasSetter)
if (jsonTypeInfo.EffectiveUnmappedMemberHandling is JsonUnmappedMemberHandling.Disallow)
{
Debug.Assert(jsonTypeInfo.ExtensionDataProperty is null, "jsonTypeInfo.Configure() should have caught conflicting configuration.");
string stringPropertyName = JsonHelpers.Utf8GetString(unescapedPropertyName);
ThrowHelper.ThrowJsonException_UnmappedJsonProperty(jsonTypeInfo.Type, stringPropertyName);
}

// Determine if we should use the extension property.
if (jsonTypeInfo.ExtensionDataProperty is JsonPropertyInfo { HasGetter: true, HasSetter: true } dataExtProperty)
{
state.Current.JsonPropertyNameAsString = JsonHelpers.Utf8GetString(unescapedPropertyName);

Expand Down
Loading