Skip to content

Commit d0969e6

Browse files
eiriktsarpalisjtschuster
authored andcommitted
Ensure required properties are validated before invoking the deserialization constructor. (dotnet#107083)
1 parent d647d61 commit d0969e6

File tree

5 files changed

+33
-9
lines changed

5 files changed

+33
-9
lines changed

src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.Large.cs

-3
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,6 @@ protected sealed override bool ReadAndCacheConstructorArgument(scoped ref ReadSt
2828
}
2929

3030
((object[])state.Current.CtorArgumentState!.Arguments)[jsonParameterInfo.Position] = arg!;
31-
32-
// if this is required property IgnoreNullTokensOnRead will always be false because we don't allow for both to be true
33-
state.Current.MarkRequiredPropertyAsRead(jsonParameterInfo.MatchingProperty);
3431
}
3532

3633
return success;

src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.Small.cs

-2
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,6 @@ private static bool TryRead<TArg>(
8080
ThrowHelper.ThrowJsonException_ConstructorParameterDisallowNull(info.Name, state.Current.JsonTypeInfo.Type);
8181
}
8282
}
83-
84-
state.Current.MarkRequiredPropertyAsRead(jsonParameterInfo.MatchingProperty);
8583
}
8684

8785
arg = value;

src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.cs

+13-4
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,11 @@ internal sealed override bool OnTryRead(ref Utf8JsonReader reader, Type typeToCo
5858

5959
ReadConstructorArguments(ref state, ref reader, options);
6060

61+
// We've read all ctor parameters and properties,
62+
// validate that all required parameters were provided
63+
// before calling the constructor which may throw.
64+
state.Current.ValidateAllRequiredPropertiesAreRead(jsonTypeInfo);
65+
6166
obj = (T)CreateObject(ref state.Current);
6267

6368
jsonTypeInfo.OnDeserializing?.Invoke(obj);
@@ -192,6 +197,11 @@ internal sealed override bool OnTryRead(ref Utf8JsonReader reader, Type typeToCo
192197
return false;
193198
}
194199

200+
// We've read all ctor parameters and properties,
201+
// validate that all required parameters were provided
202+
// before calling the constructor which may throw.
203+
state.Current.ValidateAllRequiredPropertiesAreRead(jsonTypeInfo);
204+
195205
obj = (T)CreateObject(ref state.Current);
196206

197207
if ((state.Current.MetadataPropertyNames & MetadataPropertyName.Id) != 0)
@@ -219,9 +229,6 @@ internal sealed override bool OnTryRead(ref Utf8JsonReader reader, Type typeToCo
219229
if (propValue is not null || !jsonPropertyInfo.IgnoreNullTokensOnRead || default(T) is not null)
220230
{
221231
jsonPropertyInfo.Set(obj, propValue);
222-
223-
// if this is required property IgnoreNullTokensOnRead will always be false because we don't allow for both to be true
224-
state.Current.MarkRequiredPropertyAsRead(jsonPropertyInfo);
225232
}
226233
}
227234
else
@@ -249,7 +256,6 @@ internal sealed override bool OnTryRead(ref Utf8JsonReader reader, Type typeToCo
249256
}
250257

251258
jsonTypeInfo.OnDeserialized?.Invoke(obj);
252-
state.Current.ValidateAllRequiredPropertiesAreRead(jsonTypeInfo);
253259

254260
// Unbox
255261
Debug.Assert(obj != null);
@@ -606,6 +612,9 @@ protected static bool TryLookupConstructorParameter(
606612
out bool useExtensionProperty,
607613
createExtensionProperty: false);
608614

615+
// Mark the property as read from the payload if required.
616+
state.Current.MarkRequiredPropertyAsRead(jsonPropertyInfo);
617+
609618
jsonParameterInfo = jsonPropertyInfo.AssociatedParameter;
610619
if (jsonParameterInfo != null)
611620
{

src/libraries/System.Text.Json/tests/Common/RequiredKeywordTests.cs

+19
Original file line numberDiff line numberDiff line change
@@ -727,6 +727,25 @@ public static IEnumerable<object[]> InheritedPersonWithRequiredMembersSetsRequir
727727
};
728728
}
729729

730+
[Fact]
731+
public async Task ClassWithNullValidatingConstructor_ValidatesRequiredParameterBeforeCallingCtor()
732+
{
733+
// Regression test for https://github.com/dotnet/runtime/issues/107065
734+
JsonSerializerOptions options = new(Serializer.DefaultOptions) { RespectRequiredConstructorParameters = true };
735+
JsonException ex = await Assert.ThrowsAsync<JsonException>(() => Serializer.DeserializeWrapper<ClassWithNullValidatingConstructor>("{}", options));
736+
Assert.Null(ex.InnerException);
737+
}
738+
739+
public class ClassWithNullValidatingConstructor
740+
{
741+
public ClassWithNullValidatingConstructor(string value)
742+
{
743+
Value = value ?? throw new ArgumentNullException(nameof(value));
744+
}
745+
746+
public string Value { get; }
747+
}
748+
730749
private static JsonTypeInfo GetTypeInfo<T>(JsonSerializerOptions options)
731750
{
732751
options.TypeInfoResolver ??= JsonSerializerOptions.Default.TypeInfoResolver;

src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/RequiredKeywordTests.cs

+1
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ public RequiredKeywordTests_SourceGen()
3333
[JsonSerializable(typeof(ClassWithRequiredKeywordAndJsonRequiredCustomAttribute))]
3434
[JsonSerializable(typeof(ClassWithCustomRequiredPropertyName))]
3535
[JsonSerializable(typeof(DerivedClassWithRequiredInitOnlyProperty))]
36+
[JsonSerializable(typeof(ClassWithNullValidatingConstructor))]
3637
internal sealed partial class RequiredKeywordTestsContext : JsonSerializerContext
3738
{
3839
}

0 commit comments

Comments
 (0)