From 7414890450c4aac4cc0402a306d1e6aa6bc6d53b Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Wed, 29 Sep 2021 16:24:19 +0100 Subject: [PATCH 1/3] Fix NRT warnings in source generated code --- .../gen/JsonSourceGenerator.Emitter.cs | 54 +++++++++---------- .../gen/JsonSourceGenerator.Parser.cs | 2 + .../gen/Reflection/TypeExtensions.cs | 24 +++++++++ .../gen/TypeGenerationSpec.cs | 7 +++ .../ContextClasses.cs | 1 + .../MetadataAndSerializationContextTests.cs | 2 + .../MetadataContextTests.cs | 4 ++ .../MixedModeContextTests.cs | 2 + .../RealWorldContextTests.cs | 54 +++++++++++++++++++ .../SerializationContextTests.cs | 38 +++++++++++++ 10 files changed, 159 insertions(+), 29 deletions(-) diff --git a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs index 217aed6f271c40..c17eb09a805e7a 100644 --- a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs +++ b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs @@ -559,7 +559,7 @@ private string GenerateFastPathFuncForEnumerable(TypeGenerationSpec typeGenerati } string elementSerializationLogic = writerMethodToCall == null - ? GetSerializeLogicForNonPrimitiveType(valueTypeGenerationSpec.TypeInfoPropertyName, valueToWrite, valueTypeGenerationSpec.GenerateSerializationLogic) + ? GetSerializeLogicForNonPrimitiveType(valueTypeGenerationSpec, valueToWrite) : $"{writerMethodToCall}Value({valueToWrite});"; string serializationLogic = $@"{WriterVarName}.WriteStartArray(); @@ -571,11 +571,7 @@ private string GenerateFastPathFuncForEnumerable(TypeGenerationSpec typeGenerati {WriterVarName}.WriteEndArray();"; - return GenerateFastPathFuncForType( - $"{typeGenerationSpec.TypeInfoPropertyName}{SerializeHandlerPropName}", - typeGenerationSpec.TypeRef, - serializationLogic, - typeGenerationSpec.CanBeNull); + return GenerateFastPathFuncForType(typeGenerationSpec, serializationLogic, emitNullCheck: typeGenerationSpec.CanBeNull); } private string GenerateFastPathFuncForDictionary(TypeGenerationSpec typeGenerationSpec) @@ -603,7 +599,7 @@ private string GenerateFastPathFuncForDictionary(TypeGenerationSpec typeGenerati else { elementSerializationLogic = $@"{WriterVarName}.WritePropertyName({keyToWrite}); - {GetSerializeLogicForNonPrimitiveType(valueTypeGenerationSpec.TypeInfoPropertyName, valueToWrite, valueTypeGenerationSpec.GenerateSerializationLogic)}"; + {GetSerializeLogicForNonPrimitiveType(valueTypeGenerationSpec, valueToWrite)}"; } string serializationLogic = $@"{WriterVarName}.WriteStartObject(); @@ -615,11 +611,7 @@ private string GenerateFastPathFuncForDictionary(TypeGenerationSpec typeGenerati {WriterVarName}.WriteEndObject();"; - return GenerateFastPathFuncForType( - $"{typeGenerationSpec.TypeInfoPropertyName}{SerializeHandlerPropName}", - typeGenerationSpec.TypeRef, - serializationLogic, - typeGenerationSpec.CanBeNull); + return GenerateFastPathFuncForType(typeGenerationSpec, serializationLogic, emitNullCheck: typeGenerationSpec.CanBeNull); } private string GenerateForObject(TypeGenerationSpec typeMetadata) @@ -729,7 +721,7 @@ private string GeneratePropMetadataInitFunc(TypeGenerationSpec typeGenerationSpe string getterValue = memberMetadata switch { { DefaultIgnoreCondition: JsonIgnoreCondition.Always } => "null", - { CanUseGetter: true } => $"static (obj) => (({declaringTypeCompilableName})obj).{clrPropertyName}", + { CanUseGetter: true } => $"static (obj) => (({declaringTypeCompilableName})obj).{clrPropertyName}{(memberMetadata.TypeGenerationSpec.CanContainNullableReferenceAnnotations ? "!" : "")}", { CanUseGetter: false, HasJsonInclude: true } => @$"static (obj) => throw new {InvalidOperationExceptionTypeRef}(""{string.Format(ExceptionMessages.InaccessibleJsonIncludePropertiesNotSupported, typeGenerationSpec.Type.Name, memberMetadata.ClrName)}"")", _ => "null" @@ -836,7 +828,6 @@ private string GenerateFastPathFuncForObject(TypeGenerationSpec typeGenSpec) { JsonSourceGenerationOptionsAttribute options = _currentContext.GenerationOptions; string typeRef = typeGenSpec.TypeRef; - string serializeMethodName = $"{typeGenSpec.TypeInfoPropertyName}{SerializeHandlerPropName}"; if (!typeGenSpec.TryFilterSerializableProps( options, @@ -845,11 +836,9 @@ private string GenerateFastPathFuncForObject(TypeGenerationSpec typeGenSpec) { string exceptionMessage = string.Format(ExceptionMessages.InvalidSerializablePropertyConfiguration, typeRef); - return GenerateFastPathFuncForType( - serializeMethodName, - typeRef, + return GenerateFastPathFuncForType(typeGenSpec, $@"throw new {InvalidOperationExceptionTypeRef}(""{exceptionMessage}"");", - canBeNull: false); // Skip null check since we want to throw an exception straightaway. + emitNullCheck: false); // Skip null check since we want to throw an exception straightaway. } StringBuilder sb = new(); @@ -905,7 +894,7 @@ private string GenerateFastPathFuncForObject(TypeGenerationSpec typeGenSpec) { serializationLogic = $@" {WriterVarName}.WritePropertyName({propVarName}); - {GetSerializeLogicForNonPrimitiveType(propertyTypeSpec.TypeInfoPropertyName, propValue, propertyTypeSpec.GenerateSerializationLogic)}"; + {GetSerializeLogicForNonPrimitiveType(propertyTypeSpec, propValue)}"; } JsonIgnoreCondition ignoreCondition = propertyGenSpec.DefaultIgnoreCondition ?? options.DefaultIgnoreCondition; @@ -939,7 +928,7 @@ private string GenerateFastPathFuncForObject(TypeGenerationSpec typeGenSpec) sb.Append($@"((global::{JsonConstants.IJsonOnSerializedFullName}){ValueVarName}).OnSerialized();"); }; - return GenerateFastPathFuncForType(serializeMethodName, typeRef, sb.ToString(), typeGenSpec.CanBeNull); + return GenerateFastPathFuncForType(typeGenSpec, sb.ToString(), emitNullCheck: typeGenSpec.CanBeNull); } private static bool ShouldIncludePropertyForFastPath(PropertyGenerationSpec propertyGenSpec, JsonSourceGenerationOptionsAttribute options) @@ -1039,14 +1028,20 @@ static string GetParamUnboxing(ParameterGenerationSpec spec, int index) return method; } - private string GenerateFastPathFuncForType(string serializeMethodName, string typeInfoTypeRef, string serializationLogic, bool canBeNull) + private string GenerateFastPathFuncForType(TypeGenerationSpec typeGenSpec, string serializeMethodBody, bool emitNullCheck) { + Debug.Assert(!emitNullCheck || typeGenSpec.CanBeNull); + + string serializeMethodName = $"{typeGenSpec.TypeInfoPropertyName}{SerializeHandlerPropName}"; + // fast path serializers for reference types always support null inputs. + string valueTypeRef = $"{typeGenSpec.TypeRef}{(typeGenSpec.IsValueType ? "" : "?")}"; + return $@" -private static void {serializeMethodName}({Utf8JsonWriterTypeRef} {WriterVarName}, {typeInfoTypeRef} {ValueVarName}) +private static void {serializeMethodName}({Utf8JsonWriterTypeRef} {WriterVarName}, {valueTypeRef} {ValueVarName}) {{ - {GetEarlyNullCheckSource(canBeNull)} - {serializationLogic} + {GetEarlyNullCheckSource(emitNullCheck)} + {serializeMethodBody} }}"; } @@ -1062,16 +1057,17 @@ private string GetEarlyNullCheckSource(bool canBeNull) : null; } - private string GetSerializeLogicForNonPrimitiveType(string typeInfoPropertyName, string valueToWrite, bool serializationLogicGenerated) + private string GetSerializeLogicForNonPrimitiveType(TypeGenerationSpec typeGenerationSpec, string valueExpr) { - string typeInfoRef = $"{_currentContext.ContextTypeRef}.Default.{typeInfoPropertyName}"; + string valueExprSuffix = typeGenerationSpec.CanContainNullableReferenceAnnotations ? "!" : ""; - if (serializationLogicGenerated) + if (typeGenerationSpec.GenerateSerializationLogic) { - return $"{typeInfoPropertyName}{SerializeHandlerPropName}({WriterVarName}, {valueToWrite});"; + return $"{typeGenerationSpec.TypeInfoPropertyName}{SerializeHandlerPropName}({WriterVarName}, {valueExpr}{valueExprSuffix});"; } - return $"{JsonSerializerTypeRef}.Serialize({WriterVarName}, {valueToWrite}, {typeInfoRef});"; + string typeInfoRef = $"{_currentContext.ContextTypeRef}.Default.{typeGenerationSpec.TypeInfoPropertyName}!"; + return $"{JsonSerializerTypeRef}.Serialize({WriterVarName}, {valueExpr}{valueExprSuffix}, {typeInfoRef});"; } private enum DefaultCheckType diff --git a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs index e985f1c3c38a7e..ae38bf43c3b497 100644 --- a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs +++ b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs @@ -645,6 +645,7 @@ private TypeGenerationSpec GetOrAddTypeGenerationSpec(Type type, JsonSourceGener bool hasInitOnlyProperties = false; bool hasTypeFactoryConverter = false; bool hasPropertyFactoryConverters = false; + bool canContainNullableReferenceAnnotations = type.CanContainNullableReferenceTypeAnnotations(); IList attributeDataList = CustomAttributeData.GetCustomAttributes(type); foreach (CustomAttributeData attributeData in attributeDataList) @@ -1011,6 +1012,7 @@ void CacheMemberHelper() converterInstatiationLogic, implementsIJsonOnSerialized : implementsIJsonOnSerialized, implementsIJsonOnSerializing : implementsIJsonOnSerializing, + canContainNullableReferenceAnnotations: canContainNullableReferenceAnnotations, hasTypeFactoryConverter : hasTypeFactoryConverter, hasPropertyFactoryConverters : hasPropertyFactoryConverters); diff --git a/src/libraries/System.Text.Json/gen/Reflection/TypeExtensions.cs b/src/libraries/System.Text.Json/gen/Reflection/TypeExtensions.cs index ba20339565cbf9..68e269f990c30d 100644 --- a/src/libraries/System.Text.Json/gen/Reflection/TypeExtensions.cs +++ b/src/libraries/System.Text.Json/gen/Reflection/TypeExtensions.cs @@ -109,6 +109,30 @@ public static bool IsNullableValueType(this Type type, out Type? underlyingType) return false; } + public static bool CanContainNullableReferenceTypeAnnotations(this Type type) + { + // Returns true iff Type instance has potential for receiving nullable reference type annotations, + // i.e. the type is a reference type or contains generic parameters that are reference types. + + if (!type.IsValueType) + { + return true; + } + + if (type.IsGenericType) + { + foreach (var genericParam in type.GetGenericArguments()) + { + if (CanContainNullableReferenceTypeAnnotations(genericParam)) + { + return true; + } + } + } + + return false; + } + public static bool IsObjectType(this Type type) => type.FullName == "System.Object"; public static bool IsStringType(this Type type) => type.FullName == "System.String"; diff --git a/src/libraries/System.Text.Json/gen/TypeGenerationSpec.cs b/src/libraries/System.Text.Json/gen/TypeGenerationSpec.cs index 1b83543694361a..2e544454c5234e 100644 --- a/src/libraries/System.Text.Json/gen/TypeGenerationSpec.cs +++ b/src/libraries/System.Text.Json/gen/TypeGenerationSpec.cs @@ -72,6 +72,11 @@ internal class TypeGenerationSpec public bool HasPropertyFactoryConverters { get; private set; } public bool HasTypeFactoryConverter { get; private set; } + // The spec is derived from cached `System.Type` instances, which are generally annotation-agnostic. + // Hence we can only record the potential for nullable annotations being possible for the runtime type. + // TODO: consider deriving the generation spec from the Roslyn symbols directly. + public bool CanContainNullableReferenceAnnotations { get; private set; } + public string? ImmutableCollectionBuilderName { get @@ -114,6 +119,7 @@ public void Initialize( bool implementsIJsonOnSerialized, bool implementsIJsonOnSerializing, bool hasTypeFactoryConverter, + bool canContainNullableReferenceAnnotations, bool hasPropertyFactoryConverters) { GenerationMode = generationMode; @@ -136,6 +142,7 @@ public void Initialize( ConverterInstantiationLogic = converterInstantiationLogic; ImplementsIJsonOnSerialized = implementsIJsonOnSerialized; ImplementsIJsonOnSerializing = implementsIJsonOnSerializing; + CanContainNullableReferenceAnnotations = canContainNullableReferenceAnnotations; HasTypeFactoryConverter = hasTypeFactoryConverter; HasPropertyFactoryConverters = hasPropertyFactoryConverters; } 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 682323f2e4a4be..ef79671ba640ea 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 @@ -34,6 +34,7 @@ public interface ITestContext public JsonTypeInfo String { get; } public JsonTypeInfo<(string Label1, int Label2, bool)> ValueTupleStringInt32Boolean { get; } public JsonTypeInfo ClassWithEnumAndNullable { get; } + public JsonTypeInfo ClassWithNullableProperties { get; } public JsonTypeInfo ClassWithCustomConverter { get; } public JsonTypeInfo StructWithCustomConverter { get; } public JsonTypeInfo ClassWithCustomConverterFactory { 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 e204e89f6c392a..7e83f1ae37feaa 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 @@ -28,6 +28,7 @@ namespace System.Text.Json.SourceGeneration.Tests [JsonSerializable(typeof(string))] [JsonSerializable(typeof((string Label1, int Label2, bool)))] [JsonSerializable(typeof(RealWorldContextTests.ClassWithEnumAndNullable))] + [JsonSerializable(typeof(RealWorldContextTests.ClassWithNullableProperties))] [JsonSerializable(typeof(ClassWithCustomConverter))] [JsonSerializable(typeof(StructWithCustomConverter))] [JsonSerializable(typeof(ClassWithCustomConverterFactory))] @@ -72,6 +73,7 @@ public override void EnsureFastPathGeneratedAsExpected() Assert.Null(MetadataAndSerializationContext.Default.String.SerializeHandler); Assert.NotNull(MetadataAndSerializationContext.Default.ValueTupleStringInt32Boolean.SerializeHandler); Assert.NotNull(MetadataAndSerializationContext.Default.ClassWithEnumAndNullable.SerializeHandler); + Assert.NotNull(MetadataAndSerializationContext.Default.ClassWithNullableProperties.SerializeHandler); Assert.NotNull(MetadataAndSerializationContext.Default.ClassWithCustomConverter); Assert.NotNull(MetadataAndSerializationContext.Default.StructWithCustomConverter); Assert.NotNull(MetadataAndSerializationContext.Default.ClassWithCustomConverterFactory); 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 3b9a83751da611..a9932ede2bf80b 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 @@ -27,6 +27,7 @@ namespace System.Text.Json.SourceGeneration.Tests [JsonSerializable(typeof(string), GenerationMode = JsonSourceGenerationMode.Metadata)] [JsonSerializable(typeof((string Label1, int Label2, bool)), GenerationMode = JsonSourceGenerationMode.Metadata)] [JsonSerializable(typeof(RealWorldContextTests.ClassWithEnumAndNullable), GenerationMode = JsonSourceGenerationMode.Metadata)] + [JsonSerializable(typeof(RealWorldContextTests.ClassWithNullableProperties), GenerationMode = JsonSourceGenerationMode.Metadata)] [JsonSerializable(typeof(ClassWithCustomConverter), GenerationMode = JsonSourceGenerationMode.Metadata)] [JsonSerializable(typeof(StructWithCustomConverter), GenerationMode = JsonSourceGenerationMode.Metadata)] [JsonSerializable(typeof(ClassWithCustomConverterFactory), GenerationMode = JsonSourceGenerationMode.Metadata)] @@ -69,6 +70,7 @@ public override void EnsureFastPathGeneratedAsExpected() Assert.Null(MetadataWithPerTypeAttributeContext.Default.String.SerializeHandler); Assert.Null(MetadataWithPerTypeAttributeContext.Default.ValueTupleStringInt32Boolean.SerializeHandler); Assert.Null(MetadataWithPerTypeAttributeContext.Default.ClassWithEnumAndNullable.SerializeHandler); + Assert.Null(MetadataWithPerTypeAttributeContext.Default.ClassWithNullableProperties.SerializeHandler); Assert.Null(MetadataWithPerTypeAttributeContext.Default.ClassWithCustomConverter.SerializeHandler); Assert.Null(MetadataWithPerTypeAttributeContext.Default.StructWithCustomConverter.SerializeHandler); Assert.Null(MetadataWithPerTypeAttributeContext.Default.ClassWithCustomConverterFactory.SerializeHandler); @@ -104,6 +106,7 @@ public override void EnsureFastPathGeneratedAsExpected() [JsonSerializable(typeof(string))] [JsonSerializable(typeof((string Label1, int Label2, bool)))] [JsonSerializable(typeof(RealWorldContextTests.ClassWithEnumAndNullable))] + [JsonSerializable(typeof(RealWorldContextTests.ClassWithNullableProperties))] [JsonSerializable(typeof(ClassWithCustomConverter))] [JsonSerializable(typeof(StructWithCustomConverter))] [JsonSerializable(typeof(ClassWithCustomConverterFactory))] @@ -169,6 +172,7 @@ public override void EnsureFastPathGeneratedAsExpected() Assert.Null(MetadataContext.Default.String.SerializeHandler); Assert.Null(MetadataContext.Default.ValueTupleStringInt32Boolean.SerializeHandler); Assert.Null(MetadataContext.Default.ClassWithEnumAndNullable.SerializeHandler); + Assert.Null(MetadataContext.Default.ClassWithNullableProperties.SerializeHandler); Assert.Null(MetadataContext.Default.ClassWithCustomConverter.SerializeHandler); Assert.Null(MetadataContext.Default.StructWithCustomConverter.SerializeHandler); Assert.Null(MetadataContext.Default.ClassWithCustomConverterFactory.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 ede23fcf3a6e8b..0cb5cf13d5e9b3 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 @@ -28,6 +28,7 @@ namespace System.Text.Json.SourceGeneration.Tests [JsonSerializable(typeof(string), GenerationMode = JsonSourceGenerationMode.Metadata | JsonSourceGenerationMode.Serialization)] [JsonSerializable(typeof((string Label1, int Label2, bool)), GenerationMode = JsonSourceGenerationMode.Metadata | JsonSourceGenerationMode.Serialization)] [JsonSerializable(typeof(RealWorldContextTests.ClassWithEnumAndNullable), GenerationMode = JsonSourceGenerationMode.Metadata | JsonSourceGenerationMode.Serialization)] + [JsonSerializable(typeof(RealWorldContextTests.ClassWithNullableProperties), GenerationMode = JsonSourceGenerationMode.Metadata | JsonSourceGenerationMode.Serialization)] [JsonSerializable(typeof(ClassWithCustomConverter), GenerationMode = JsonSourceGenerationMode.Metadata | JsonSourceGenerationMode.Serialization)] [JsonSerializable(typeof(StructWithCustomConverter), GenerationMode = JsonSourceGenerationMode.Metadata | JsonSourceGenerationMode.Serialization)] [JsonSerializable(typeof(ClassWithCustomConverterFactory), GenerationMode = JsonSourceGenerationMode.Metadata | JsonSourceGenerationMode.Serialization)] @@ -71,6 +72,7 @@ public override void EnsureFastPathGeneratedAsExpected() Assert.Null(MixedModeContext.Default.String.SerializeHandler); Assert.NotNull(MixedModeContext.Default.ValueTupleStringInt32Boolean.SerializeHandler); Assert.NotNull(MixedModeContext.Default.ClassWithEnumAndNullable.SerializeHandler); + Assert.NotNull(MixedModeContext.Default.ClassWithNullableProperties.SerializeHandler); Assert.Null(MixedModeContext.Default.ClassWithCustomConverter.SerializeHandler); Assert.Null(MixedModeContext.Default.StructWithCustomConverter.SerializeHandler); Assert.Null(MixedModeContext.Default.ClassWithCustomConverterFactory.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 520be63598f441..dfdc657e051141 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 @@ -734,6 +734,60 @@ public class ClassWithEnumAndNullable public DayOfWeek? NullableDay { get; set; } } + [Fact] + public virtual void ClassWithNullableProperties_Roundtrip() + { + RunTest(new ClassWithNullableProperties + { + Uri = new Uri("http://contoso.com"), + Array = new int[] { 42 }, + Poco = new ClassWithNullableProperties.MyPoco(), + + NullableUri = new Uri("http://contoso.com"), + NullableArray = new int[] { 42 }, + NullablePoco = new ClassWithNullableProperties.MyPoco() + }); + + RunTest(new ClassWithNullableProperties()); + + void RunTest(ClassWithNullableProperties expected) + { + string json = JsonSerializer.Serialize(expected, DefaultContext.ClassWithNullableProperties); + ClassWithNullableProperties actual = JsonSerializer.Deserialize(json, DefaultContext.ClassWithNullableProperties); + + Assert.Equal(expected.Uri, actual.Uri); + Assert.Equal(expected.Array, actual.Array); + Assert.Equal(expected.Poco, actual.Poco); + + Assert.Equal(expected.NullableUri, actual.NullableUri); + Assert.Equal(expected.NullableArray, actual.NullableArray); + Assert.Equal(expected.NullablePoco, actual.NullablePoco); + + Assert.Equal(expected.NullableUriParameter, actual.NullableUriParameter); + Assert.Equal(expected.NullableArrayParameter, actual.NullableArrayParameter); + Assert.Equal(expected.NullablePocoParameter, actual.NullablePocoParameter); + } + } + + public class ClassWithNullableProperties + { + public Uri Uri { get; set; } + public int[] Array { get; set; } + public MyPoco Poco { get; set; } + + public Uri? NullableUri { get; set; } + public int[]? NullableArray { get; set; } + public MyPoco? NullablePoco { get; set; } + + // struct types containing nullable reference types as generic parameters + public GenericStruct NullableUriParameter { get; set; } + public GenericStruct NullableArrayParameter { get; set; } + public GenericStruct NullablePocoParameter { get; set; } + + public record MyPoco { } + public struct GenericStruct { } + } + private const string ExceptionMessageFromCustomContext = "Exception thrown from custom context."; [Fact] 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 5f4b0dc8362372..0c9079a259e39c 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 @@ -28,6 +28,7 @@ namespace System.Text.Json.SourceGeneration.Tests [JsonSerializable(typeof(string))] [JsonSerializable(typeof((string Label1, int Label2, bool)))] [JsonSerializable(typeof(RealWorldContextTests.ClassWithEnumAndNullable))] + [JsonSerializable(typeof(RealWorldContextTests.ClassWithNullableProperties))] [JsonSerializable(typeof(ClassWithCustomConverter))] [JsonSerializable(typeof(StructWithCustomConverter))] [JsonSerializable(typeof(ClassWithCustomConverterFactory))] @@ -64,6 +65,7 @@ internal partial class SerializationContext : JsonSerializerContext, ITestContex [JsonSerializable(typeof(string), GenerationMode = JsonSourceGenerationMode.Serialization)] [JsonSerializable(typeof((string Label1, int Label2, bool)), GenerationMode = JsonSourceGenerationMode.Serialization)] [JsonSerializable(typeof(RealWorldContextTests.ClassWithEnumAndNullable), GenerationMode = JsonSourceGenerationMode.Serialization)] + [JsonSerializable(typeof(RealWorldContextTests.ClassWithNullableProperties), GenerationMode = JsonSourceGenerationMode.Serialization)] [JsonSerializable(typeof(ClassWithCustomConverter), GenerationMode = JsonSourceGenerationMode.Serialization)] [JsonSerializable(typeof(StructWithCustomConverter), GenerationMode = JsonSourceGenerationMode.Serialization)] [JsonSerializable(typeof(ClassWithCustomConverterFactory), GenerationMode = JsonSourceGenerationMode.Serialization)] @@ -101,6 +103,7 @@ internal partial class SerializationWithPerTypeAttributeContext : JsonSerializer [JsonSerializable(typeof(string), GenerationMode = JsonSourceGenerationMode.Serialization)] [JsonSerializable(typeof((string Label1, int Label2, bool)), GenerationMode = JsonSourceGenerationMode.Serialization)] [JsonSerializable(typeof(RealWorldContextTests.ClassWithEnumAndNullable), GenerationMode = JsonSourceGenerationMode.Serialization)] + [JsonSerializable(typeof(RealWorldContextTests.ClassWithNullableProperties), GenerationMode = JsonSourceGenerationMode.Serialization)] [JsonSerializable(typeof(ClassWithCustomConverter), GenerationMode = JsonSourceGenerationMode.Serialization)] [JsonSerializable(typeof(StructWithCustomConverter), GenerationMode = JsonSourceGenerationMode.Serialization)] [JsonSerializable(typeof(ClassWithCustomConverterFactory), GenerationMode = JsonSourceGenerationMode.Serialization)] @@ -369,6 +372,41 @@ void RunTest(ClassWithEnumAndNullable expected) } } + [Fact] + public override void ClassWithNullableProperties_Roundtrip() + { + RunTest(new ClassWithNullableProperties + { + Uri = new Uri("http://contoso.com"), + Array = new int[] { 42 }, + Poco = new ClassWithNullableProperties.MyPoco(), + + NullableUri = new Uri("http://contoso.com"), + NullableArray = new int[] { 42 }, + NullablePoco = new ClassWithNullableProperties.MyPoco() + }); + + RunTest(new ClassWithNullableProperties()); + + void RunTest(ClassWithNullableProperties expected) + { + string json = JsonSerializer.Serialize(expected, DefaultContext.ClassWithNullableProperties); + ClassWithNullableProperties actual = JsonSerializer.Deserialize(json, ((ITestContext)MetadataWithPerTypeAttributeContext.Default).ClassWithNullableProperties); + + Assert.Equal(expected.Uri, actual.Uri); + Assert.Equal(expected.Array, actual.Array); + Assert.Equal(expected.Poco, actual.Poco); + + Assert.Equal(expected.NullableUri, actual.NullableUri); + Assert.Equal(expected.NullableArray, actual.NullableArray); + Assert.Equal(expected.NullablePoco, actual.NullablePoco); + + Assert.Equal(expected.NullableUriParameter, actual.NullableUriParameter); + Assert.Equal(expected.NullableArrayParameter, actual.NullableArrayParameter); + Assert.Equal(expected.NullablePocoParameter, actual.NullablePocoParameter); + } + } + [Fact] public override void ParameterizedConstructor() { From 51629154d7d62ba3e4f5d31d5af534412ac4c025 Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Wed, 29 Sep 2021 17:54:14 +0100 Subject: [PATCH 2/3] address feedback --- .../RealWorldContextTests.cs | 2 ++ 1 file changed, 2 insertions(+) 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 dfdc657e051141..8348e7c0df8e4e 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 @@ -784,6 +784,8 @@ public class ClassWithNullableProperties public GenericStruct NullableArrayParameter { get; set; } public GenericStruct NullablePocoParameter { get; set; } + public (string? x, int y)? NullableArgumentOfNullableStruct { get; set; } + public record MyPoco { } public struct GenericStruct { } } From c2f7e57e22c4b07888df6a3d0138145aa3ebb383 Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Thu, 30 Sep 2021 09:42:07 +0100 Subject: [PATCH 3/3] address style --- src/libraries/System.Text.Json/gen/Reflection/TypeExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libraries/System.Text.Json/gen/Reflection/TypeExtensions.cs b/src/libraries/System.Text.Json/gen/Reflection/TypeExtensions.cs index 68e269f990c30d..b5b02c4f7d6556 100644 --- a/src/libraries/System.Text.Json/gen/Reflection/TypeExtensions.cs +++ b/src/libraries/System.Text.Json/gen/Reflection/TypeExtensions.cs @@ -121,7 +121,7 @@ public static bool CanContainNullableReferenceTypeAnnotations(this Type type) if (type.IsGenericType) { - foreach (var genericParam in type.GetGenericArguments()) + foreach (Type genericParam in type.GetGenericArguments()) { if (CanContainNullableReferenceTypeAnnotations(genericParam)) {