diff --git a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs index 9590a3b90f07d2..0f1043f8c8cce8 100644 --- a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs +++ b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs @@ -1028,7 +1028,7 @@ void CacheMemberHelper(Location memberLocation) _implicitlyRegisteredTypes.Add(dataExtensionPropGenSpec); } - if (!hasInitOnlyProperties && spec.CanUseSetter && spec.IsInitOnlySetter) + if (!hasInitOnlyProperties && spec.CanUseSetter && spec.IsInitOnlySetter && !PropertyIsConstructorParameter(spec, paramGenSpecArray)) { _sourceGenerationContext.ReportDiagnostic(Diagnostic.Create(InitOnlyPropertyDeserializationNotSupported, memberLocation, new string[] { type.Name })); hasInitOnlyProperties = true; @@ -1119,6 +1119,9 @@ private void CacheMember( } } + private static bool PropertyIsConstructorParameter(PropertyGenerationSpec propSpec, ParameterGenerationSpec[]? paramGenSpecArray) + => paramGenSpecArray != null && paramGenSpecArray.Any(paramSpec => propSpec.ClrName.Equals(paramSpec.ParameterInfo.Name, StringComparison.OrdinalIgnoreCase)); + private static bool PropertyIsOverridenAndIgnored( string currentMemberName, Type currentMemberType, diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/ContextClasses.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/ContextClasses.cs index a21ab179cc8eda..ef78645435fc27 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/ContextClasses.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/ContextClasses.cs @@ -28,6 +28,7 @@ public interface ITestContext public JsonTypeInfo MyTypeWithPropertyOrdering { get; } public JsonTypeInfo MyIntermediateType { get; } public JsonTypeInfo HighLowTempsImmutable { get; } + public JsonTypeInfo HighLowTempsRecord { get; } public JsonTypeInfo MyNestedClass { get; } public JsonTypeInfo MyNestedNestedClass { get; } public JsonTypeInfo ObjectArray { get; } diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/MetadataAndSerializationContextTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/MetadataAndSerializationContextTests.cs index 092d6110170739..8544ccbb934da2 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/MetadataAndSerializationContextTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/MetadataAndSerializationContextTests.cs @@ -22,6 +22,7 @@ namespace System.Text.Json.SourceGeneration.Tests [JsonSerializable(typeof(MyTypeWithPropertyOrdering))] [JsonSerializable(typeof(MyIntermediateType))] [JsonSerializable(typeof(HighLowTempsImmutable))] + [JsonSerializable(typeof(HighLowTempsRecord))] [JsonSerializable(typeof(byte[]))] [JsonSerializable(typeof(RealWorldContextTests.MyNestedClass))] [JsonSerializable(typeof(RealWorldContextTests.MyNestedClass.MyNestedNestedClass))] @@ -70,6 +71,7 @@ public override void EnsureFastPathGeneratedAsExpected() Assert.NotNull(MetadataAndSerializationContext.Default.MyTypeWithPropertyOrdering.SerializeHandler); Assert.NotNull(MetadataAndSerializationContext.Default.MyIntermediateType.SerializeHandler); Assert.NotNull(MetadataAndSerializationContext.Default.HighLowTempsImmutable.SerializeHandler); + Assert.NotNull(MetadataAndSerializationContext.Default.HighLowTempsRecord.SerializeHandler); Assert.NotNull(MetadataAndSerializationContext.Default.MyNestedClass.SerializeHandler); Assert.NotNull(MetadataAndSerializationContext.Default.MyNestedNestedClass.SerializeHandler); Assert.Null(MetadataAndSerializationContext.Default.ObjectArray.SerializeHandler); diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/MetadataContextTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/MetadataContextTests.cs index 0f9df146d25450..6a180d517f63d4 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/MetadataContextTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/MetadataContextTests.cs @@ -21,6 +21,7 @@ namespace System.Text.Json.SourceGeneration.Tests [JsonSerializable(typeof(MyTypeWithPropertyOrdering), GenerationMode = JsonSourceGenerationMode.Metadata)] [JsonSerializable(typeof(MyIntermediateType), GenerationMode = JsonSourceGenerationMode.Metadata)] [JsonSerializable(typeof(HighLowTempsImmutable), GenerationMode = JsonSourceGenerationMode.Metadata)] + [JsonSerializable(typeof(HighLowTempsRecord), GenerationMode = JsonSourceGenerationMode.Metadata)] [JsonSerializable(typeof(RealWorldContextTests.MyNestedClass), GenerationMode = JsonSourceGenerationMode.Metadata)] [JsonSerializable(typeof(RealWorldContextTests.MyNestedClass.MyNestedNestedClass), GenerationMode = JsonSourceGenerationMode.Metadata)] [JsonSerializable(typeof(object[]), GenerationMode = JsonSourceGenerationMode.Metadata)] @@ -67,6 +68,7 @@ public override void EnsureFastPathGeneratedAsExpected() Assert.Null(MetadataWithPerTypeAttributeContext.Default.MyType2.SerializeHandler); Assert.Null(MetadataWithPerTypeAttributeContext.Default.MyIntermediateType.SerializeHandler); Assert.Null(MetadataWithPerTypeAttributeContext.Default.HighLowTempsImmutable.SerializeHandler); + Assert.Null(MetadataWithPerTypeAttributeContext.Default.HighLowTempsRecord.SerializeHandler); Assert.Null(MetadataWithPerTypeAttributeContext.Default.MyNestedClass.SerializeHandler); Assert.Null(MetadataWithPerTypeAttributeContext.Default.MyNestedNestedClass.SerializeHandler); Assert.Null(MetadataWithPerTypeAttributeContext.Default.ObjectArray.SerializeHandler); @@ -109,6 +111,7 @@ public override void EnsureFastPathGeneratedAsExpected() [JsonSerializable(typeof(MyTypeWithPropertyOrdering))] [JsonSerializable(typeof(MyIntermediateType))] [JsonSerializable(typeof(HighLowTempsImmutable))] + [JsonSerializable(typeof(HighLowTempsRecord))] [JsonSerializable(typeof(RealWorldContextTests.MyNestedClass))] [JsonSerializable(typeof(RealWorldContextTests.MyNestedClass.MyNestedNestedClass))] [JsonSerializable(typeof(object[]))] @@ -178,6 +181,7 @@ public override void EnsureFastPathGeneratedAsExpected() Assert.Null(MetadataContext.Default.MyTypeWithPropertyOrdering.SerializeHandler); Assert.Null(MetadataContext.Default.MyIntermediateType.SerializeHandler); Assert.Null(MetadataContext.Default.HighLowTempsImmutable.SerializeHandler); + Assert.Null(MetadataContext.Default.HighLowTempsRecord.SerializeHandler); Assert.Null(MetadataContext.Default.MyNestedClass.SerializeHandler); Assert.Null(MetadataContext.Default.MyNestedNestedClass.SerializeHandler); Assert.Null(MetadataContext.Default.ObjectArray.SerializeHandler); diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/MixedModeContextTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/MixedModeContextTests.cs index 202a014f2d6077..e274c9ec04372f 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/MixedModeContextTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/MixedModeContextTests.cs @@ -22,6 +22,7 @@ namespace System.Text.Json.SourceGeneration.Tests [JsonSerializable(typeof(MyTypeWithPropertyOrdering), GenerationMode = JsonSourceGenerationMode.Metadata | JsonSourceGenerationMode.Serialization)] [JsonSerializable(typeof(MyIntermediateType), GenerationMode = JsonSourceGenerationMode.Metadata | JsonSourceGenerationMode.Serialization)] [JsonSerializable(typeof(HighLowTempsImmutable), GenerationMode = JsonSourceGenerationMode.Metadata)] + [JsonSerializable(typeof(HighLowTempsRecord), GenerationMode = JsonSourceGenerationMode.Metadata)] [JsonSerializable(typeof(RealWorldContextTests.MyNestedClass), GenerationMode = JsonSourceGenerationMode.Serialization)] [JsonSerializable(typeof(RealWorldContextTests.MyNestedClass.MyNestedNestedClass), GenerationMode = JsonSourceGenerationMode.Serialization)] [JsonSerializable(typeof(object[]), GenerationMode = JsonSourceGenerationMode.Metadata)] @@ -69,6 +70,7 @@ public override void EnsureFastPathGeneratedAsExpected() Assert.NotNull(MixedModeContext.Default.MyTypeWithPropertyOrdering.SerializeHandler); Assert.NotNull(MixedModeContext.Default.MyIntermediateType.SerializeHandler); Assert.Null(MixedModeContext.Default.HighLowTempsImmutable.SerializeHandler); + Assert.Null(MixedModeContext.Default.HighLowTempsRecord.SerializeHandler); Assert.NotNull(MixedModeContext.Default.MyNestedClass.SerializeHandler); Assert.NotNull(MixedModeContext.Default.MyNestedNestedClass.SerializeHandler); Assert.Null(MixedModeContext.Default.ObjectArray.SerializeHandler); diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/RealWorldContextTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/RealWorldContextTests.cs index cfc6d486c7dd03..f9aded985c69a7 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/RealWorldContextTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/RealWorldContextTests.cs @@ -736,6 +736,18 @@ public virtual void ParameterizedConstructor() Assert.Equal(2, obj.Low); } + [Fact] + public virtual void PositionalRecord() + { + string json = JsonSerializer.Serialize(new HighLowTempsRecord(1, 2), DefaultContext.HighLowTempsRecord); + Assert.Contains(@"""High"":1", json); + Assert.Contains(@"""Low"":2", json); + + HighLowTempsRecord obj = JsonSerializer.Deserialize(json, DefaultContext.HighLowTempsRecord); + Assert.Equal(1, obj.High); + Assert.Equal(2, obj.Low); + } + [Fact] public virtual void EnumAndNullable() { diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/SerializationContextTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/SerializationContextTests.cs index b177cfff8ea117..e3cda3daa9d3b0 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/SerializationContextTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/SerializationContextTests.cs @@ -22,6 +22,7 @@ namespace System.Text.Json.SourceGeneration.Tests [JsonSerializable(typeof(MyTypeWithPropertyOrdering))] [JsonSerializable(typeof(MyIntermediateType))] [JsonSerializable(typeof(HighLowTempsImmutable))] + [JsonSerializable(typeof(HighLowTempsRecord))] [JsonSerializable(typeof(RealWorldContextTests.MyNestedClass))] [JsonSerializable(typeof(RealWorldContextTests.MyNestedClass.MyNestedNestedClass))] [JsonSerializable(typeof(object[]))] @@ -63,6 +64,7 @@ internal partial class SerializationContext : JsonSerializerContext, ITestContex [JsonSerializable(typeof(MyTypeWithPropertyOrdering), GenerationMode = JsonSourceGenerationMode.Serialization)] [JsonSerializable(typeof(MyIntermediateType), GenerationMode = JsonSourceGenerationMode.Serialization)] [JsonSerializable(typeof(HighLowTempsImmutable), GenerationMode = JsonSourceGenerationMode.Serialization)] + [JsonSerializable(typeof(HighLowTempsRecord), GenerationMode = JsonSourceGenerationMode.Serialization)] [JsonSerializable(typeof(RealWorldContextTests.MyNestedClass), GenerationMode = JsonSourceGenerationMode.Serialization)] [JsonSerializable(typeof(RealWorldContextTests.MyNestedClass.MyNestedNestedClass), GenerationMode = JsonSourceGenerationMode.Serialization)] [JsonSerializable(typeof(object[]), GenerationMode = JsonSourceGenerationMode.Serialization)] @@ -105,6 +107,7 @@ internal partial class SerializationWithPerTypeAttributeContext : JsonSerializer [JsonSerializable(typeof(MyTypeWithPropertyOrdering), GenerationMode = JsonSourceGenerationMode.Serialization)] [JsonSerializable(typeof(MyIntermediateType), GenerationMode = JsonSourceGenerationMode.Serialization)] [JsonSerializable(typeof(HighLowTempsImmutable), GenerationMode = JsonSourceGenerationMode.Serialization)] + [JsonSerializable(typeof(HighLowTempsRecord), GenerationMode = JsonSourceGenerationMode.Serialization)] [JsonSerializable(typeof(RealWorldContextTests.MyNestedClass), GenerationMode = JsonSourceGenerationMode.Serialization)] [JsonSerializable(typeof(RealWorldContextTests.MyNestedClass.MyNestedNestedClass), GenerationMode = JsonSourceGenerationMode.Serialization)] [JsonSerializable(typeof(object[]), GenerationMode = JsonSourceGenerationMode.Serialization)] @@ -158,6 +161,7 @@ public override void EnsureFastPathGeneratedAsExpected() Assert.NotNull(SerializationContext.Default.MyTypeWithPropertyOrdering.SerializeHandler); Assert.NotNull(SerializationContext.Default.MyIntermediateType.SerializeHandler); Assert.NotNull(SerializationContext.Default.HighLowTempsImmutable.SerializeHandler); + Assert.NotNull(SerializationContext.Default.HighLowTempsRecord.SerializeHandler); Assert.NotNull(SerializationContext.Default.MyNestedClass.SerializeHandler); Assert.NotNull(SerializationContext.Default.MyNestedNestedClass.SerializeHandler); Assert.Null(SerializationContext.Default.ObjectArray.SerializeHandler); @@ -436,6 +440,16 @@ public override void ParameterizedConstructor() JsonTestHelper.AssertThrows_PropMetadataInit(() => JsonSerializer.Deserialize(json, DefaultContext.HighLowTempsImmutable), typeof(HighLowTempsImmutable)); } + [Fact] + public override void PositionalRecord() + { + string json = JsonSerializer.Serialize(new HighLowTempsRecord(1, 2), DefaultContext.HighLowTempsRecord); + Assert.Contains(@"""High"":1", json); + Assert.Contains(@"""Low"":2", json); + + JsonTestHelper.AssertThrows_PropMetadataInit(() => JsonSerializer.Deserialize(json, DefaultContext.HighLowTempsRecord), typeof(HighLowTempsRecord)); + } + [Fact] public void OnSerializeCallbacks() { @@ -482,6 +496,7 @@ public override void EnsureFastPathGeneratedAsExpected() Assert.NotNull(SerializationWithPerTypeAttributeContext.Default.MyType2.SerializeHandler); Assert.NotNull(SerializationWithPerTypeAttributeContext.Default.MyIntermediateType.SerializeHandler); Assert.NotNull(SerializationWithPerTypeAttributeContext.Default.HighLowTempsImmutable.SerializeHandler); + Assert.NotNull(SerializationWithPerTypeAttributeContext.Default.HighLowTempsRecord.SerializeHandler); Assert.NotNull(SerializationWithPerTypeAttributeContext.Default.MyNestedClass.SerializeHandler); Assert.NotNull(SerializationWithPerTypeAttributeContext.Default.MyNestedNestedClass.SerializeHandler); Assert.Null(SerializationWithPerTypeAttributeContext.Default.ObjectArray.SerializeHandler); diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/TestClasses.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/TestClasses.cs index a9124235863371..42578ddc950787 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/TestClasses.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/TestClasses.cs @@ -106,6 +106,8 @@ public class HighLowTempsImmutable public HighLowTempsImmutable(int high, int low) => (High, Low) = (high, low); } + public record HighLowTempsRecord(int High, int Low); + public class EmptyPoco { } diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Unit.Tests/CompilationHelper.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Unit.Tests/CompilationHelper.cs index 485a79cd333cd6..332b8123bcb43d 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Unit.Tests/CompilationHelper.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Unit.Tests/CompilationHelper.cs @@ -274,7 +274,7 @@ public class Location return CreateCompilation(source); } - + public static Compilation CreateCompilationWithInitOnlyProperties() { string source = @" @@ -305,6 +305,91 @@ public partial class MyJsonContext : JsonSerializerContext return CreateCompilation(source); } + public static Compilation CreateCompilationWithConstructorInitOnlyProperties() + { + string source = @" + using System; + using System.Text.Json.Serialization; + + namespace HelloWorld + { + public class MyClass + { + public MyClass(int value) + { + Value = value; + } + + public int Value { get; init; } + } + + [JsonSerializable(typeof(MyClass))] + public partial class MyJsonContext : JsonSerializerContext + { + } + }"; + + return CreateCompilation(source); + } + + public static Compilation CreateCompilationWithMixedInitOnlyProperties() + { + string source = @" + using System; + using System.Text.Json.Serialization; + + namespace HelloWorld + { + public class MyClass + { + public MyClass(int value) + { + Value = value; + } + + public int Value { get; init; } + public string Orphaned { get; init; } + } + + [JsonSerializable(typeof(MyClass))] + public partial class MyJsonContext : JsonSerializerContext + { + } + }"; + + return CreateCompilation(source); + } + + public static Compilation CreateCompilationWithRecordPositionalParameters() + { + string source = @" + using System; + using System.Text.Json.Serialization; + + namespace HelloWorld + { + public record Location + ( + int Id, + string Address1, + string Address2, + string City, + string State, + string PostalCode, + string Name, + string PhoneNumber, + string Country + ); + + [JsonSerializable(typeof(Location))] + public partial class MyJsonContext : JsonSerializerContext + { + } + }"; + + return CreateCompilation(source); + } + public static Compilation CreateCompilationWithInaccessibleJsonIncludeProperties() { string source = @" diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Unit.Tests/JsonSourceGeneratorDiagnosticsTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Unit.Tests/JsonSourceGeneratorDiagnosticsTests.cs index d3378b84f2f83a..77f76fa92c4d80 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Unit.Tests/JsonSourceGeneratorDiagnosticsTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Unit.Tests/JsonSourceGeneratorDiagnosticsTests.cs @@ -236,6 +236,52 @@ public void WarnOnClassesWithInitOnlyProperties() CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Error, generatorDiags, Array.Empty<(Location, string)>()); } + [Fact] + [ActiveIssue("https://github.com/dotnet/runtime/issues/58770", TestPlatforms.Browser)] + public void DoNotWarnOnClassesWithConstructorInitOnlyProperties() + { + Compilation compilation = CompilationHelper.CreateCompilationWithConstructorInitOnlyProperties(); + JsonSourceGenerator generator = new JsonSourceGenerator(); + CompilationHelper.RunGenerators(compilation, out var generatorDiags, generator); + + CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Info, generatorDiags, Array.Empty<(Location, string)>()); + CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Warning, generatorDiags, Array.Empty<(Location, string)>()); + CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Error, generatorDiags, Array.Empty<(Location, string)>()); + } + + [Fact] + [ActiveIssue("https://github.com/dotnet/runtime/issues/58770", TestPlatforms.Browser)] + public void WarnOnClassesWithMixedInitOnlyProperties() + { + Compilation compilation = CompilationHelper.CreateCompilationWithMixedInitOnlyProperties(); + JsonSourceGenerator generator = new JsonSourceGenerator(); + CompilationHelper.RunGenerators(compilation, out var generatorDiags, generator); + + Location location = compilation.GetSymbolsWithName("Orphaned").First().Locations[0]; + + (Location, string)[] expectedWarningDiagnostics = new (Location, string)[] + { + (location, "The type 'MyClass' defines init-only properties, deserialization of which is currently not supported in source generation mode.") + }; + + CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Info, generatorDiags, Array.Empty<(Location, string)>()); + CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Warning, generatorDiags, expectedWarningDiagnostics); + CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Error, generatorDiags, Array.Empty<(Location, string)>()); + } + + [Fact] + [ActiveIssue("https://github.com/dotnet/runtime/issues/58770", TestPlatforms.Browser)] + public void DoNotWarnOnRecordsWithInitOnlyPositionalParameters() + { + Compilation compilation = CompilationHelper.CreateCompilationWithRecordPositionalParameters(); + JsonSourceGenerator generator = new JsonSourceGenerator(); + CompilationHelper.RunGenerators(compilation, out var generatorDiags, generator); + + CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Info, generatorDiags, Array.Empty<(Location, string)>()); + CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Warning, generatorDiags, Array.Empty<(Location, string)>()); + CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Error, generatorDiags, Array.Empty<(Location, string)>()); + } + [Fact] [ActiveIssue("https://github.com/dotnet/runtime/issues/58226", TestPlatforms.Browser)] public void WarnOnClassesWithInaccessibleJsonIncludeProperties()