Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -76,11 +76,9 @@ private static void PopulateProperties(JsonTypeInfo typeInfo)
Debug.Assert(!typeInfo.IsReadOnly);
Debug.Assert(typeInfo.Kind is JsonTypeInfoKind.Object);

// 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 =
typeInfo.Type.HasRequiredMemberAttribute()
&& !(typeInfo.Converter.ConstructorInfo?.HasSetsRequiredMembersAttribute() ?? false);
bool constructorHasSetsRequiredMembersAttribute =
typeInfo.Converter.ConstructorInfo?.HasSetsRequiredMembersAttribute() ?? false;

JsonTypeInfo.PropertyHierarchyResolutionState state = new();

Expand All @@ -93,6 +91,10 @@ private static void PopulateProperties(JsonTypeInfo typeInfo)
break;
}

// Compiler adds RequiredMemberAttribute to type if any of the members are marked with 'required' keyword.
bool shouldCheckMembersForRequiredMemberAttribute =
!constructorHasSetsRequiredMembersAttribute && currentType.HasRequiredMemberAttribute();

AddMembersDeclaredBySuperType(
typeInfo,
currentType,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ private static string GetNumberAsString<T>(T number)
double @double => @double.ToString(JsonTestHelper.DoubleFormatString, CultureInfo.InvariantCulture),
float @float => @float.ToString(JsonTestHelper.SingleFormatString, CultureInfo.InvariantCulture),
decimal @decimal => @decimal.ToString(CultureInfo.InvariantCulture),
_ => number.ToString()
_ => Convert.ToString(number, CultureInfo.InvariantCulture)
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,86 @@ public async Task ClassWithRequiredKeywordAndSmallParametrizedCtorFailsDeseriali
Assert.Contains("Info2", exception.Message);
}

[Fact]
public async Task InheritedPersonWithRequiredMembersWorksAsExpected()
{
var options = new JsonSerializerOptions(Serializer.DefaultOptions);
options.MakeReadOnly();

JsonTypeInfo typeInfo = options.GetTypeInfo(typeof(InheritedPersonWithRequiredMembers));
Assert.Equal(3, typeInfo.Properties.Count);

AssertJsonTypeInfoHasRequiredProperties(GetTypeInfo<InheritedPersonWithRequiredMembers>(options),
nameof(InheritedPersonWithRequiredMembers.FirstName),
nameof(InheritedPersonWithRequiredMembers.LastName));

JsonException exception = await Assert.ThrowsAsync<JsonException>(() => Serializer.DeserializeWrapper("{}", typeInfo));
Assert.Contains("FirstName", exception.Message);
Assert.Contains("LastName", exception.Message);
Assert.DoesNotContain("MiddleName", exception.Message);
}

[Fact]
public async Task InheritedPersonWithRequiredMembersWithAdditionalRequiredMembersWorksAsExpected()
{
var options = new JsonSerializerOptions(Serializer.DefaultOptions);
options.MakeReadOnly();

JsonTypeInfo typeInfo = options.GetTypeInfo(typeof(InheritedPersonWithRequiredMembersWithAdditionalRequiredMembers));
Assert.Equal(4, typeInfo.Properties.Count);

AssertJsonTypeInfoHasRequiredProperties(GetTypeInfo<InheritedPersonWithRequiredMembersWithAdditionalRequiredMembers>(options),
nameof(InheritedPersonWithRequiredMembersWithAdditionalRequiredMembers.FirstName),
nameof(InheritedPersonWithRequiredMembersWithAdditionalRequiredMembers.LastName),
nameof(InheritedPersonWithRequiredMembersWithAdditionalRequiredMembers.Post));

JsonException exception = await Assert.ThrowsAsync<JsonException>(() => Serializer.DeserializeWrapper("{}", typeInfo));
Assert.Contains("FirstName", exception.Message);
Assert.Contains("LastName", exception.Message);
Assert.Contains("Post", exception.Message);
Assert.DoesNotContain("MiddleName", exception.Message);
}

[Theory]
[MemberData(nameof(InheritedPersonWithRequiredMembersSetsRequiredMembersWorksAsExpectedSources))]
public async Task InheritedPersonWithRequiredMembersSetsRequiredMembersWorksAsExpected(string jsonValue,
InheritedPersonWithRequiredMembersSetsRequiredMembers expectedValue)
{
var options = new JsonSerializerOptions(Serializer.DefaultOptions);
options.MakeReadOnly();

JsonTypeInfo typeInfo = options.GetTypeInfo(typeof(InheritedPersonWithRequiredMembersSetsRequiredMembers));
Assert.Equal(3, typeInfo.Properties.Count);

AssertJsonTypeInfoHasRequiredProperties(GetTypeInfo<InheritedPersonWithRequiredMembersSetsRequiredMembers>(options));

InheritedPersonWithRequiredMembersSetsRequiredMembers actualValue =
await Serializer.DeserializeWrapper<InheritedPersonWithRequiredMembersSetsRequiredMembers>(jsonValue, options);
Assert.Equal(expectedValue.FirstName, actualValue.FirstName);
Assert.Equal(expectedValue.LastName, actualValue.LastName);
Assert.Equal(expectedValue.MiddleName, actualValue.MiddleName);
}

public class InheritedPersonWithRequiredMembers : PersonWithRequiredMembers
{
}

public class InheritedPersonWithRequiredMembersWithAdditionalRequiredMembers : PersonWithRequiredMembers
{
public required string Post { get; set; }
}

public class InheritedPersonWithRequiredMembersSetsRequiredMembers : PersonWithRequiredMembers
{
[SetsRequiredMembers]
public InheritedPersonWithRequiredMembersSetsRequiredMembers()
{
FirstName = "FirstNameValueFromConstructor";
LastName = "LastNameValueFromConstructor";
MiddleName = "MiddleNameValueFromConstructor";
}
}

public class PersonWithRequiredMembersAndSmallParametrizedCtor
{
public required string FirstName { get; set; }
Expand Down Expand Up @@ -584,6 +664,23 @@ public class ClassWithCustomRequiredPropertyName
public required int PropertyWithInitOnlySetter { get; init; }
}

public static IEnumerable<object[]> InheritedPersonWithRequiredMembersSetsRequiredMembersWorksAsExpectedSources()
{
yield return new object[]
{
"{}",
new InheritedPersonWithRequiredMembersSetsRequiredMembers()
};
yield return new object[]
{
"""{"FirstName": "FirstNameFromJson"}""",
new InheritedPersonWithRequiredMembersSetsRequiredMembers
{
FirstName = "FirstNameFromJson"
}
};
}

private static JsonTypeInfo GetTypeInfo<T>(JsonSerializerOptions options)
{
options.TypeInfoResolver ??= JsonSerializerOptions.Default.TypeInfoResolver;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ public RequiredKeywordTests_SourceGen()
{
}

[JsonSerializable(typeof(InheritedPersonWithRequiredMembers))]
[JsonSerializable(typeof(InheritedPersonWithRequiredMembersWithAdditionalRequiredMembers))]
[JsonSerializable(typeof(InheritedPersonWithRequiredMembersSetsRequiredMembers))]
[JsonSerializable(typeof(PersonWithRequiredMembers))]
[JsonSerializable(typeof(PersonWithRequiredMembersAndSmallParametrizedCtor))]
[JsonSerializable(typeof(PersonWithRequiredMembersAndLargeParametrizedCtor))]
Expand Down