diff --git a/src/libraries/System.Text.Json/src/Resources/Strings.resx b/src/libraries/System.Text.Json/src/Resources/Strings.resx index f4f1cc96e76d51..01d0af8f556137 100644 --- a/src/libraries/System.Text.Json/src/Resources/Strings.resx +++ b/src/libraries/System.Text.Json/src/Resources/Strings.resx @@ -659,4 +659,13 @@ Parameter already associated with a different JsonTypeInfo instance. + + JsonPropertyInfo '{0}' defined in type '{1}' is marked required but does not specify a setter. + + + JsonPropertyInfo '{0}' defined in type '{1}' is marked both as required and as an extension data property. This combination is not supported. + + + JSON deserialization for type '{0}' was missing required properties, including the following: {1} + diff --git a/src/libraries/System.Text.Json/src/System/ReflectionExtensions.cs b/src/libraries/System.Text.Json/src/System/ReflectionExtensions.cs index 9a5d6f4bb610d9..e15e8993889e43 100644 --- a/src/libraries/System.Text.Json/src/System/ReflectionExtensions.cs +++ b/src/libraries/System.Text.Json/src/System/ReflectionExtensions.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics.CodeAnalysis; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.ExceptionServices; @@ -42,6 +43,32 @@ public static bool IsInSubtypeRelationshipWith(this Type type, Type other) => private static bool HasJsonConstructorAttribute(ConstructorInfo constructorInfo) => constructorInfo.GetCustomAttribute() != null; + public static bool HasRequiredMemberAttribute(this ICustomAttributeProvider memberInfo) + { + // For compiler related attributes we should only look at full type name rather than trying to do something different for version when attribute was introduced. + // I.e. library is targetting netstandard2.0 with polyfilled attributes and is being consumed by app targetting net7.0. + return memberInfo.HasCustomAttributeWithName("System.Runtime.CompilerServices.RequiredMemberAttribute", inherit: true); + } + + public static bool HasSetsRequiredMembersAttribute(this ICustomAttributeProvider memberInfo) + { + // See comment for HasRequiredMemberAttribute for why we need to always only look at full name + return memberInfo.HasCustomAttributeWithName("System.Diagnostics.CodeAnalysis.SetsRequiredMembersAttribute", inherit: true); + } + + private static bool HasCustomAttributeWithName(this ICustomAttributeProvider memberInfo, string fullName, bool inherit) + { + foreach (object attribute in memberInfo.GetCustomAttributes(inherit)) + { + if (attribute.GetType().FullName == fullName) + { + return true; + } + } + + return false; + } + public static TAttribute? GetUniqueCustomAttribute(this MemberInfo memberInfo, bool inherit) where TAttribute : Attribute { diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/JsonHelpers.cs b/src/libraries/System.Text.Json/src/System/Text/Json/JsonHelpers.cs index 0bdd85880bd0fa..6aced2cc4b9145 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/JsonHelpers.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/JsonHelpers.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Buffers; +using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Runtime.CompilerServices; @@ -147,5 +148,19 @@ public static void ValidateInt32MaxArrayLength(uint length) ThrowHelper.ThrowOutOfMemoryException(length); } } + + public static bool AllBitsEqual(this BitArray bitArray, bool value) + { + // Optimize this when https://github.com/dotnet/runtime/issues/72999 is fixed + for (int i = 0; i < bitArray.Count; i++) + { + if (bitArray[i] != value) + { + return false; + } + } + + return true; + } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectDefaultConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectDefaultConverter.cs index 9871d76f83d5b0..365a8d2901279c 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectDefaultConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectDefaultConverter.cs @@ -39,6 +39,7 @@ internal override bool OnTryRead(ref Utf8JsonReader reader, Type typeToConvert, obj = jsonTypeInfo.CreateObject()!; jsonTypeInfo.OnDeserializing?.Invoke(obj); + state.Current.InitializeRequiredPropertiesValidationState(jsonTypeInfo); // Process all properties. while (true) @@ -143,6 +144,7 @@ internal override bool OnTryRead(ref Utf8JsonReader reader, Type typeToConvert, state.Current.ReturnValue = obj; state.Current.ObjectState = StackFrameObjectState.CreatedObject; + state.Current.InitializeRequiredPropertiesValidationState(jsonTypeInfo); } else { @@ -250,6 +252,7 @@ internal override bool OnTryRead(ref Utf8JsonReader reader, Type typeToConvert, } jsonTypeInfo.OnDeserialized?.Invoke(obj); + state.Current.ValidateAllRequiredPropertiesAreRead(jsonTypeInfo); // Unbox Debug.Assert(obj != null); diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.Large.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.Large.cs index a369e8a349bcf6..eabe8e6a62fec6 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.Large.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.Large.cs @@ -24,6 +24,9 @@ protected sealed override bool ReadAndCacheConstructorArgument(ref ReadStack sta if (success && !(arg == null && jsonParameterInfo.IgnoreNullTokensOnRead)) { ((object[])state.Current.CtorArgumentState!.Arguments)[jsonParameterInfo.ClrInfo.Position] = arg!; + + // if this is required property IgnoreNullTokensOnRead will always be false because we don't allow for both to be true + state.Current.MarkRequiredPropertyAsRead(jsonParameterInfo.MatchingProperty); } return success; diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.Small.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.Small.cs index 913198dd364199..1482a32144ec4f 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.Small.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.Small.cs @@ -71,6 +71,11 @@ private static bool TryRead( ? (TArg?)info.DefaultValue! // Use default value specified on parameter, if any. : value!; + if (success) + { + state.Current.MarkRequiredPropertyAsRead(jsonParameterInfo.MatchingProperty); + } + return success; } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.cs index 9a2b85469f9821..d0faaa3c3d238b 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.cs @@ -184,7 +184,15 @@ internal sealed override bool OnTryRead(ref Utf8JsonReader reader, Type typeToCo if (dataExtKey == null) { - jsonPropertyInfo.SetExtensionDictionaryAsObject(obj, propValue); + Debug.Assert(jsonPropertyInfo.Set != null); + + if (propValue is not null || !jsonPropertyInfo.IgnoreNullTokensOnRead || default(T) is not null) + { + jsonPropertyInfo.Set(obj, propValue); + + // if this is required property IgnoreNullTokensOnRead will always be false because we don't allow for both to be true + state.Current.MarkRequiredPropertyAsRead(jsonPropertyInfo); + } } else { @@ -211,6 +219,7 @@ internal sealed override bool OnTryRead(ref Utf8JsonReader reader, Type typeToCo } jsonTypeInfo.OnDeserialized?.Invoke(obj); + state.Current.ValidateAllRequiredPropertiesAreRead(jsonTypeInfo); // Unbox Debug.Assert(obj != null); @@ -272,6 +281,7 @@ private void ReadConstructorArguments(ref ReadStack state, ref Utf8JsonReader re continue; } + Debug.Assert(jsonParameterInfo.MatchingProperty != null); ReadAndCacheConstructorArgument(ref state, ref reader, jsonParameterInfo); state.Current.EndConstructorParameter(); @@ -532,6 +542,8 @@ private void BeginRead(ref ReadStack state, ref Utf8JsonReader reader, JsonSeria ThrowHelper.ThrowInvalidOperationException_ConstructorParameterIncompleteBinding(TypeToConvert); } + state.Current.InitializeRequiredPropertiesValidationState(jsonTypeInfo); + // Set current JsonPropertyInfo to null to avoid conflicts on push. state.Current.JsonPropertyInfo = null; diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandlePropertyName.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandlePropertyName.cs index 598d7157542b84..f374fc70daf79c 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandlePropertyName.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandlePropertyName.cs @@ -139,7 +139,8 @@ internal static void CreateExtensionDataProperty( } extensionData = createObjectForExtensionDataProp(); - jsonPropertyInfo.SetExtensionDictionaryAsObject(obj, extensionData); + Debug.Assert(jsonPropertyInfo.Set != null); + jsonPropertyInfo.Set(obj, extensionData); } // We don't add the value to the dictionary here because we need to support the read-ahead functionality for Streams. diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonParameterInfo.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonParameterInfo.cs index 247f58db58cdc3..2f9ea58106e13c 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonParameterInfo.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonParameterInfo.cs @@ -53,8 +53,11 @@ public JsonTypeInfo JsonTypeInfo public bool ShouldDeserialize { get; private set; } + public JsonPropertyInfo MatchingProperty { get; private set; } = null!; + public virtual void Initialize(JsonParameterInfoValues parameterInfo, JsonPropertyInfo matchingProperty, JsonSerializerOptions options) { + MatchingProperty = matchingProperty; ClrInfo = parameterInfo; Options = options; ShouldDeserialize = true; diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfo.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfo.cs index 128ebf2356c813..7605421549e759 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfo.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfo.cs @@ -203,6 +203,18 @@ public bool IsExtensionData private bool _isExtensionDataProperty; + internal bool IsRequired + { + get => _isRequired; + set + { + VerifyMutable(); + _isRequired = value; + } + } + + private bool _isRequired; + internal JsonPropertyInfo(Type declaringType, Type propertyType, JsonTypeInfo? declaringTypeInfo, JsonSerializerOptions options) { Debug.Assert(declaringTypeInfo is null || declaringTypeInfo.Type == declaringType); @@ -279,6 +291,21 @@ internal void Configure() DetermineIgnoreCondition(); DetermineSerializationCapabilities(); } + + if (IsRequired) + { + if (!CanDeserialize) + { + ThrowHelper.ThrowInvalidOperationException_JsonPropertyRequiredAndNotDeserializable(this); + } + + if (IsExtensionData) + { + ThrowHelper.ThrowInvalidOperationException_JsonPropertyRequiredAndExtensionData(this); + } + + Debug.Assert(!IgnoreNullTokensOnRead); + } } private protected abstract void DetermineEffectiveConverter(JsonTypeInfo jsonTypeInfo); @@ -341,7 +368,7 @@ private void DetermineIgnoreCondition() Debug.Assert(Options.DefaultIgnoreCondition == JsonIgnoreCondition.Never); if (PropertyTypeCanBeNull) { - IgnoreNullTokensOnRead = !_isUserSpecifiedSetter; + IgnoreNullTokensOnRead = !_isUserSpecifiedSetter && !IsRequired; IgnoreDefaultValuesOnWrite = ShouldSerialize is null; } } @@ -477,6 +504,14 @@ private bool NumberHandingIsApplicable() potentialNumberType == JsonTypeInfo.ObjectType; } + private void DetermineIsRequired(MemberInfo memberInfo, bool shouldCheckForRequiredKeyword) + { + if (shouldCheckForRequiredKeyword && memberInfo.HasRequiredMemberAttribute()) + { + IsRequired = true; + } + } + internal abstract bool GetMemberAndWriteJson(object obj, ref WriteStack state, Utf8JsonWriter writer); internal abstract bool GetMemberAndWriteJsonExtensionData(object obj, ref WriteStack state, Utf8JsonWriter writer); @@ -504,7 +539,7 @@ internal string GetDebugInfo(int indent = 0) internal bool HasGetter => _untypedGet is not null; internal bool HasSetter => _untypedSet is not null; - internal void InitializeUsingMemberReflection(MemberInfo memberInfo, JsonConverter? customConverter, JsonIgnoreCondition? ignoreCondition) + internal void InitializeUsingMemberReflection(MemberInfo memberInfo, JsonConverter? customConverter, JsonIgnoreCondition? ignoreCondition, bool shouldCheckForRequiredKeyword) { Debug.Assert(AttributeProvider == null); @@ -531,6 +566,7 @@ internal void InitializeUsingMemberReflection(MemberInfo memberInfo, JsonConvert CustomConverter = customConverter; DeterminePoliciesFromMember(memberInfo); DeterminePropertyNameFromMember(memberInfo); + DetermineIsRequired(memberInfo, shouldCheckForRequiredKeyword); if (ignoreCondition != JsonIgnoreCondition.Always) { @@ -760,8 +796,6 @@ internal JsonTypeInfo JsonTypeInfo } } - internal abstract void SetExtensionDictionaryAsObject(object obj, object? extensionDict); - internal bool IsIgnored => _ignoreCondition == JsonIgnoreCondition.Always; /// @@ -823,6 +857,29 @@ public JsonNumberHandling? NumberHandling /// internal abstract object? DefaultValue { get; } + /// + /// Required property index on the list of JsonTypeInfo properties. + /// It is used as a unique identifier for required properties. + /// It is set just before property is configured and does not change afterward. + /// It is not equivalent to index on the properties list + /// + internal int RequiredPropertyIndex + { + get + { + Debug.Assert(_isConfigured); + Debug.Assert(IsRequired); + return _index; + } + set + { + Debug.Assert(!_isConfigured); + _index = value; + } + } + + private int _index; + [DebuggerBrowsable(DebuggerBrowsableState.Never)] private string DebuggerDisplay => $"PropertyType = {PropertyType}, Name = {Name}, DeclaringType = {DeclaringType}"; } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfoOfT.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfoOfT.cs index 31d0e699ef78d8..d7ed57757927b1 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfoOfT.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfoOfT.cs @@ -342,6 +342,7 @@ internal override bool ReadJsonAndSetMember(object obj, ref ReadStack state, ref } success = true; + state.Current.MarkRequiredPropertyAsRead(this); } else if (TypedEffectiveConverter.CanUseDirectReadOrWrite && state.Current.NumberHandling == null) { @@ -356,6 +357,7 @@ internal override bool ReadJsonAndSetMember(object obj, ref ReadStack state, ref } success = true; + state.Current.MarkRequiredPropertyAsRead(this); } else { @@ -366,6 +368,7 @@ internal override bool ReadJsonAndSetMember(object obj, ref ReadStack state, ref if (success) { Set!(obj, value!); + state.Current.MarkRequiredPropertyAsRead(this); } } } @@ -408,13 +411,6 @@ internal override bool ReadJsonAsObject(ref ReadStack state, ref Utf8JsonReader return success; } - internal override void SetExtensionDictionaryAsObject(object obj, object? extensionDict) - { - Debug.Assert(HasSetter); - T typedValue = (T)extensionDict!; - Set!(obj, typedValue); - } - private protected override void ConfigureIgnoreCondition(JsonIgnoreCondition? ignoreCondition) { switch (ignoreCondition) diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.cs index 99e75e28c6be70..2b41583d5dd92d 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.cs @@ -26,6 +26,11 @@ public abstract partial class JsonTypeInfo private JsonPropertyInfoList? _properties; + /// + /// Indices of required properties. + /// + internal int NumberOfRequiredProperties { get; private set; } + private Action? _onSerializing; private Action? _onSerialized; private Action? _onDeserializing; @@ -878,13 +883,21 @@ internal void InitializePropertyCache() ExtensionDataProperty.EnsureConfigured(); } + int numberOfRequiredProperties = 0; foreach (KeyValuePair jsonPropertyInfoKv in PropertyCache.List) { JsonPropertyInfo jsonPropertyInfo = jsonPropertyInfoKv.Value; + if (jsonPropertyInfo.IsRequired) + { + jsonPropertyInfo.RequiredPropertyIndex = numberOfRequiredProperties++; + } + jsonPropertyInfo.EnsureChildOf(this); jsonPropertyInfo.EnsureConfigured(); } + + NumberOfRequiredProperties = numberOfRequiredProperties; } internal void InitializeConstructorParameters(JsonParameterInfoValues[] jsonParameters, bool sourceGenMode = false) diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/ReflectionJsonTypeInfoOfT.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/ReflectionJsonTypeInfoOfT.cs index 89fe3eade4f775..f176e154011b85 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/ReflectionJsonTypeInfoOfT.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/ReflectionJsonTypeInfoOfT.cs @@ -1,10 +1,12 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Reflection; +using System.Runtime.CompilerServices; using System.Text.Json.Reflection; namespace System.Text.Json.Serialization.Metadata @@ -55,6 +57,11 @@ private void AddPropertiesAndParametersUsingReflection() Dictionary? ignoredMembers = null; bool propertyOrderSpecified = false; + // Compiler adds RequiredMemberAttribute to type if any of the members is marked with 'required' keyword. + // SetsRequiredMembersAttribute means that all required members are assigned by constructor and therefore there is no enforcement + bool shouldCheckMembersForRequiredMemberAttribute = + typeof(T).HasRequiredMemberAttribute() + && !(Converter.ConstructorInfo?.HasSetsRequiredMembersAttribute() ?? false); // Walk through the inheritance hierarchy, starting from the most derived type upward. for (Type? currentType = Type; currentType != null; currentType = currentType.BaseType) @@ -85,7 +92,8 @@ private void AddPropertiesAndParametersUsingReflection() typeToConvert: propertyInfo.PropertyType, memberInfo: propertyInfo, ref propertyOrderSpecified, - ref ignoredMembers); + ref ignoredMembers, + shouldCheckMembersForRequiredMemberAttribute); } else { @@ -117,7 +125,8 @@ private void AddPropertiesAndParametersUsingReflection() typeToConvert: fieldInfo.FieldType, memberInfo: fieldInfo, ref propertyOrderSpecified, - ref ignoredMembers); + ref ignoredMembers, + shouldCheckMembersForRequiredMemberAttribute); } } else @@ -144,9 +153,10 @@ private void CacheMember( Type typeToConvert, MemberInfo memberInfo, ref bool propertyOrderSpecified, - ref Dictionary? ignoredMembers) + ref Dictionary? ignoredMembers, + bool shouldCheckForRequiredKeyword) { - JsonPropertyInfo? jsonPropertyInfo = CreateProperty(typeToConvert, memberInfo, Options); + JsonPropertyInfo? jsonPropertyInfo = CreateProperty(typeToConvert, memberInfo, Options, shouldCheckForRequiredKeyword); if (jsonPropertyInfo == null) { // ignored invalid property @@ -166,7 +176,8 @@ private void CacheMember( private JsonPropertyInfo? CreateProperty( Type typeToConvert, MemberInfo memberInfo, - JsonSerializerOptions options) + JsonSerializerOptions options, + bool shouldCheckForRequiredKeyword) { JsonIgnoreCondition? ignoreCondition = memberInfo.GetCustomAttribute(inherit: false)?.Condition; @@ -191,7 +202,7 @@ private void CacheMember( } JsonPropertyInfo jsonPropertyInfo = CreatePropertyUsingReflection(typeToConvert); - jsonPropertyInfo.InitializeUsingMemberReflection(memberInfo, customConverter, ignoreCondition); + jsonPropertyInfo.InitializeUsingMemberReflection(memberInfo, customConverter, ignoreCondition, shouldCheckForRequiredKeyword); return jsonPropertyInfo; } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReadStackFrame.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReadStackFrame.cs index 253ebbbd170e6c..abf8aa0bd39b5f 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReadStackFrame.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReadStackFrame.cs @@ -1,8 +1,10 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Collections; using System.Collections.Generic; using System.Diagnostics; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text.Json.Serialization; using System.Text.Json.Serialization.Metadata; @@ -66,6 +68,13 @@ public JsonTypeInfo BaseJsonTypeInfo // Whether to use custom number handling. public JsonNumberHandling? NumberHandling; + // Represents required properties which have value assigned. + // Each bit corresponds to a required property. + // False means that property is not set (not yet occured in the payload). + // Length of the BitArray is equal to number of required properties. + // Every required JsonPropertyInfo has RequiredPropertyIndex property which maps to an index in this BitArray. + public BitArray? RequiredPropertiesSet; + public void EndConstructorParameter() { CtorArgumentState!.JsonParameterInfo = null; @@ -107,6 +116,41 @@ public bool IsProcessingEnumerable() return (JsonTypeInfo.PropertyInfoForTypeInfo.ConverterStrategy & ConverterStrategy.Enumerable) != 0; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void MarkRequiredPropertyAsRead(JsonPropertyInfo propertyInfo) + { + if (propertyInfo.IsRequired) + { + Debug.Assert(RequiredPropertiesSet != null); + RequiredPropertiesSet[propertyInfo.RequiredPropertyIndex] = true; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal void InitializeRequiredPropertiesValidationState(JsonTypeInfo typeInfo) + { + Debug.Assert(RequiredPropertiesSet == null); + + if (typeInfo.NumberOfRequiredProperties > 0) + { + RequiredPropertiesSet = new BitArray(typeInfo.NumberOfRequiredProperties); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal void ValidateAllRequiredPropertiesAreRead(JsonTypeInfo typeInfo) + { + if (typeInfo.NumberOfRequiredProperties > 0) + { + Debug.Assert(RequiredPropertiesSet != null); + + if (!RequiredPropertiesSet.AllBitsEqual(true)) + { + ThrowHelper.ThrowJsonException_JsonRequiredPropertyMissing(typeInfo, RequiredPropertiesSet); + } + } + } + [DebuggerBrowsable(DebuggerBrowsableState.Never)] private string DebuggerDisplay => $"ConverterStrategy.{JsonTypeInfo?.PropertyInfoForTypeInfo.ConverterStrategy}, {JsonTypeInfo?.Type.Name}"; } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.Serialization.cs b/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.Serialization.cs index dee7b47adaa160..66384b01b4ce97 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.Serialization.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.Serialization.cs @@ -2,8 +2,11 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Buffers; +using System.Collections; +using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; +using System.Globalization; using System.Reflection; using System.Runtime.CompilerServices; using System.Text.Json.Serialization; @@ -200,6 +203,56 @@ public static void ThrowInvalidOperationException_SerializerPropertyNameNull(Jso throw new InvalidOperationException(SR.Format(SR.SerializerPropertyNameNull, jsonPropertyInfo.DeclaringType, jsonPropertyInfo.MemberName)); } + [DoesNotReturn] + public static void ThrowInvalidOperationException_JsonPropertyRequiredAndNotDeserializable(JsonPropertyInfo jsonPropertyInfo) + { + throw new InvalidOperationException(SR.Format(SR.JsonPropertyRequiredAndNotDeserializable, jsonPropertyInfo.Name, jsonPropertyInfo.DeclaringType)); + } + + [DoesNotReturn] + public static void ThrowInvalidOperationException_JsonPropertyRequiredAndExtensionData(JsonPropertyInfo jsonPropertyInfo) + { + throw new InvalidOperationException(SR.Format(SR.JsonPropertyRequiredAndExtensionData, jsonPropertyInfo.Name, jsonPropertyInfo.DeclaringType)); + } + + [DoesNotReturn] + public static void ThrowJsonException_JsonRequiredPropertyMissing(JsonTypeInfo parent, BitArray requiredPropertiesSet) + { + StringBuilder listOfMissingPropertiesBuilder = new(); + bool first = true; + + Debug.Assert(parent.PropertyCache != null); + + // Soft cut-off length - once message becomes longer than that we won't be adding more elements + const int CutOffLength = 50; + + for (int propertyIdx = 0; propertyIdx < parent.PropertyCache.List.Count; propertyIdx++) + { + JsonPropertyInfo property = parent.PropertyCache.List[propertyIdx].Value; + + if (!property.IsRequired || requiredPropertiesSet[property.RequiredPropertyIndex]) + { + continue; + } + + if (!first) + { + listOfMissingPropertiesBuilder.Append(CultureInfo.CurrentUICulture.TextInfo.ListSeparator); + listOfMissingPropertiesBuilder.Append(' '); + } + + listOfMissingPropertiesBuilder.Append(property.Name); + first = false; + + if (listOfMissingPropertiesBuilder.Length >= CutOffLength) + { + break; + } + } + + throw new JsonException(SR.Format(SR.JsonRequiredPropertiesMissing, parent.Type, listOfMissingPropertiesBuilder.ToString())); + } + [DoesNotReturn] public static void ThrowInvalidOperationException_NamingPolicyReturnNull(JsonNamingPolicy namingPolicy) { @@ -656,11 +709,18 @@ public static void ThrowInvalidOperationException_NoMetadataForType(Type type, I throw new InvalidOperationException(SR.Format(SR.NoMetadataForType, type, resolver?.GetType().FullName ?? "")); } + public static Exception GetInvalidOperationException_NoMetadataForTypeProperties(IJsonTypeInfoResolver? resolver, Type type) + { + return new InvalidOperationException(SR.Format(SR.NoMetadataForTypeProperties, resolver?.GetType().FullName ?? "", type)); + } + + [DoesNotReturn] public static void ThrowInvalidOperationException_NoMetadataForTypeProperties(IJsonTypeInfoResolver? resolver, Type type) { - throw new InvalidOperationException(SR.Format(SR.NoMetadataForTypeProperties, resolver?.GetType().FullName ?? "", type)); + throw GetInvalidOperationException_NoMetadataForTypeProperties(resolver, type); } + [DoesNotReturn] public static void ThrowInvalidOperationException_NoMetadataForTypeCtorParams(IJsonTypeInfoResolver? resolver, Type type) { throw new InvalidOperationException(SR.Format(SR.NoMetadataForTypeCtorParams, resolver?.GetType().FullName ?? "", type)); diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/RequiredKeywordTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/RequiredKeywordTests.cs new file mode 100644 index 00000000000000..9517d94c2ce1ad --- /dev/null +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/RequiredKeywordTests.cs @@ -0,0 +1,521 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Text.Json.Serialization.Metadata; +using Newtonsoft.Json; +using Xunit; + +namespace System.Text.Json.Serialization.Tests +{ + public class RequiredKeywordTests_Span : RequiredKeywordTests + { + public RequiredKeywordTests_Span() : base(JsonSerializerWrapper.SpanSerializer) { } + } + + public class RequiredKeywordTests_String : RequiredKeywordTests + { + public RequiredKeywordTests_String() : base(JsonSerializerWrapper.StringSerializer) { } + } + + public class RequiredKeywordTests_AsyncStream : RequiredKeywordTests + { + public RequiredKeywordTests_AsyncStream() : base(JsonSerializerWrapper.AsyncStreamSerializer) { } + } + + public class RequiredKeywordTests_AsyncStreamWithSmallBuffer : RequiredKeywordTests + { + public RequiredKeywordTests_AsyncStreamWithSmallBuffer() : base(JsonSerializerWrapper.AsyncStreamSerializerWithSmallBuffer) { } + } + + public class RequiredKeywordTests_SyncStream : RequiredKeywordTests + { + public RequiredKeywordTests_SyncStream() : base(JsonSerializerWrapper.SyncStreamSerializer) { } + } + + public class RequiredKeywordTests_Writer : RequiredKeywordTests + { + public RequiredKeywordTests_Writer() : base(JsonSerializerWrapper.ReaderWriterSerializer) { } + } + + public class RequiredKeywordTests_Document : RequiredKeywordTests + { + public RequiredKeywordTests_Document() : base(JsonSerializerWrapper.DocumentSerializer) { } + } + + public class RequiredKeywordTests_Element : RequiredKeywordTests + { + public RequiredKeywordTests_Element() : base(JsonSerializerWrapper.ElementSerializer) { } + } + + public class RequiredKeywordTests_Node : RequiredKeywordTests + { + public RequiredKeywordTests_Node() : base(JsonSerializerWrapper.NodeSerializer) { } + } + + public abstract class RequiredKeywordTests : SerializerTests + { + public RequiredKeywordTests(JsonSerializerWrapper serializer) : base(serializer) + { + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async void ClassWithRequiredKeywordDeserialization(bool ignoreNullValues) + { + JsonSerializerOptions options = new() + { + IgnoreNullValues = ignoreNullValues + }; + + var obj = new PersonWithRequiredMembers() + { + FirstName = "foo", + LastName = "bar" + }; + + string json = await Serializer.SerializeWrapper(obj, options); + Assert.Equal("""{"FirstName":"foo","MiddleName":"","LastName":"bar"}""", json); + + PersonWithRequiredMembers deserialized = await Serializer.DeserializeWrapper(json, options); + Assert.Equal(obj.FirstName, deserialized.FirstName); + Assert.Equal(obj.MiddleName, deserialized.MiddleName); + Assert.Equal(obj.LastName, deserialized.LastName); + + json = """{"LastName":"bar"}"""; + JsonException exception = await Assert.ThrowsAsync(async () => await Serializer.DeserializeWrapper(json, options)); + Assert.Contains("FirstName", exception.Message); + Assert.DoesNotContain("LastName", exception.Message); + Assert.DoesNotContain("MiddleName", exception.Message); + + json = """{"LastName":null}"""; + exception = await Assert.ThrowsAsync(async () => await Serializer.DeserializeWrapper(json, options)); + Assert.Contains("FirstName", exception.Message); + Assert.DoesNotContain("LastName", exception.Message); + Assert.DoesNotContain("MiddleName", exception.Message); + + json = "{}"; + exception = await Assert.ThrowsAsync(async () => await Serializer.DeserializeWrapper(json, options)); + Assert.Contains("FirstName", exception.Message); + Assert.Contains("LastName", exception.Message); + Assert.DoesNotContain("MiddleName", exception.Message); + } + + [Fact] + public async void RequiredPropertyOccuringTwiceInThePayloadWorksAsExpected() + { + string json = """{"FirstName":"foo","MiddleName":"","LastName":"bar","FirstName":"newfoo"}"""; + PersonWithRequiredMembers deserialized = await Serializer.DeserializeWrapper(json); + Assert.Equal("newfoo", deserialized.FirstName); + Assert.Equal("", deserialized.MiddleName); + Assert.Equal("bar", deserialized.LastName); + } + + private class PersonWithRequiredMembers + { + public required string FirstName { get; set; } + public string MiddleName { get; set; } = ""; + public required string LastName { get; set; } + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async void ClassWithRequiredKeywordAndSmallParametrizedCtorFailsDeserialization(bool ignoreNullValues) + { + JsonSerializerOptions options = new() + { + IgnoreNullValues = ignoreNullValues + }; + + var obj = new PersonWithRequiredMembersAndSmallParametrizedCtor("badfoo", "badbar") + { + // note: these must be set during initialize or otherwise we get compiler errors + FirstName = "foo", + LastName = "bar", + Info1 = "info1", + Info2 = "info2", + }; + + string json = await Serializer.SerializeWrapper(obj, options); + Assert.Equal("""{"FirstName":"foo","MiddleName":"","LastName":"bar","Info1":"info1","Info2":"info2"}""", json); + + var deserialized = await Serializer.DeserializeWrapper(json, options); + Assert.Equal(obj.FirstName, deserialized.FirstName); + Assert.Equal(obj.MiddleName, deserialized.MiddleName); + Assert.Equal(obj.LastName, deserialized.LastName); + Assert.Equal(obj.Info1, deserialized.Info1); + Assert.Equal(obj.Info2, deserialized.Info2); + + json = """{"FirstName":"foo","MiddleName":"","LastName":null,"Info1":null,"Info2":"info2"}"""; + deserialized = await Serializer.DeserializeWrapper(json, options); + Assert.Equal(obj.FirstName, deserialized.FirstName); + Assert.Equal(obj.MiddleName, deserialized.MiddleName); + Assert.Null(deserialized.LastName); + Assert.Null(deserialized.Info1); + Assert.Equal(obj.Info2, deserialized.Info2); + + json = """{"LastName":"bar","Info1":"info1"}"""; + JsonException exception = await Assert.ThrowsAsync(async () => await Serializer.DeserializeWrapper(json, options)); + Assert.Contains("FirstName", exception.Message); + Assert.DoesNotContain("LastName", exception.Message); + Assert.DoesNotContain("MiddleName", exception.Message); + Assert.DoesNotContain("Info1", exception.Message); + Assert.Contains("Info2", exception.Message); + + json = """{"LastName":null,"Info1":null}"""; + exception = await Assert.ThrowsAsync(async () => await Serializer.DeserializeWrapper(json, options)); + Assert.Contains("FirstName", exception.Message); + Assert.DoesNotContain("LastName", exception.Message); + Assert.DoesNotContain("MiddleName", exception.Message); + Assert.DoesNotContain("Info1", exception.Message); + Assert.Contains("Info2", exception.Message); + + json = "{}"; + exception = await Assert.ThrowsAsync(async () => await Serializer.DeserializeWrapper(json, options)); + Assert.Contains("FirstName", exception.Message); + Assert.Contains("LastName", exception.Message); + Assert.DoesNotContain("MiddleName", exception.Message); + Assert.Contains("Info1", exception.Message); + Assert.Contains("Info2", exception.Message); + } + + private class PersonWithRequiredMembersAndSmallParametrizedCtor + { + public required string FirstName { get; set; } + public string MiddleName { get; set; } = ""; + public required string LastName { get; set; } + public required string Info1 { get; set; } + public required string Info2 { get; set; } + + public PersonWithRequiredMembersAndSmallParametrizedCtor(string firstName, string lastName) + { + FirstName = firstName; + LastName = lastName; + } + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async void ClassWithRequiredKeywordAndLargeParametrizedCtorFailsDeserialization(bool ignoreNullValues) + { + JsonSerializerOptions options = new() + { + IgnoreNullValues = ignoreNullValues + }; + + var obj = new PersonWithRequiredMembersAndLargeParametrizedCtor("bada", "badb", "badc", "badd", "bade", "badf", "badg") + { + // note: these must be set during initialize or otherwise we get compiler errors + AProp = "a", + BProp = "b", + CProp = "c", + DProp = "d", + EProp = "e", + FProp = "f", + GProp = "g", + HProp = "h", + IProp = "i", + }; + + string json = await Serializer.SerializeWrapper(obj, options); + Assert.Equal("""{"AProp":"a","BProp":"b","CProp":"c","DProp":"d","EProp":"e","FProp":"f","GProp":"g","HProp":"h","IProp":"i"}""", json); + + var deserialized = await Serializer.DeserializeWrapper(json, options); + Assert.Equal(obj.AProp, deserialized.AProp); + Assert.Equal(obj.BProp, deserialized.BProp); + Assert.Equal(obj.CProp, deserialized.CProp); + Assert.Equal(obj.DProp, deserialized.DProp); + Assert.Equal(obj.EProp, deserialized.EProp); + Assert.Equal(obj.FProp, deserialized.FProp); + Assert.Equal(obj.GProp, deserialized.GProp); + Assert.Equal(obj.HProp, deserialized.HProp); + Assert.Equal(obj.IProp, deserialized.IProp); + + json = """{"AProp":"a","BProp":"b","CProp":"c","DProp":"d","EProp":null,"FProp":"f","GProp":"g","HProp":null,"IProp":"i"}"""; + deserialized = await Serializer.DeserializeWrapper(json, options); + Assert.Equal(obj.AProp, deserialized.AProp); + Assert.Equal(obj.BProp, deserialized.BProp); + Assert.Equal(obj.CProp, deserialized.CProp); + Assert.Equal(obj.DProp, deserialized.DProp); + Assert.Null(deserialized.EProp); + Assert.Equal(obj.FProp, deserialized.FProp); + Assert.Equal(obj.GProp, deserialized.GProp); + Assert.Null(deserialized.HProp); + Assert.Equal(obj.IProp, deserialized.IProp); + + json = """{"AProp":"a","IProp":"i"}"""; + JsonException exception = await Assert.ThrowsAsync(async () => await Serializer.DeserializeWrapper(json, options)); + Assert.DoesNotContain("AProp", exception.Message); + Assert.Contains("BProp", exception.Message); + Assert.Contains("CProp", exception.Message); + Assert.Contains("DProp", exception.Message); + Assert.Contains("EProp", exception.Message); + Assert.Contains("FProp", exception.Message); + Assert.Contains("GProp", exception.Message); + Assert.Contains("HProp", exception.Message); + Assert.DoesNotContain("IProp", exception.Message); + + json = """{"AProp":null,"IProp":null}"""; + exception = await Assert.ThrowsAsync(async () => await Serializer.DeserializeWrapper(json, options)); + Assert.DoesNotContain("AProp", exception.Message); + Assert.Contains("BProp", exception.Message); + Assert.Contains("CProp", exception.Message); + Assert.Contains("DProp", exception.Message); + Assert.Contains("EProp", exception.Message); + Assert.Contains("FProp", exception.Message); + Assert.Contains("GProp", exception.Message); + Assert.Contains("HProp", exception.Message); + Assert.DoesNotContain("IProp", exception.Message); + + json = """{"BProp":"b","CProp":"c","DProp":"d","EProp":"e","FProp":"f","HProp":"h"}"""; + exception = await Assert.ThrowsAsync(async () => await Serializer.DeserializeWrapper(json, options)); + Assert.Contains("AProp", exception.Message); + Assert.DoesNotContain("BProp", exception.Message); + Assert.DoesNotContain("CProp", exception.Message); + Assert.DoesNotContain("DProp", exception.Message); + Assert.DoesNotContain("EProp", exception.Message); + Assert.DoesNotContain("FProp", exception.Message); + Assert.Contains("GProp", exception.Message); + Assert.DoesNotContain("HProp", exception.Message); + Assert.Contains("IProp", exception.Message); + } + + private class PersonWithRequiredMembersAndLargeParametrizedCtor + { + // Using suffix for names so that checking if required property is missing can be done with simple string.Contains without false positives + public required string AProp { get; set; } + public required string BProp { get; set; } + public required string CProp { get; set; } + public required string DProp { get; set; } + public required string EProp { get; set; } + public required string FProp { get; set; } + public required string GProp { get; set; } + public required string HProp { get; set; } + public required string IProp { get; set; } + + public PersonWithRequiredMembersAndLargeParametrizedCtor(string aprop, string bprop, string cprop, string dprop, string eprop, string fprop, string gprop) + { + AProp = aprop; + BProp = bprop; + CProp = cprop; + DProp = dprop; + EProp = eprop; + FProp = fprop; + GProp = gprop; + } + } + + [Fact] + public async void ClassWithRequiredKeywordAndSetsRequiredMembersOnCtorWorks() + { + var obj = new PersonWithRequiredMembersAndSetsRequiredMembers() + { + FirstName = "foo", + LastName = "bar" + }; + + string json = await Serializer.SerializeWrapper(obj); + Assert.Equal("""{"FirstName":"foo","MiddleName":"","LastName":"bar"}""", json); + + json = """{"LastName":"bar"}"""; + var deserialized = await Serializer.DeserializeWrapper(json); + Assert.Equal("", deserialized.FirstName); + Assert.Equal("", deserialized.MiddleName); + Assert.Equal("bar", deserialized.LastName); + } + + private class PersonWithRequiredMembersAndSetsRequiredMembers + { + public required string FirstName { get; set; } + public string MiddleName { get; set; } = ""; + public required string LastName { get; set; } + + [SetsRequiredMembers] + public PersonWithRequiredMembersAndSetsRequiredMembers() + { + FirstName = ""; + LastName = ""; + } + } + + [Fact] + public async void ClassWithRequiredKeywordSmallParametrizedCtorAndSetsRequiredMembersOnCtorWorks() + { + var obj = new PersonWithRequiredMembersAndSmallParametrizedCtorAndSetsRequiredMembers("foo", "bar"); + + string json = await Serializer.SerializeWrapper(obj); + Assert.Equal("""{"FirstName":"foo","MiddleName":"","LastName":"bar"}""", json); + + var deserialized = await Serializer.DeserializeWrapper(json); + Assert.Equal("foo", deserialized.FirstName); + Assert.Equal("", deserialized.MiddleName); + Assert.Equal("bar", deserialized.LastName); + } + + private class PersonWithRequiredMembersAndSmallParametrizedCtorAndSetsRequiredMembers + { + public required string FirstName { get; init; } + public string MiddleName { get; init; } = ""; + public required string LastName { get; init; } + + [SetsRequiredMembers] + public PersonWithRequiredMembersAndSmallParametrizedCtorAndSetsRequiredMembers(string firstName, string lastName) + { + FirstName = firstName; + LastName = lastName; + } + } + + [Fact] + public async void ClassWithRequiredKeywordLargeParametrizedCtorAndSetsRequiredMembersOnCtorWorks() + { + var obj = new PersonWithRequiredMembersAndLargeParametrizedCtorndSetsRequiredMembers("a", "b", "c", "d", "e", "f", "g"); + + string json = await Serializer.SerializeWrapper(obj); + Assert.Equal("""{"A":"a","B":"b","C":"c","D":"d","E":"e","F":"f","G":"g"}""", json); + + var deserialized = await Serializer.DeserializeWrapper(json); + Assert.Equal("a", deserialized.A); + Assert.Equal("b", deserialized.B); + Assert.Equal("c", deserialized.C); + Assert.Equal("d", deserialized.D); + Assert.Equal("e", deserialized.E); + Assert.Equal("f", deserialized.F); + Assert.Equal("g", deserialized.G); + } + + private class PersonWithRequiredMembersAndLargeParametrizedCtorndSetsRequiredMembers + { + public required string A { get; set; } + public required string B { get; set; } + public required string C { get; set; } + public required string D { get; set; } + public required string E { get; set; } + public required string F { get; set; } + public required string G { get; set; } + + [SetsRequiredMembers] + public PersonWithRequiredMembersAndLargeParametrizedCtorndSetsRequiredMembers(string a, string b, string c, string d, string e, string f, string g) + { + A = a; + B = b; + C = c; + D = d; + E = e; + F = f; + G = g; + } + } + + [Fact] + public async void RemovingRequiredPropertiesAllowsDeserialization() + { + JsonSerializerOptions options = new() + { + TypeInfoResolver = new DefaultJsonTypeInfoResolver() + { + Modifiers = + { + (ti) => + { + for (int i = 0; i < ti.Properties.Count; i++) + { + if (ti.Properties[i].Name == nameof(PersonWithRequiredMembers.FirstName)) + { + JsonPropertyInfo property = ti.CreateJsonPropertyInfo(typeof(string), nameof(PersonWithRequiredMembers.FirstName)); + property.Get = (obj) => ((PersonWithRequiredMembers)obj).FirstName; + property.Set = (obj, val) => ((PersonWithRequiredMembers)obj).FirstName = (string)val; + ti.Properties[i] = property; + } + else if (ti.Properties[i].Name == nameof(PersonWithRequiredMembers.LastName)) + { + JsonPropertyInfo property = ti.CreateJsonPropertyInfo(typeof(string), nameof(PersonWithRequiredMembers.LastName)); + property.Get = (obj) => ((PersonWithRequiredMembers)obj).LastName; + property.Set = (obj, val) => ((PersonWithRequiredMembers)obj).LastName = (string)val; + ti.Properties[i] = property; + } + } + } + } + } + }; + + var obj = new PersonWithRequiredMembers() + { + FirstName = "foo", + LastName = "bar" + }; + + string json = await Serializer.SerializeWrapper(obj, options); + Assert.Equal("""{"FirstName":"foo","MiddleName":"","LastName":"bar"}""", json); + + json = """{"LastName":"bar"}"""; + PersonWithRequiredMembers deserialized = await Serializer.DeserializeWrapper(json, options); + Assert.Null(deserialized.FirstName); + Assert.Equal("", deserialized.MiddleName); + Assert.Equal("bar", deserialized.LastName); + } + + [Fact] + public async void RequiredNonDeserializablePropertyThrows() + { + JsonSerializerOptions options = new() + { + TypeInfoResolver = new DefaultJsonTypeInfoResolver() + { + Modifiers = + { + (ti) => + { + for (int i = 0; i < ti.Properties.Count; i++) + { + if (ti.Properties[i].Name == nameof(PersonWithRequiredMembers.FirstName)) + { + ti.Properties[i].Set = null; + } + } + } + } + } + }; + + string json = """{"FirstName":"foo","MiddleName":"","LastName":"bar"}"""; + InvalidOperationException exception = await Assert.ThrowsAsync(async () => await Serializer.DeserializeWrapper(json, options)); + Assert.Contains(nameof(PersonWithRequiredMembers.FirstName), exception.Message); + } + + [Fact] + public async void RequiredInitOnlyPropertyDoesNotThrow() + { + string json = """{"Prop":"foo"}"""; + ClassWithInitOnlyRequiredProperty deserialized = await Serializer.DeserializeWrapper(json); + Assert.Equal("foo", deserialized.Prop); + } + + private class ClassWithInitOnlyRequiredProperty + { + public required string Prop { get; init; } + } + + [Fact] + public async void RequiredExtensionDataPropertyThrows() + { + string json = """{"Foo":"foo","Bar":"bar"}"""; + InvalidOperationException exception = await Assert.ThrowsAsync( + async () => await Serializer.DeserializeWrapper(json)); + Assert.Contains(nameof(ClassWithRequiredExtensionDataProperty.TestExtensionData), exception.Message); + } + + private class ClassWithRequiredExtensionDataProperty + { + [JsonExtensionData] + public required Dictionary? TestExtensionData { get; set; } + } + } +} diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/System.Text.Json.Tests.csproj b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/System.Text.Json.Tests.csproj index d68f600d0ca832..2bd76ae43071f8 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/System.Text.Json.Tests.csproj +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/System.Text.Json.Tests.csproj @@ -25,10 +25,6 @@ - - - - @@ -159,6 +155,7 @@ + @@ -236,10 +233,15 @@ - + + + + + +