From 96102333b20331437ca037441803c758fd502b14 Mon Sep 17 00:00:00 2001 From: Sychev Vadim Date: Wed, 13 Oct 2021 23:26:29 +0300 Subject: [PATCH] Src-Gen serialization for nullable structs (#59719) --- .../gen/JsonSourceGenerator.Emitter.cs | 59 ++++++++++++++++++- .../gen/TypeGenerationSpec.cs | 9 +++ .../System.Text.Json/ref/System.Text.Json.cs | 2 +- .../Metadata/JsonMetadataServices.cs | 2 +- .../ContextClasses.cs | 2 + .../MetadataAndSerializationContextTests.cs | 22 +++++++ .../MetadataContextTests.cs | 29 ++++++++- .../MixedModeContextTests.cs | 22 +++++++ .../SerializationContextTests.cs | 29 +++++++++ .../TestClasses.cs | 18 ++++++ 10 files changed, 190 insertions(+), 4 deletions(-) diff --git a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs index c17eb09a805e7a..2a82f12e34a230 100644 --- a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs +++ b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs @@ -357,6 +357,56 @@ private string GenerateForNullable(TypeGenerationSpec typeMetadata) TypeGenerationSpec? underlyingTypeMetadata = typeMetadata.NullableUnderlyingTypeMetadata; Debug.Assert(underlyingTypeMetadata != null); + + ObjectConstructionStrategy constructionStrategy = underlyingTypeMetadata.ConstructionStrategy; + + string? propMetadataInitFuncSource = null; + string? ctorParamMetadataInitFuncSource = null; + + string propInitMethodName = "null"; + string ctorParamMetadataInitMethodName = "null"; + + + if (typeMetadata.GenerateSerializationLogic) + { + string? serializeFuncSource = GenerateFastPathFuncForNullableStruct(typeMetadata); + string serializeMethodName = $"{typeFriendlyName}{SerializeHandlerPropName}"; + + const string ObjectInfoVarName = "objectInfo"; + + string genericArg = typeMetadata.TypeRef; + + string creatorInvocation = constructionStrategy == ObjectConstructionStrategy.ParameterlessConstructor + ? $"static () => new {underlyingTypeMetadata.TypeRef}()" + : "null"; + + string parameterizedCreatorInvocation = constructionStrategy == ObjectConstructionStrategy.ParameterizedConstructor + ? GetParameterizedCtorInvocationFunc(typeMetadata.NullableUnderlyingTypeMetadata) + : "null"; + + if (typeMetadata.GenerateMetadata) + { + propInitMethodName = $"{underlyingTypeMetadata.TypeInfoPropertyName}{PropInitMethodNameSuffix}"; + + } + + string objectInfoInitSource = $@"{JsonObjectInfoValuesTypeRef}<{genericArg}> {ObjectInfoVarName} = new {JsonObjectInfoValuesTypeRef}<{genericArg}>() + {{ + {ObjectCreatorPropName} = {creatorInvocation}, + ObjectWithParameterizedConstructorCreator = {parameterizedCreatorInvocation}, + PropertyMetadataInitializer = {propInitMethodName}, + ConstructorParameterMetadataInitializer = {ctorParamMetadataInitMethodName}, + {NumberHandlingPropName} = {GetNumberHandlingAsStr(typeMetadata.NumberHandling)}, + {SerializeHandlerPropName} = {serializeMethodName} + }}; + + _{typeFriendlyName} = {JsonMetadataServicesTypeRef}.CreateObjectInfo<{typeCompilableName}>({OptionsInstanceVariableName}, {ObjectInfoVarName});"; + + string additionalSource = @$"{propMetadataInitFuncSource}{serializeFuncSource}{ctorParamMetadataInitFuncSource}"; + + return GenerateForType(typeMetadata, objectInfoInitSource, additionalSource); + } + string underlyingTypeCompilableName = underlyingTypeMetadata.TypeRef; string underlyingTypeFriendlyName = underlyingTypeMetadata.TypeInfoPropertyName; string underlyingTypeInfoNamedArg = underlyingTypeMetadata.ClassType == ClassType.TypeUnsupportedBySourceGen @@ -672,7 +722,7 @@ private string GenerateForObject(TypeGenerationSpec typeMetadata) return GenerateForType(typeMetadata, objectInfoInitSource, additionalSource); } - + private string GeneratePropMetadataInitFunc(TypeGenerationSpec typeGenerationSpec) { const string PropVarName = "properties"; @@ -824,6 +874,13 @@ private string GenerateCtorParamMetadataInitFunc(TypeGenerationSpec typeGenerati return sb.ToString(); } + private string GenerateFastPathFuncForNullableStruct(TypeGenerationSpec typeGenSpec) + { + string serializeMethod = $"{typeGenSpec.NullableUnderlyingTypeMetadata.TypeInfoPropertyName}{SerializeHandlerPropName}(writer, value.Value);"; + + return GenerateFastPathFuncForType(typeGenSpec, serializeMethod, emitNullCheck: typeGenSpec.CanBeNull); + } + private string GenerateFastPathFuncForObject(TypeGenerationSpec typeGenSpec) { JsonSourceGenerationOptionsAttribute options = _currentContext.GenerationOptions; diff --git a/src/libraries/System.Text.Json/gen/TypeGenerationSpec.cs b/src/libraries/System.Text.Json/gen/TypeGenerationSpec.cs index a85bdd05b914cb..2ff742619becdd 100644 --- a/src/libraries/System.Text.Json/gen/TypeGenerationSpec.cs +++ b/src/libraries/System.Text.Json/gen/TypeGenerationSpec.cs @@ -232,6 +232,15 @@ public bool TryFilterSerializableProps( private bool FastPathIsSupported() { + + if (ClassType == ClassType.Nullable + && (NullableUnderlyingTypeMetadata != null) + && (NullableUnderlyingTypeMetadata.ClassType == ClassType.Object) + && (NullableUnderlyingTypeMetadata.GenerateSerializationLogic)) + { + return true; + } + if (ClassType == ClassType.Object) { if (ExtensionDataPropertyTypeSpec != null) diff --git a/src/libraries/System.Text.Json/ref/System.Text.Json.cs b/src/libraries/System.Text.Json/ref/System.Text.Json.cs index 0080e269aafbe1..59e244ed6f62b1 100644 --- a/src/libraries/System.Text.Json/ref/System.Text.Json.cs +++ b/src/libraries/System.Text.Json/ref/System.Text.Json.cs @@ -1008,7 +1008,7 @@ public static partial class JsonMetadataServices public static System.Text.Json.Serialization.Metadata.JsonTypeInfo CreateIReadOnlyDictionaryInfo(System.Text.Json.JsonSerializerOptions options, System.Text.Json.Serialization.Metadata.JsonCollectionInfoValues collectionInfo) where TCollection : System.Collections.Generic.IReadOnlyDictionary where TKey : notnull { throw null; } public static System.Text.Json.Serialization.Metadata.JsonTypeInfo CreateISetInfo(System.Text.Json.JsonSerializerOptions options, System.Text.Json.Serialization.Metadata.JsonCollectionInfoValues collectionInfo) where TCollection : System.Collections.Generic.ISet { throw null; } public static System.Text.Json.Serialization.Metadata.JsonTypeInfo CreateListInfo(System.Text.Json.JsonSerializerOptions options, System.Text.Json.Serialization.Metadata.JsonCollectionInfoValues collectionInfo) where TCollection : System.Collections.Generic.List { throw null; } - public static System.Text.Json.Serialization.Metadata.JsonTypeInfo CreateObjectInfo(System.Text.Json.JsonSerializerOptions options, System.Text.Json.Serialization.Metadata.JsonObjectInfoValues objectInfo) where T : notnull { throw null; } + public static System.Text.Json.Serialization.Metadata.JsonTypeInfo CreateObjectInfo(System.Text.Json.JsonSerializerOptions options, System.Text.Json.Serialization.Metadata.JsonObjectInfoValues objectInfo) { throw null; } public static System.Text.Json.Serialization.Metadata.JsonPropertyInfo CreatePropertyInfo(System.Text.Json.JsonSerializerOptions options, System.Text.Json.Serialization.Metadata.JsonPropertyInfoValues propertyInfo) { throw null; } public static System.Text.Json.Serialization.Metadata.JsonTypeInfo CreateQueueInfo(System.Text.Json.JsonSerializerOptions options, System.Text.Json.Serialization.Metadata.JsonCollectionInfoValues collectionInfo, System.Action addFunc) where TCollection : System.Collections.IEnumerable { throw null; } public static System.Text.Json.Serialization.Metadata.JsonTypeInfo CreateQueueInfo(System.Text.Json.JsonSerializerOptions options, System.Text.Json.Serialization.Metadata.JsonCollectionInfoValues collectionInfo) where TCollection : System.Collections.Generic.Queue { throw null; } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonMetadataServices.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonMetadataServices.cs index 14d002cf7cb1e6..4e0bfe89a92f81 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonMetadataServices.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonMetadataServices.cs @@ -79,7 +79,7 @@ public static JsonPropertyInfo CreatePropertyInfo(JsonSerializerOptions optio /// Thrown when or is null. /// A instance representing the class or struct. /// This API is for use by the output of the System.Text.Json source generator and should not be called directly. - public static JsonTypeInfo CreateObjectInfo(JsonSerializerOptions options, JsonObjectInfoValues objectInfo) where T : notnull + public static JsonTypeInfo CreateObjectInfo(JsonSerializerOptions options, JsonObjectInfoValues objectInfo) => new JsonTypeInfoInternal( options ?? throw new ArgumentNullException(nameof(options)), objectInfo ?? throw new ArgumentNullException(nameof(objectInfo))); 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 24559eb6c86a54..cb23a9d050cbc9 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 @@ -46,6 +46,8 @@ public interface ITestContext public JsonTypeInfo StructWithCustomConverterPropertyFactory { get; } public JsonTypeInfo ClassWithBadCustomConverter { get; } public JsonTypeInfo StructWithBadCustomConverter { get; } + public JsonTypeInfo NullableMyStructWithProperties { get; } + public JsonTypeInfo NullableMyStructWithCtrProperties { get; } } internal partial class JsonContext : JsonSerializerContext 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 81513b46499a70..5989ca198c3889 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 @@ -40,6 +40,8 @@ namespace System.Text.Json.SourceGeneration.Tests [JsonSerializable(typeof(StructWithCustomConverterPropertyFactory))] [JsonSerializable(typeof(ClassWithBadCustomConverter))] [JsonSerializable(typeof(StructWithBadCustomConverter))] + [JsonSerializable(typeof(MyStructWithProperties?))] + [JsonSerializable(typeof(MyStructWithCtrProperties?))] internal partial class MetadataAndSerializationContext : JsonSerializerContext, ITestContext { public JsonSourceGenerationMode JsonSourceGenerationMode => JsonSourceGenerationMode.Default; @@ -83,8 +85,28 @@ public override void EnsureFastPathGeneratedAsExpected() Assert.NotNull(MetadataAndSerializationContext.Default.StructWithCustomConverterProperty); Assert.NotNull(MetadataAndSerializationContext.Default.ClassWithCustomConverterPropertyFactory); Assert.NotNull(MetadataAndSerializationContext.Default.StructWithCustomConverterPropertyFactory); + Assert.NotNull(MetadataAndSerializationContext.Default.MyStructWithProperties?.SerializeHandler); + Assert.NotNull(MetadataAndSerializationContext.Default.MyStructWithCtrProperties?.SerializeHandler); Assert.Throws(() => MetadataAndSerializationContext.Default.ClassWithBadCustomConverter); Assert.Throws(() => MetadataAndSerializationContext.Default.StructWithBadCustomConverter); } + + [Fact] + public void Serialize_NullableStruct() + { + MyStructWithProperties? obj = new MyStructWithProperties { A = 1, B = 2 }; + string json = JsonSerializer.Serialize(obj, DefaultContext.NullableMyStructWithProperties); + string expected = "{\"B\":2,\"A\":1}"; + Assert.Equal(expected, json); + } + + [Fact] + public void Serialize_NullableStructWithCtr() + { + MyStructWithCtrProperties? obj = new MyStructWithCtrProperties(1, 2); + string json = JsonSerializer.Serialize(obj, DefaultContext.NullableMyStructWithCtrProperties); + string expected = "{\"B\":2,\"A\":1}"; + Assert.Equal(expected, json); + } } } 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 f83a2fc923421b..76567077f8d413 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 @@ -39,6 +39,8 @@ namespace System.Text.Json.SourceGeneration.Tests [JsonSerializable(typeof(StructWithCustomConverterPropertyFactory), GenerationMode = JsonSourceGenerationMode.Metadata)] [JsonSerializable(typeof(ClassWithBadCustomConverter), GenerationMode = JsonSourceGenerationMode.Metadata)] [JsonSerializable(typeof(StructWithBadCustomConverter), GenerationMode = JsonSourceGenerationMode.Metadata)] + [JsonSerializable(typeof(MyStructWithProperties?), GenerationMode = JsonSourceGenerationMode.Metadata)] + [JsonSerializable(typeof(MyStructWithCtrProperties?), GenerationMode = JsonSourceGenerationMode.Metadata)] internal partial class MetadataWithPerTypeAttributeContext : JsonSerializerContext, ITestContext { public JsonSourceGenerationMode JsonSourceGenerationMode => JsonSourceGenerationMode.Metadata; @@ -81,6 +83,8 @@ public override void EnsureFastPathGeneratedAsExpected() Assert.Null(MetadataWithPerTypeAttributeContext.Default.StructWithCustomConverterProperty.SerializeHandler); Assert.Null(MetadataWithPerTypeAttributeContext.Default.ClassWithCustomConverterPropertyFactory.SerializeHandler); Assert.Null(MetadataWithPerTypeAttributeContext.Default.StructWithCustomConverterPropertyFactory.SerializeHandler); + Assert.Null(MetadataWithPerTypeAttributeContext.Default.MyStructWithProperties?.SerializeHandler); + Assert.Null(MetadataWithPerTypeAttributeContext.Default.MyStructWithCtrProperties?.SerializeHandler); Assert.Throws(() => MetadataWithPerTypeAttributeContext.Default.ClassWithBadCustomConverter.SerializeHandler); Assert.Throws(() => MetadataWithPerTypeAttributeContext.Default.StructWithBadCustomConverter.SerializeHandler); } @@ -119,7 +123,10 @@ public override void EnsureFastPathGeneratedAsExpected() [JsonSerializable(typeof(ClassWithCustomConverterPropertyFactory))] [JsonSerializable(typeof(StructWithCustomConverterPropertyFactory))] [JsonSerializable(typeof(ClassWithBadCustomConverter))] - [JsonSerializable(typeof(StructWithBadCustomConverter))] + [JsonSerializable(typeof(StructWithBadCustomConverter))] + [JsonSerializable(typeof(MyStructWithProperties?))] + [JsonSerializable(typeof(MyStructWithCtrProperties?))] + internal partial class MetadataContext : JsonSerializerContext, ITestContext { public JsonSourceGenerationMode JsonSourceGenerationMode => JsonSourceGenerationMode.Metadata; @@ -185,6 +192,8 @@ public override void EnsureFastPathGeneratedAsExpected() Assert.Null(MetadataContext.Default.StructWithCustomConverterProperty.SerializeHandler); Assert.Null(MetadataContext.Default.ClassWithCustomConverterPropertyFactory.SerializeHandler); Assert.Null(MetadataContext.Default.StructWithCustomConverterPropertyFactory.SerializeHandler); + Assert.Null(MetadataContext.Default.MyStructWithProperties?.SerializeHandler); + Assert.Null(MetadataContext.Default.MyStructWithCtrProperties?.SerializeHandler); Assert.Throws(() => MetadataContext.Default.ClassWithBadCustomConverter.SerializeHandler); Assert.Throws(() => MetadataContext.Default.StructWithBadCustomConverter.SerializeHandler); } @@ -225,5 +234,23 @@ public void EnsureHelperMethodGenerated_ImplicitPropertyFactory() obj = JsonSerializer.Deserialize(Json, ContextWithImplicitStringEnum.Default.PocoWithEnum); Assert.Equal(EnumWrittenAsString.A, obj.MyEnum); } + + [Fact] + public void Serialize_NullableStruct() + { + MyStructWithProperties? obj = new MyStructWithProperties { A = 1, B = 2 }; + string json = JsonSerializer.Serialize(obj, DefaultContext.NullableMyStructWithProperties); + string expected = "{\"B\":2,\"A\":1}"; + Assert.Equal(expected, json); + } + + [Fact] + public void Serialize_NullableStructWithCtr() + { + MyStructWithCtrProperties? obj = new MyStructWithCtrProperties(1, 2); + string json = JsonSerializer.Serialize(obj, DefaultContext.NullableMyStructWithCtrProperties); + string expected = "{\"B\":2,\"A\":1}"; + Assert.Equal(expected, json); + } } } 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 060db030bcadab..8348d722f95bd1 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 @@ -40,6 +40,8 @@ namespace System.Text.Json.SourceGeneration.Tests [JsonSerializable(typeof(StructWithCustomConverterPropertyFactory), GenerationMode = JsonSourceGenerationMode.Metadata | JsonSourceGenerationMode.Serialization)] [JsonSerializable(typeof(ClassWithBadCustomConverter), GenerationMode = JsonSourceGenerationMode.Metadata | JsonSourceGenerationMode.Serialization)] [JsonSerializable(typeof(StructWithBadCustomConverter), GenerationMode = JsonSourceGenerationMode.Metadata | JsonSourceGenerationMode.Serialization)] + [JsonSerializable(typeof(MyStructWithProperties?), GenerationMode = JsonSourceGenerationMode.Metadata | JsonSourceGenerationMode.Serialization)] + [JsonSerializable(typeof(MyStructWithCtrProperties?), GenerationMode = JsonSourceGenerationMode.Metadata | JsonSourceGenerationMode.Serialization)] internal partial class MixedModeContext : JsonSerializerContext, ITestContext { public JsonSourceGenerationMode JsonSourceGenerationMode => JsonSourceGenerationMode.Metadata | JsonSourceGenerationMode.Serialization; @@ -83,6 +85,8 @@ public override void EnsureFastPathGeneratedAsExpected() Assert.Null(MixedModeContext.Default.StructWithCustomConverterProperty.SerializeHandler); Assert.Null(MixedModeContext.Default.ClassWithCustomConverterPropertyFactory.SerializeHandler); Assert.Null(MixedModeContext.Default.StructWithCustomConverterPropertyFactory.SerializeHandler); + Assert.NotNull(MixedModeContext.Default.MyStructWithProperties?.SerializeHandler); + Assert.NotNull(MixedModeContext.Default.MyStructWithCtrProperties?.SerializeHandler); Assert.Throws(() => MixedModeContext.Default.ClassWithBadCustomConverter.SerializeHandler); Assert.Throws(() => MixedModeContext.Default.StructWithBadCustomConverter.SerializeHandler); } @@ -219,5 +223,23 @@ public void OnSerializeCallbacks_WithCustomOptions() Assert.Equal("{\"myProperty\":\"Before\"}", json); Assert.Equal("After", obj.MyProperty); } + + [Fact] + public void Serialize_NullableStruct() + { + MyStructWithProperties? obj = new MyStructWithProperties { A = 1, B = 2 }; + string json = JsonSerializer.Serialize(obj, DefaultContext.NullableMyStructWithProperties); + string expected = "{\"B\":2,\"A\":1}"; + Assert.Equal(expected, json); + } + + [Fact] + public void Serialize_NullableStructWithCtr() + { + MyStructWithCtrProperties? obj = new MyStructWithCtrProperties(1, 2); + string json = JsonSerializer.Serialize(obj, DefaultContext.NullableMyStructWithCtrProperties); + string expected = "{\"B\":2,\"A\":1}"; + Assert.Equal(expected, json); + } } } 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 57f663e8b36339..422540240edb05 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 @@ -40,6 +40,8 @@ namespace System.Text.Json.SourceGeneration.Tests [JsonSerializable(typeof(StructWithCustomConverterPropertyFactory))] [JsonSerializable(typeof(ClassWithBadCustomConverter))] [JsonSerializable(typeof(StructWithBadCustomConverter))] + [JsonSerializable(typeof(MyStructWithProperties?))] + [JsonSerializable(typeof(MyStructWithCtrProperties?))] internal partial class SerializationContext : JsonSerializerContext, ITestContext { public JsonSourceGenerationMode JsonSourceGenerationMode => JsonSourceGenerationMode.Serialization; @@ -78,6 +80,9 @@ internal partial class SerializationContext : JsonSerializerContext, ITestContex [JsonSerializable(typeof(StructWithCustomConverterPropertyFactory), GenerationMode = JsonSourceGenerationMode.Serialization)] [JsonSerializable(typeof(ClassWithBadCustomConverter), GenerationMode = JsonSourceGenerationMode.Serialization)] [JsonSerializable(typeof(StructWithBadCustomConverter), GenerationMode = JsonSourceGenerationMode.Serialization)] + [JsonSerializable(typeof(MyStructWithProperties?), GenerationMode = JsonSourceGenerationMode.Serialization)] + [JsonSerializable(typeof(MyStructWithCtrProperties?), GenerationMode = JsonSourceGenerationMode.Serialization)] + internal partial class SerializationWithPerTypeAttributeContext : JsonSerializerContext, ITestContext { public JsonSourceGenerationMode JsonSourceGenerationMode => JsonSourceGenerationMode.Serialization; @@ -117,6 +122,8 @@ internal partial class SerializationWithPerTypeAttributeContext : JsonSerializer [JsonSerializable(typeof(StructWithCustomConverterPropertyFactory), GenerationMode = JsonSourceGenerationMode.Serialization)] [JsonSerializable(typeof(ClassWithBadCustomConverter), GenerationMode = JsonSourceGenerationMode.Serialization)] [JsonSerializable(typeof(StructWithBadCustomConverter), GenerationMode = JsonSourceGenerationMode.Serialization)] + [JsonSerializable(typeof(MyStructWithProperties?), GenerationMode = JsonSourceGenerationMode.Serialization)] + [JsonSerializable(typeof(MyStructWithCtrProperties?), GenerationMode = JsonSourceGenerationMode.Serialization)] internal partial class SerializationContextWithCamelCase : JsonSerializerContext, ITestContext { public JsonSourceGenerationMode JsonSourceGenerationMode => JsonSourceGenerationMode.Serialization; @@ -162,6 +169,8 @@ public override void EnsureFastPathGeneratedAsExpected() Assert.Null(SerializationContext.Default.StructWithCustomConverterFactory.SerializeHandler); Assert.Null(SerializationContext.Default.ClassWithCustomConverterProperty.SerializeHandler); Assert.Null(SerializationContext.Default.StructWithCustomConverterProperty.SerializeHandler); + Assert.NotNull(SerializationContext.Default.MyStructWithProperties?.SerializeHandler); + Assert.NotNull(SerializationContext.Default.MyStructWithCtrProperties?.SerializeHandler); Assert.Throws(() => SerializationContext.Default.ClassWithBadCustomConverter.SerializeHandler); Assert.Throws(() => SerializationContext.Default.StructWithBadCustomConverter.SerializeHandler); } @@ -376,6 +385,24 @@ void RunTest(ClassWithEnumAndNullable expected) } } + [Fact] + public void Serialize_NullableStruct() + { + MyStructWithProperties? obj = new MyStructWithProperties { A = 1, B = 2 }; + string json = JsonSerializer.Serialize(obj, DefaultContext.NullableMyStructWithProperties); + string expected = "{\"B\":2,\"A\":1}"; + Assert.Equal(expected, json); + } + + [Fact] + public void Serialize_NullableStructWithCtr() + { + MyStructWithCtrProperties? obj = new MyStructWithCtrProperties(1, 2); + string json = JsonSerializer.Serialize(obj, DefaultContext.NullableMyStructWithCtrProperties); + string expected = "{\"B\":2,\"A\":1}"; + Assert.Equal(expected,json); + } + [Fact] public override void ClassWithNullableProperties_Roundtrip() { @@ -468,6 +495,8 @@ public override void EnsureFastPathGeneratedAsExpected() Assert.Null(SerializationWithPerTypeAttributeContext.Default.StructWithCustomConverterProperty.SerializeHandler); Assert.Null(SerializationWithPerTypeAttributeContext.Default.ClassWithCustomConverterPropertyFactory.SerializeHandler); Assert.Null(SerializationWithPerTypeAttributeContext.Default.StructWithCustomConverterPropertyFactory.SerializeHandler); + Assert.NotNull(SerializationWithPerTypeAttributeContext.Default.MyStructWithProperties?.SerializeHandler); + Assert.NotNull(SerializationWithPerTypeAttributeContext.Default.MyStructWithCtrProperties?.SerializeHandler); Assert.Throws(() => SerializationWithPerTypeAttributeContext.Default.ClassWithBadCustomConverter.SerializeHandler); Assert.Throws(() => SerializationWithPerTypeAttributeContext.Default.StructWithBadCustomConverter.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 887f65b80da83b..47090297f29a8e 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 @@ -151,4 +151,22 @@ public class JsonMessage } internal struct MyStruct { } + + public struct MyStructWithProperties + { + public int B { get; set; } + public int A { get; set; } + } + + public struct MyStructWithCtrProperties + { + public int B { get; private set; } + public int A { get; private set; } + + public MyStructWithCtrProperties(int a, int b) + { + A = a; + B = b; + } + } }