From e0906faf73803e9d8243001642ecf1c53872aee2 Mon Sep 17 00:00:00 2001 From: pedrobsaila Date: Sat, 1 Apr 2023 13:45:20 +0200 Subject: [PATCH 1/7] Use underlying type converter for nullable type --- .../gen/JsonSourceGenerator.Emitter.cs | 2 +- .../gen/JsonSourceGenerator.Parser.cs | 9 ++- .../JsonSourceGeneratorTests.cs | 70 +++++++++++++++++++ 3 files changed, 79 insertions(+), 2 deletions(-) diff --git a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs index c773f7d006943..fb62312f0537d 100644 --- a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs +++ b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs @@ -66,7 +66,7 @@ private sealed partial class Emitter private const string JsonIgnoreConditionTypeRef = "global::System.Text.Json.Serialization.JsonIgnoreCondition"; private const string JsonNumberHandlingTypeRef = "global::System.Text.Json.Serialization.JsonNumberHandling"; private const string JsonUnmappedMemberHandlingTypeRef = "global::System.Text.Json.Serialization.JsonUnmappedMemberHandling"; - private const string JsonMetadataServicesTypeRef = "global::System.Text.Json.Serialization.Metadata.JsonMetadataServices"; + internal const string JsonMetadataServicesTypeRef = "global::System.Text.Json.Serialization.Metadata.JsonMetadataServices"; private const string JsonObjectInfoValuesTypeRef = "global::System.Text.Json.Serialization.Metadata.JsonObjectInfoValues"; private const string JsonParameterInfoValuesTypeRef = "global::System.Text.Json.Serialization.Metadata.JsonParameterInfoValues"; private const string JsonPropertyInfoTypeRef = "global::System.Text.Json.Serialization.Metadata.JsonPropertyInfo"; diff --git a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs index d641c432cf604..2767b682f6606 100644 --- a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs +++ b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs @@ -1517,8 +1517,15 @@ private static bool PropertyAccessorCanBeReferenced(MethodInfo? accessor) return null; } - if (converterType.GetCompatibleGenericBaseClass(_jsonConverterOfTType) != null) + Type? compatibleJsonConverterBaseClass = converterType.GetCompatibleGenericBaseClass(_jsonConverterOfTType); + + if (compatibleJsonConverterBaseClass != null) { + if (type.IsNullableValueType(out Type? underlyingType) && underlyingType == compatibleJsonConverterBaseClass.GenericTypeArguments[0]) + { + return $"{Emitter.JsonMetadataServicesTypeRef}.GetNullableConverter<{underlyingType.FullName}>({OptionsLocalVariableName})"; + } + return $"new {converterType.GetCompilableName()}()"; } else if (converterType.GetCompatibleBaseClass(JsonConverterFactoryFullName) != null) diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Unit.Tests/JsonSourceGeneratorTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Unit.Tests/JsonSourceGeneratorTests.cs index ce52184065a5f..8e789902408c1 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Unit.Tests/JsonSourceGeneratorTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Unit.Tests/JsonSourceGeneratorTests.cs @@ -813,5 +813,75 @@ public class ClassWithPropertyNameThatIsAReservedKeyword CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Warning, generatorDiags, Array.Empty<(Location, string)>()); CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Error, generatorDiags, Array.Empty<(Location, string)>()); } + + [Fact] + public void UseUnderlyingTypeConverterForNullableType() + { + // Compile the referenced assembly first. + Compilation referencedCompilation = CompilationHelper.CreateReferencedLocationCompilation(); + + // Emit the image of the referenced assembly. + byte[] referencedImage = CompilationHelper.CreateAssemblyImage(referencedCompilation); + + string source = @" + using ReferencedAssembly; + using System; + using System.Text.Json; + using System.Text.Json.Serialization; + + namespace Test + { + [JsonSourceGenerationOptions] + [JsonSerializable(typeof(Sample))] + public partial class SourceGenerationContext : JsonSerializerContext + { + } + + public class Sample + { + [JsonConverter(typeof(DateTimeOffsetToTimestampJsonConverter))] + public DateTimeOffset Start { get; set; } + + [JsonConverter(typeof(DateTimeOffsetToTimestampJsonConverter))] + public DateTimeOffset? End { get; set; } // Without this property, this is fine + } + + public class DateTimeOffsetToTimestampJsonConverter : JsonConverter + { + internal const long TicksPerMicroseconds = 10; + + public override DateTimeOffset Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var value = reader.GetInt64(); + return new DateTimeOffset(value * TicksPerMicroseconds, TimeSpan.Zero); + } + + public override void Write(Utf8JsonWriter writer, DateTimeOffset value, JsonSerializerOptions options) + { + writer.WriteNumberValue(value.Ticks / TicksPerMicroseconds); + } + } + }"; + + MetadataReference[] additionalReferences = { MetadataReference.CreateFromImage(referencedImage) }; + + Compilation compilation = CompilationHelper.CreateCompilation(source, additionalReferences); + + JsonSourceGenerator generator = new JsonSourceGenerator(); + + Compilation newCompilation = CompilationHelper.RunGenerators(compilation, out ImmutableArray generatorDiags, generator); + + // Make sure compilation was successful. + CheckCompilationDiagnosticsErrors(generatorDiags); + CheckCompilationDiagnosticsErrors(newCompilation.GetDiagnostics()); + + Dictionary types = generator.GetSerializableTypes(); + Assert.Equal(1, types.Count); + Type myType = types["Test.Sample"]; + Assert.Equal("Test.Sample", myType.FullName); + string[] expectedPropertyNamesMyType = { "End", "Start" }; + string[] expectedMethodNamesMyType = { "get_End", "get_Start", "set_End", "set_Start" }; + CheckFieldsPropertiesMethods(myType, new string[0], expectedPropertyNamesMyType, expectedMethodNamesMyType); + } } } From 3ed8366d3fbfd795784a39d31bbf0a293296300d Mon Sep 17 00:00:00 2001 From: pedrobsaila Date: Tue, 30 May 2023 22:39:33 +0200 Subject: [PATCH 2/7] verify that we use underlying type converter for nullable types --- .../JsonSourceGeneratorTests.cs | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Unit.Tests/JsonSourceGeneratorTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Unit.Tests/JsonSourceGeneratorTests.cs index a0e038aa1062e..1a7013fee5134 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Unit.Tests/JsonSourceGeneratorTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Unit.Tests/JsonSourceGeneratorTests.cs @@ -694,6 +694,64 @@ public class ClassWithPropertyNameThatIsAReservedKeyword CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Error, generatorDiags, Array.Empty<(Location, string)>()); } + [Fact] + public void UseUnderlyingTypeConverterForNullableType() + { + // Compile the referenced assembly first. + Compilation referencedCompilation = CompilationHelper.CreateReferencedLocationCompilation(); + + // Emit the image of the referenced assembly. + byte[] referencedImage = CompilationHelper.CreateAssemblyImage(referencedCompilation); + + string source = @" + using ReferencedAssembly; + using System; + using System.Text.Json; + using System.Text.Json.Serialization; + namespace Test + { + [JsonSourceGenerationOptions] + [JsonSerializable(typeof(Sample))] + public partial class SourceGenerationContext : JsonSerializerContext + { + } + public class Sample + { + [JsonConverter(typeof(DateTimeOffsetToTimestampJsonConverter))] + public DateTimeOffset Start { get; set; } + [JsonConverter(typeof(DateTimeOffsetToTimestampJsonConverter))] + public DateTimeOffset? End { get; set; } // Without this property, this is fine + } + public class DateTimeOffsetToTimestampJsonConverter : JsonConverter + { + internal const long TicksPerMicroseconds = 10; + public override DateTimeOffset Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var value = reader.GetInt64(); + return new DateTimeOffset(value * TicksPerMicroseconds, TimeSpan.Zero); + } + public override void Write(Utf8JsonWriter writer, DateTimeOffset value, JsonSerializerOptions options) + { + writer.WriteNumberValue(value.Ticks / TicksPerMicroseconds); + } + } + }"; + + MetadataReference[] additionalReferences = { MetadataReference.CreateFromImage(referencedImage) }; + + Compilation compilation = CompilationHelper.CreateCompilation(source, additionalReferences); + + JsonSourceGeneratorResult result = CompilationHelper.RunJsonSourceGenerator(compilation); + + // Make sure compilation was successful. + CheckCompilationDiagnosticsErrors(result.NewCompilation.GetDiagnostics()); + + Assert.Equal(3, result.AllGeneratedTypes.Count()); + result.AssertContainsType("global::Test.Sample"); + result.AssertContainsType("global::System.DateTimeOffset"); + result.AssertContainsType("global::System.DateTimeOffset?"); + } + [Fact] public void VariousGenericSerializableTypesAreSupported() { From bb67ed17503f0117a5ec98b618ddd95debaf8bf6 Mon Sep 17 00:00:00 2001 From: pedrobsaila Date: Tue, 30 May 2023 22:40:42 +0200 Subject: [PATCH 3/7] rollback a little change --- .../System.Text.Json/gen/JsonSourceGenerator.Emitter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs index 167e76fb484e0..e329d014f075c 100644 --- a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs +++ b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs @@ -59,7 +59,7 @@ private sealed partial class Emitter private const string JsonNumberHandlingTypeRef = "global::System.Text.Json.Serialization.JsonNumberHandling"; private const string JsonObjectCreationHandlingTypeRef = "global::System.Text.Json.Serialization.JsonObjectCreationHandling"; private const string JsonUnmappedMemberHandlingTypeRef = "global::System.Text.Json.Serialization.JsonUnmappedMemberHandling"; - internal const string JsonMetadataServicesTypeRef = "global::System.Text.Json.Serialization.Metadata.JsonMetadataServices"; + private const string JsonMetadataServicesTypeRef = "global::System.Text.Json.Serialization.Metadata.JsonMetadataServices"; private const string JsonObjectInfoValuesTypeRef = "global::System.Text.Json.Serialization.Metadata.JsonObjectInfoValues"; private const string JsonParameterInfoValuesTypeRef = "global::System.Text.Json.Serialization.Metadata.JsonParameterInfoValues"; private const string JsonPropertyInfoTypeRef = "global::System.Text.Json.Serialization.Metadata.JsonPropertyInfo"; From b69668cf3d87e4aa863cac9c6234171cfefc2d13 Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Tue, 30 May 2023 22:34:40 +0100 Subject: [PATCH 4/7] Update src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Unit.Tests/JsonSourceGeneratorTests.cs --- .../JsonSourceGeneratorTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Unit.Tests/JsonSourceGeneratorTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Unit.Tests/JsonSourceGeneratorTests.cs index 1a7013fee5134..e3e2eba56db88 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Unit.Tests/JsonSourceGeneratorTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Unit.Tests/JsonSourceGeneratorTests.cs @@ -703,7 +703,7 @@ public void UseUnderlyingTypeConverterForNullableType() // Emit the image of the referenced assembly. byte[] referencedImage = CompilationHelper.CreateAssemblyImage(referencedCompilation); - string source = @" + string source = """ using ReferencedAssembly; using System; using System.Text.Json; From e8d2405e92729a9b61e8e2aa2826b706d4fca815 Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Tue, 30 May 2023 22:34:46 +0100 Subject: [PATCH 5/7] Update src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Unit.Tests/JsonSourceGeneratorTests.cs --- .../JsonSourceGeneratorTests.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Unit.Tests/JsonSourceGeneratorTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Unit.Tests/JsonSourceGeneratorTests.cs index e3e2eba56db88..4814d4930d7e1 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Unit.Tests/JsonSourceGeneratorTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Unit.Tests/JsonSourceGeneratorTests.cs @@ -735,7 +735,8 @@ public override void Write(Utf8JsonWriter writer, DateTimeOffset value, JsonSeri writer.WriteNumberValue(value.Ticks / TicksPerMicroseconds); } } - }"; + } + """; MetadataReference[] additionalReferences = { MetadataReference.CreateFromImage(referencedImage) }; From 6b5a54d6f5ee0aeeba20ae9a0238941cc5b4b07d Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Tue, 30 May 2023 22:37:28 +0100 Subject: [PATCH 6/7] Use consistent indentation. --- .../JsonSourceGeneratorTests.cs | 54 +++++++++---------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Unit.Tests/JsonSourceGeneratorTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Unit.Tests/JsonSourceGeneratorTests.cs index 4814d4930d7e1..4287e161ca048 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Unit.Tests/JsonSourceGeneratorTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Unit.Tests/JsonSourceGeneratorTests.cs @@ -704,39 +704,39 @@ public void UseUnderlyingTypeConverterForNullableType() byte[] referencedImage = CompilationHelper.CreateAssemblyImage(referencedCompilation); string source = """ - using ReferencedAssembly; - using System; - using System.Text.Json; - using System.Text.Json.Serialization; - namespace Test - { - [JsonSourceGenerationOptions] - [JsonSerializable(typeof(Sample))] - public partial class SourceGenerationContext : JsonSerializerContext - { - } - public class Sample - { - [JsonConverter(typeof(DateTimeOffsetToTimestampJsonConverter))] - public DateTimeOffset Start { get; set; } - [JsonConverter(typeof(DateTimeOffsetToTimestampJsonConverter))] - public DateTimeOffset? End { get; set; } // Without this property, this is fine - } - public class DateTimeOffsetToTimestampJsonConverter : JsonConverter + using ReferencedAssembly; + using System; + using System.Text.Json; + using System.Text.Json.Serialization; + namespace Test { - internal const long TicksPerMicroseconds = 10; - public override DateTimeOffset Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + [JsonSourceGenerationOptions] + [JsonSerializable(typeof(Sample))] + public partial class SourceGenerationContext : JsonSerializerContext { - var value = reader.GetInt64(); - return new DateTimeOffset(value * TicksPerMicroseconds, TimeSpan.Zero); } - public override void Write(Utf8JsonWriter writer, DateTimeOffset value, JsonSerializerOptions options) + public class Sample { - writer.WriteNumberValue(value.Ticks / TicksPerMicroseconds); + [JsonConverter(typeof(DateTimeOffsetToTimestampJsonConverter))] + public DateTimeOffset Start { get; set; } + [JsonConverter(typeof(DateTimeOffsetToTimestampJsonConverter))] + public DateTimeOffset? End { get; set; } // Without this property, this is fine + } + public class DateTimeOffsetToTimestampJsonConverter : JsonConverter + { + internal const long TicksPerMicroseconds = 10; + public override DateTimeOffset Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var value = reader.GetInt64(); + return new DateTimeOffset(value * TicksPerMicroseconds, TimeSpan.Zero); + } + public override void Write(Utf8JsonWriter writer, DateTimeOffset value, JsonSerializerOptions options) + { + writer.WriteNumberValue(value.Ticks / TicksPerMicroseconds); + } } } - } - """; + """; MetadataReference[] additionalReferences = { MetadataReference.CreateFromImage(referencedImage) }; From cf44449e44ad1da5c2f19dfcaad3fb12b6e6a80f Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Wed, 31 May 2023 01:35:57 +0100 Subject: [PATCH 7/7] Fix runtime support for nullable properties with custom converters. --- .../gen/JsonSourceGenerator.Emitter.cs | 65 ++++++++++++++----- .../ContextClasses.cs | 2 + .../MetadataAndSerializationContextTests.cs | 2 + .../MetadataContextTests.cs | 4 ++ .../MixedModeContextTests.cs | 2 + .../RealWorldContextTests.cs | 55 +++++++++++++++- .../SerializationContextTests.cs | 6 ++ .../TestClasses.CustomConverters.cs | 25 +++++++ 8 files changed, 144 insertions(+), 17 deletions(-) diff --git a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs index e329d014f075c..14f66ef020e1e 100644 --- a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs +++ b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs @@ -33,6 +33,7 @@ private sealed partial class Emitter private const string PropInitMethodNameSuffix = "PropInit"; private const string TryGetTypeInfoForRuntimeCustomConverterMethodName = "TryGetTypeInfoForRuntimeCustomConverter"; private const string ExpandConverterMethodName = "ExpandConverter"; + private const string GetConverterForNullablePropertyMethodName = "GetConverterForNullableProperty"; private const string SerializeHandlerPropName = "SerializeHandler"; private const string OptionsLocalVariableName = "options"; private const string ValueVarName = "value"; @@ -79,6 +80,11 @@ private sealed partial class Emitter /// private readonly Dictionary _propertyNames = new(); + /// + /// Indicates that the type graph contains a nullable property with a design-time custom converter declaration. + /// + private bool _emitGetConverterForNullablePropertyMethod; + /// /// The SourceText emit implementation filled by the individual Roslyn versions. /// @@ -88,6 +94,7 @@ public void Emit(ContextGenerationSpec contextGenerationSpec) { Debug.Assert(_typeIndex.Count == 0); Debug.Assert(_propertyNames.Count == 0); + Debug.Assert(!_emitGetConverterForNullablePropertyMethod); foreach (TypeGenerationSpec spec in contextGenerationSpec.GeneratedTypes) { @@ -106,7 +113,7 @@ public void Emit(ContextGenerationSpec contextGenerationSpec) string contextName = contextGenerationSpec.ContextType.Name; // Add root context implementation. - AddSource($"{contextName}.g.cs", GetRootJsonContextImplementation(contextGenerationSpec)); + AddSource($"{contextName}.g.cs", GetRootJsonContextImplementation(contextGenerationSpec, _emitGetConverterForNullablePropertyMethod)); // Add GetJsonTypeInfo override implementation. AddSource($"{contextName}.GetJsonTypeInfo.g.cs", GetGetTypeInfoImplementation(contextGenerationSpec)); @@ -114,6 +121,7 @@ public void Emit(ContextGenerationSpec contextGenerationSpec) // Add property name initialization. AddSource($"{contextName}.PropertyNames.g.cs", GetPropertyNameInitialization(contextGenerationSpec)); + _emitGetConverterForNullablePropertyMethod = false; _propertyNames.Clear(); _typeIndex.Clear(); } @@ -539,7 +547,7 @@ private SourceText GenerateForObject(ContextGenerationSpec contextSpec, TypeGene return CompleteSourceFileAndReturnText(writer); } - private static void GeneratePropMetadataInitFunc(SourceWriter writer, string propInitMethodName, TypeGenerationSpec typeGenerationSpec) + private void GeneratePropMetadataInitFunc(SourceWriter writer, string propInitMethodName, TypeGenerationSpec typeGenerationSpec) { Debug.Assert(typeGenerationSpec.PropertyGenSpecs != null); ImmutableEquatableArray properties = typeGenerationSpec.PropertyGenSpecs; @@ -585,9 +593,15 @@ private static void GeneratePropMetadataInitFunc(SourceWriter writer, string pro ? $"{JsonIgnoreConditionTypeRef}.{property.DefaultIgnoreCondition.Value}" : "null"; - string converterInstantiationExpr = property.ConverterType != null - ? $"({JsonConverterTypeRef}<{propertyTypeFQN}>){ExpandConverterMethodName}(typeof({propertyTypeFQN}), new {property.ConverterType.FullyQualifiedName}(), {OptionsLocalVariableName})" - : "null"; + string? converterInstantiationExpr = null; + if (property.ConverterType != null) + { + TypeRef? nullableUnderlyingType = _typeIndex[property.PropertyType].NullableUnderlyingType; + _emitGetConverterForNullablePropertyMethod |= nullableUnderlyingType != null; + converterInstantiationExpr = nullableUnderlyingType != null + ? $"{GetConverterForNullablePropertyMethodName}<{nullableUnderlyingType.FullyQualifiedName}>(new {property.ConverterType.FullyQualifiedName}(), {OptionsLocalVariableName})" + : $"({JsonConverterTypeRef}<{propertyTypeFQN}>){ExpandConverterMethodName}(typeof({propertyTypeFQN}), new {property.ConverterType.FullyQualifiedName}(), {OptionsLocalVariableName})"; + } writer.WriteLine($$""" var {{InfoVarName}}{{i}} = new {{JsonPropertyInfoValuesTypeRef}}<{{propertyTypeFQN}}>() @@ -596,7 +610,7 @@ private static void GeneratePropMetadataInitFunc(SourceWriter writer, string pro IsPublic = {{FormatBool(property.IsPublic)}}, IsVirtual = {{FormatBool(property.IsVirtual)}}, DeclaringType = typeof({{property.DeclaringType.FullyQualifiedName}}), - Converter = {{converterInstantiationExpr}}, + Converter = {{converterInstantiationExpr ?? "null"}}, Getter = {{getterValue}}, Setter = {{setterValue}}, IgnoreCondition = {{ignoreConditionNamedArg}}, @@ -1007,7 +1021,7 @@ private static void GenerateTypeInfoProperty(SourceWriter writer, TypeGeneration """); } - private static SourceText GetRootJsonContextImplementation(ContextGenerationSpec contextSpec) + private static SourceText GetRootJsonContextImplementation(ContextGenerationSpec contextSpec, bool emitGetConverterForNullablePropertyMethod) { string contextTypeRef = contextSpec.ContextType.FullyQualifiedName; string contextTypeName = contextSpec.ContextType.Name; @@ -1048,7 +1062,7 @@ private static SourceText GetRootJsonContextImplementation(ContextGenerationSpec writer.WriteLine(); - GenerateConverterHelpers(writer); + GenerateConverterHelpers(writer, emitGetConverterForNullablePropertyMethod); return CompleteSourceFileAndReturnText(writer); } @@ -1082,7 +1096,7 @@ private static void GetLogicForDefaultSerializerOptionsInit(ContextGenerationSpe """); } - private static void GenerateConverterHelpers(SourceWriter writer) + private static void GenerateConverterHelpers(SourceWriter writer, bool emitGetConverterForNullablePropertyMethod) { // The generic type parameter could capture type parameters from containing types, // so use a name that is unlikely to be used. @@ -1109,15 +1123,20 @@ private static void GenerateConverterHelpers(SourceWriter writer) {{JsonConverterTypeRef}}? converter = options.Converters[i]; if (converter?.CanConvert(type) == true) { - return {{ExpandConverterMethodName}}(type, converter, options); + return {{ExpandConverterMethodName}}(type, converter, options, validateCanConvert: false); } } return null; } - private static {{JsonConverterTypeRef}} {{ExpandConverterMethodName}}({{TypeTypeRef}} type, {{JsonConverterTypeRef}} converter, {{JsonSerializerOptionsTypeRef}} options) + private static {{JsonConverterTypeRef}} {{ExpandConverterMethodName}}({{TypeTypeRef}} type, {{JsonConverterTypeRef}} converter, {{JsonSerializerOptionsTypeRef}} options, bool validateCanConvert = true) { + if (validateCanConvert && !converter.CanConvert(type)) + { + throw new {{InvalidOperationExceptionTypeRef}}(string.Format("{{ExceptionMessages.IncompatibleConverterType}}", converter.GetType(), type)); + } + if (converter is {{JsonConverterFactoryTypeRef}} factory) { converter = factory.CreateConverter(type, options); @@ -1126,15 +1145,29 @@ private static void GenerateConverterHelpers(SourceWriter writer) throw new {{InvalidOperationExceptionTypeRef}}(string.Format("{{ExceptionMessages.InvalidJsonConverterFactoryOutput}}", factory.GetType())); } } - - if (!converter.CanConvert(type)) - { - throw new {{InvalidOperationExceptionTypeRef}}(string.Format("{{ExceptionMessages.IncompatibleConverterType}}", converter.GetType(), type)); - } return converter; } """); + + if (emitGetConverterForNullablePropertyMethod) + { + writer.WriteLine($$""" + + private static {{JsonConverterTypeRef}}<{{TypeParameter}}?> {{GetConverterForNullablePropertyMethodName}}<{{TypeParameter}}>({{JsonConverterTypeRef}} converter, {{JsonSerializerOptionsTypeRef}} options) + where {{TypeParameter}} : struct + { + if (converter.CanConvert(typeof({{TypeParameter}}?))) + { + return ({{JsonConverterTypeRef}}<{{TypeParameter}}?>){{ExpandConverterMethodName}}(typeof({{TypeParameter}}?), converter, options, validateCanConvert: false); + } + + converter = {{ExpandConverterMethodName}}(typeof({{TypeParameter}}), converter, options); + {{JsonTypeInfoTypeRef}}<{{TypeParameter}}> typeInfo = {{JsonMetadataServicesTypeRef}}.{{CreateValueInfoMethodName}}<{{TypeParameter}}>(options, converter); + return {{JsonMetadataServicesTypeRef}}.GetNullableConverter<{{TypeParameter}}>(typeInfo); + } + """); + } } private static SourceText GetGetTypeInfoImplementation(ContextGenerationSpec contextSpec) 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 0d87b280c992a..863d86bd5886f 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 @@ -49,6 +49,8 @@ public interface ITestContext public JsonTypeInfo StructWithCustomConverterProperty { get; } public JsonTypeInfo ClassWithCustomConverterFactoryProperty { get; } public JsonTypeInfo StructWithCustomConverterFactoryProperty { get; } + public JsonTypeInfo ClassWithCustomConverterNullableProperty { get; } + public JsonTypeInfo ClassWithCustomConverterFactoryNullableProperty { get; } public JsonTypeInfo ClassWithBadCustomConverter { get; } public JsonTypeInfo StructWithBadCustomConverter { get; } public JsonTypeInfo NullablePersonStruct { 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 8e37e934f7c34..8ebf4c2e7f7c8 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 @@ -45,6 +45,8 @@ namespace System.Text.Json.SourceGeneration.Tests [JsonSerializable(typeof(StructWithCustomConverterProperty))] [JsonSerializable(typeof(ClassWithCustomConverterFactoryProperty))] [JsonSerializable(typeof(StructWithCustomConverterFactoryProperty))] + [JsonSerializable(typeof(ClassWithCustomConverterNullableProperty))] + [JsonSerializable(typeof(ClassWithCustomConverterFactoryNullableProperty))] [JsonSerializable(typeof(ClassWithBadCustomConverter))] [JsonSerializable(typeof(StructWithBadCustomConverter))] [JsonSerializable(typeof(PersonStruct?))] 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 4cb83945fabf7..562dba39b59f4 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 @@ -44,6 +44,8 @@ namespace System.Text.Json.SourceGeneration.Tests [JsonSerializable(typeof(StructWithCustomConverterProperty), GenerationMode = JsonSourceGenerationMode.Metadata)] [JsonSerializable(typeof(ClassWithCustomConverterFactoryProperty), GenerationMode = JsonSourceGenerationMode.Metadata)] [JsonSerializable(typeof(StructWithCustomConverterFactoryProperty), GenerationMode = JsonSourceGenerationMode.Metadata)] + [JsonSerializable(typeof(ClassWithCustomConverterNullableProperty), GenerationMode = JsonSourceGenerationMode.Metadata)] + [JsonSerializable(typeof(ClassWithCustomConverterFactoryNullableProperty), GenerationMode = JsonSourceGenerationMode.Metadata)] [JsonSerializable(typeof(ClassWithBadCustomConverter), GenerationMode = JsonSourceGenerationMode.Metadata)] [JsonSerializable(typeof(StructWithBadCustomConverter), GenerationMode = JsonSourceGenerationMode.Metadata)] [JsonSerializable(typeof(PersonStruct?), GenerationMode = JsonSourceGenerationMode.Metadata)] @@ -144,6 +146,8 @@ public override void EnsureFastPathGeneratedAsExpected() [JsonSerializable(typeof(StructWithCustomConverterProperty))] [JsonSerializable(typeof(ClassWithCustomConverterFactoryProperty))] [JsonSerializable(typeof(StructWithCustomConverterFactoryProperty))] + [JsonSerializable(typeof(ClassWithCustomConverterNullableProperty))] + [JsonSerializable(typeof(ClassWithCustomConverterFactoryNullableProperty))] [JsonSerializable(typeof(ClassWithBadCustomConverter))] [JsonSerializable(typeof(StructWithBadCustomConverter))] [JsonSerializable(typeof(PersonStruct?))] 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 98d3dfa3bd569..bd5a5256f663e 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 @@ -45,6 +45,8 @@ namespace System.Text.Json.SourceGeneration.Tests [JsonSerializable(typeof(StructWithCustomConverterProperty), GenerationMode = JsonSourceGenerationMode.Metadata | JsonSourceGenerationMode.Serialization)] [JsonSerializable(typeof(ClassWithCustomConverterFactoryProperty), GenerationMode = JsonSourceGenerationMode.Metadata | JsonSourceGenerationMode.Serialization)] [JsonSerializable(typeof(StructWithCustomConverterFactoryProperty), GenerationMode = JsonSourceGenerationMode.Metadata | JsonSourceGenerationMode.Serialization)] + [JsonSerializable(typeof(ClassWithCustomConverterNullableProperty), GenerationMode = JsonSourceGenerationMode.Metadata | JsonSourceGenerationMode.Serialization)] + [JsonSerializable(typeof(ClassWithCustomConverterFactoryNullableProperty), GenerationMode = JsonSourceGenerationMode.Metadata | JsonSourceGenerationMode.Serialization)] [JsonSerializable(typeof(ClassWithBadCustomConverter), GenerationMode = JsonSourceGenerationMode.Metadata | JsonSourceGenerationMode.Serialization)] [JsonSerializable(typeof(StructWithBadCustomConverter), GenerationMode = JsonSourceGenerationMode.Metadata | JsonSourceGenerationMode.Serialization)] [JsonSerializable(typeof(PersonStruct?), GenerationMode = JsonSourceGenerationMode.Metadata | JsonSourceGenerationMode.Serialization)] 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 1d482e1cd8223..d0cef8311b67e 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 @@ -6,7 +6,6 @@ using System.Linq; using System.Text.Json.Serialization; using System.Text.Json.Serialization.Metadata; -using System.Text.Json.Serialization.Tests; using Xunit; namespace System.Text.Json.SourceGeneration.Tests @@ -257,6 +256,60 @@ public virtual void RoundtripWithCustomConverterProperty_Class() Assert.Equal(42, obj.Property.Value); } + [Fact] + public virtual void RoundTripWithCustomConverterNullableProperty() + { + const string Json = "{\"TimeSpan\":42}"; + + var obj = new ClassWithCustomConverterNullableProperty + { + TimeSpan = TimeSpan.FromSeconds(42) + }; + + // Types with properties in custom converters do not support fast path serialization. + Assert.True(DefaultContext.ClassWithCustomConverterNullableProperty.SerializeHandler is null); + + if (DefaultContext.JsonSourceGenerationMode == JsonSourceGenerationMode.Serialization) + { + Assert.Throws(() => JsonSerializer.Serialize(obj, DefaultContext.ClassWithCustomConverterNullableProperty)); + } + else + { + string json = JsonSerializer.Serialize(obj, DefaultContext.ClassWithCustomConverterNullableProperty); + Assert.Equal(Json, json); + + obj = JsonSerializer.Deserialize(Json, DefaultContext.ClassWithCustomConverterNullableProperty); + Assert.Equal(42, obj.TimeSpan.Value.TotalSeconds); + } + } + + [Fact] + public virtual void RoundTripWithCustomConverterFactoryNullableProperty() + { + const string Json = "{\"MyEnum\":\"Two\"}"; + + var obj = new ClassWithCustomConverterFactoryNullableProperty + { + MyEnum = SourceGenSampleEnum.Two + }; + + // Types with properties in custom converters do not support fast path serialization. + Assert.True(DefaultContext.ClassWithCustomConverterFactoryNullableProperty.SerializeHandler is null); + + if (DefaultContext.JsonSourceGenerationMode == JsonSourceGenerationMode.Serialization) + { + Assert.Throws(() => JsonSerializer.Serialize(obj, DefaultContext.ClassWithCustomConverterFactoryNullableProperty)); + } + else + { + string json = JsonSerializer.Serialize(obj, DefaultContext.ClassWithCustomConverterFactoryNullableProperty); + Assert.Equal(Json, json); + + obj = JsonSerializer.Deserialize(Json, DefaultContext.ClassWithCustomConverterFactoryNullableProperty); + Assert.Equal(SourceGenSampleEnum.Two, obj.MyEnum.Value); + } + } + [Fact] public virtual void RoundtripWithCustomConverterProperty_Struct() { 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 837e896d5be75..4ff2b9033513c 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 @@ -46,6 +46,8 @@ namespace System.Text.Json.SourceGeneration.Tests [JsonSerializable(typeof(StructWithCustomConverterProperty))] [JsonSerializable(typeof(ClassWithCustomConverterFactoryProperty))] [JsonSerializable(typeof(StructWithCustomConverterFactoryProperty))] + [JsonSerializable(typeof(ClassWithCustomConverterNullableProperty))] + [JsonSerializable(typeof(ClassWithCustomConverterFactoryNullableProperty))] [JsonSerializable(typeof(ClassWithBadCustomConverter))] [JsonSerializable(typeof(StructWithBadCustomConverter))] [JsonSerializable(typeof(PersonStruct?))] @@ -95,6 +97,8 @@ internal partial class SerializationContext : JsonSerializerContext, ITestContex [JsonSerializable(typeof(StructWithCustomConverterProperty), GenerationMode = JsonSourceGenerationMode.Serialization)] [JsonSerializable(typeof(ClassWithCustomConverterFactoryProperty), GenerationMode = JsonSourceGenerationMode.Serialization)] [JsonSerializable(typeof(StructWithCustomConverterFactoryProperty), GenerationMode = JsonSourceGenerationMode.Serialization)] + [JsonSerializable(typeof(ClassWithCustomConverterNullableProperty), GenerationMode = JsonSourceGenerationMode.Serialization)] + [JsonSerializable(typeof(ClassWithCustomConverterFactoryNullableProperty), GenerationMode = JsonSourceGenerationMode.Serialization)] [JsonSerializable(typeof(ClassWithBadCustomConverter), GenerationMode = JsonSourceGenerationMode.Serialization)] [JsonSerializable(typeof(StructWithBadCustomConverter), GenerationMode = JsonSourceGenerationMode.Serialization)] [JsonSerializable(typeof(PersonStruct?), GenerationMode = JsonSourceGenerationMode.Serialization)] @@ -145,6 +149,8 @@ internal partial class SerializationWithPerTypeAttributeContext : JsonSerializer [JsonSerializable(typeof(StructWithCustomConverterProperty), GenerationMode = JsonSourceGenerationMode.Serialization)] [JsonSerializable(typeof(ClassWithCustomConverterFactoryProperty), GenerationMode = JsonSourceGenerationMode.Serialization)] [JsonSerializable(typeof(StructWithCustomConverterFactoryProperty), GenerationMode = JsonSourceGenerationMode.Serialization)] + [JsonSerializable(typeof(ClassWithCustomConverterNullableProperty), GenerationMode = JsonSourceGenerationMode.Serialization)] + [JsonSerializable(typeof(ClassWithCustomConverterFactoryNullableProperty), GenerationMode = JsonSourceGenerationMode.Serialization)] [JsonSerializable(typeof(ClassWithBadCustomConverter), GenerationMode = JsonSourceGenerationMode.Serialization)] [JsonSerializable(typeof(StructWithBadCustomConverter), GenerationMode = JsonSourceGenerationMode.Serialization)] [JsonSerializable(typeof(PersonStruct?), GenerationMode = JsonSourceGenerationMode.Serialization)] diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/TestClasses.CustomConverters.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/TestClasses.CustomConverters.cs index 18762d22bcee3..7ed8c5e6bf0ac 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/TestClasses.CustomConverters.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/TestClasses.CustomConverters.cs @@ -257,6 +257,31 @@ public struct StructWithCustomConverterFactoryProperty public SourceGenSampleEnum MyEnum { get; set; } } + public class ClassWithCustomConverterFactoryNullableProperty + { + [JsonConverter(typeof(JsonStringEnumConverter))] // This converter is a JsonConverterFactory + public SourceGenSampleEnum? MyEnum { get; set; } + } + + public class ClassWithCustomConverterNullableProperty + { + [JsonConverter(typeof(TimeSpanSecondsConverter))] + public TimeSpan? TimeSpan { get; set; } + } + + public class TimeSpanSecondsConverter : JsonConverter + { + public override TimeSpan Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return TimeSpan.FromSeconds(reader.GetDouble()); + } + + public override void Write(Utf8JsonWriter writer, TimeSpan value, JsonSerializerOptions options) + { + writer.WriteNumberValue(value.TotalSeconds); + } + } + [JsonConverter(typeof(CustomConverter_StructWithCustomConverter))] // Invalid public class ClassWithBadCustomConverter {