Skip to content

Commit

Permalink
Add support for JsonUnmappedMemberHandling (#79945)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
eiriktsarpalis and krwq authored Jan 12, 2023
1 parent 5493c13 commit 870825a
Show file tree
Hide file tree
Showing 29 changed files with 638 additions and 57 deletions.
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;
}
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

0 comments on commit 870825a

Please sign in to comment.