Skip to content

Commit 2a1b52a

Browse files
Use underlying type converter for nullable type (#84208)
* Use underlying type converter for nullable type * verify that we use underlying type converter for nullable types * rollback a little change * Update src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Unit.Tests/JsonSourceGeneratorTests.cs * Update src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Unit.Tests/JsonSourceGeneratorTests.cs * Use consistent indentation. * Fix runtime support for nullable properties with custom converters. --------- Co-authored-by: Eirik Tsarpalis <[email protected]>
1 parent f924653 commit 2a1b52a

File tree

9 files changed

+203
-17
lines changed

9 files changed

+203
-17
lines changed

src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs

Lines changed: 49 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ private sealed partial class Emitter
3333
private const string PropInitMethodNameSuffix = "PropInit";
3434
private const string TryGetTypeInfoForRuntimeCustomConverterMethodName = "TryGetTypeInfoForRuntimeCustomConverter";
3535
private const string ExpandConverterMethodName = "ExpandConverter";
36+
private const string GetConverterForNullablePropertyMethodName = "GetConverterForNullableProperty";
3637
private const string SerializeHandlerPropName = "SerializeHandler";
3738
private const string OptionsLocalVariableName = "options";
3839
private const string ValueVarName = "value";
@@ -79,6 +80,11 @@ private sealed partial class Emitter
7980
/// </summary>
8081
private readonly Dictionary<string, string> _propertyNames = new();
8182

83+
/// <summary>
84+
/// Indicates that the type graph contains a nullable property with a design-time custom converter declaration.
85+
/// </summary>
86+
private bool _emitGetConverterForNullablePropertyMethod;
87+
8288
/// <summary>
8389
/// The SourceText emit implementation filled by the individual Roslyn versions.
8490
/// </summary>
@@ -88,6 +94,7 @@ public void Emit(ContextGenerationSpec contextGenerationSpec)
8894
{
8995
Debug.Assert(_typeIndex.Count == 0);
9096
Debug.Assert(_propertyNames.Count == 0);
97+
Debug.Assert(!_emitGetConverterForNullablePropertyMethod);
9198

9299
foreach (TypeGenerationSpec spec in contextGenerationSpec.GeneratedTypes)
93100
{
@@ -106,14 +113,15 @@ public void Emit(ContextGenerationSpec contextGenerationSpec)
106113
string contextName = contextGenerationSpec.ContextType.Name;
107114

108115
// Add root context implementation.
109-
AddSource($"{contextName}.g.cs", GetRootJsonContextImplementation(contextGenerationSpec));
116+
AddSource($"{contextName}.g.cs", GetRootJsonContextImplementation(contextGenerationSpec, _emitGetConverterForNullablePropertyMethod));
110117

111118
// Add GetJsonTypeInfo override implementation.
112119
AddSource($"{contextName}.GetJsonTypeInfo.g.cs", GetGetTypeInfoImplementation(contextGenerationSpec));
113120

114121
// Add property name initialization.
115122
AddSource($"{contextName}.PropertyNames.g.cs", GetPropertyNameInitialization(contextGenerationSpec));
116123

124+
_emitGetConverterForNullablePropertyMethod = false;
117125
_propertyNames.Clear();
118126
_typeIndex.Clear();
119127
}
@@ -539,7 +547,7 @@ private SourceText GenerateForObject(ContextGenerationSpec contextSpec, TypeGene
539547
return CompleteSourceFileAndReturnText(writer);
540548
}
541549

542-
private static void GeneratePropMetadataInitFunc(SourceWriter writer, string propInitMethodName, TypeGenerationSpec typeGenerationSpec)
550+
private void GeneratePropMetadataInitFunc(SourceWriter writer, string propInitMethodName, TypeGenerationSpec typeGenerationSpec)
543551
{
544552
Debug.Assert(typeGenerationSpec.PropertyGenSpecs != null);
545553
ImmutableEquatableArray<PropertyGenerationSpec> properties = typeGenerationSpec.PropertyGenSpecs;
@@ -585,9 +593,15 @@ private static void GeneratePropMetadataInitFunc(SourceWriter writer, string pro
585593
? $"{JsonIgnoreConditionTypeRef}.{property.DefaultIgnoreCondition.Value}"
586594
: "null";
587595

588-
string converterInstantiationExpr = property.ConverterType != null
589-
? $"({JsonConverterTypeRef}<{propertyTypeFQN}>){ExpandConverterMethodName}(typeof({propertyTypeFQN}), new {property.ConverterType.FullyQualifiedName}(), {OptionsLocalVariableName})"
590-
: "null";
596+
string? converterInstantiationExpr = null;
597+
if (property.ConverterType != null)
598+
{
599+
TypeRef? nullableUnderlyingType = _typeIndex[property.PropertyType].NullableUnderlyingType;
600+
_emitGetConverterForNullablePropertyMethod |= nullableUnderlyingType != null;
601+
converterInstantiationExpr = nullableUnderlyingType != null
602+
? $"{GetConverterForNullablePropertyMethodName}<{nullableUnderlyingType.FullyQualifiedName}>(new {property.ConverterType.FullyQualifiedName}(), {OptionsLocalVariableName})"
603+
: $"({JsonConverterTypeRef}<{propertyTypeFQN}>){ExpandConverterMethodName}(typeof({propertyTypeFQN}), new {property.ConverterType.FullyQualifiedName}(), {OptionsLocalVariableName})";
604+
}
591605

592606
writer.WriteLine($$"""
593607
var {{InfoVarName}}{{i}} = new {{JsonPropertyInfoValuesTypeRef}}<{{propertyTypeFQN}}>()
@@ -596,7 +610,7 @@ private static void GeneratePropMetadataInitFunc(SourceWriter writer, string pro
596610
IsPublic = {{FormatBool(property.IsPublic)}},
597611
IsVirtual = {{FormatBool(property.IsVirtual)}},
598612
DeclaringType = typeof({{property.DeclaringType.FullyQualifiedName}}),
599-
Converter = {{converterInstantiationExpr}},
613+
Converter = {{converterInstantiationExpr ?? "null"}},
600614
Getter = {{getterValue}},
601615
Setter = {{setterValue}},
602616
IgnoreCondition = {{ignoreConditionNamedArg}},
@@ -1007,7 +1021,7 @@ private static void GenerateTypeInfoProperty(SourceWriter writer, TypeGeneration
10071021
""");
10081022
}
10091023

1010-
private static SourceText GetRootJsonContextImplementation(ContextGenerationSpec contextSpec)
1024+
private static SourceText GetRootJsonContextImplementation(ContextGenerationSpec contextSpec, bool emitGetConverterForNullablePropertyMethod)
10111025
{
10121026
string contextTypeRef = contextSpec.ContextType.FullyQualifiedName;
10131027
string contextTypeName = contextSpec.ContextType.Name;
@@ -1048,7 +1062,7 @@ private static SourceText GetRootJsonContextImplementation(ContextGenerationSpec
10481062

10491063
writer.WriteLine();
10501064

1051-
GenerateConverterHelpers(writer);
1065+
GenerateConverterHelpers(writer, emitGetConverterForNullablePropertyMethod);
10521066

10531067
return CompleteSourceFileAndReturnText(writer);
10541068
}
@@ -1082,7 +1096,7 @@ private static void GetLogicForDefaultSerializerOptionsInit(ContextGenerationSpe
10821096
""");
10831097
}
10841098

1085-
private static void GenerateConverterHelpers(SourceWriter writer)
1099+
private static void GenerateConverterHelpers(SourceWriter writer, bool emitGetConverterForNullablePropertyMethod)
10861100
{
10871101
// The generic type parameter could capture type parameters from containing types,
10881102
// so use a name that is unlikely to be used.
@@ -1109,15 +1123,20 @@ private static void GenerateConverterHelpers(SourceWriter writer)
11091123
{{JsonConverterTypeRef}}? converter = options.Converters[i];
11101124
if (converter?.CanConvert(type) == true)
11111125
{
1112-
return {{ExpandConverterMethodName}}(type, converter, options);
1126+
return {{ExpandConverterMethodName}}(type, converter, options, validateCanConvert: false);
11131127
}
11141128
}
11151129
11161130
return null;
11171131
}
11181132
1119-
private static {{JsonConverterTypeRef}} {{ExpandConverterMethodName}}({{TypeTypeRef}} type, {{JsonConverterTypeRef}} converter, {{JsonSerializerOptionsTypeRef}} options)
1133+
private static {{JsonConverterTypeRef}} {{ExpandConverterMethodName}}({{TypeTypeRef}} type, {{JsonConverterTypeRef}} converter, {{JsonSerializerOptionsTypeRef}} options, bool validateCanConvert = true)
11201134
{
1135+
if (validateCanConvert && !converter.CanConvert(type))
1136+
{
1137+
throw new {{InvalidOperationExceptionTypeRef}}(string.Format("{{ExceptionMessages.IncompatibleConverterType}}", converter.GetType(), type));
1138+
}
1139+
11211140
if (converter is {{JsonConverterFactoryTypeRef}} factory)
11221141
{
11231142
converter = factory.CreateConverter(type, options);
@@ -1126,15 +1145,29 @@ private static void GenerateConverterHelpers(SourceWriter writer)
11261145
throw new {{InvalidOperationExceptionTypeRef}}(string.Format("{{ExceptionMessages.InvalidJsonConverterFactoryOutput}}", factory.GetType()));
11271146
}
11281147
}
1129-
1130-
if (!converter.CanConvert(type))
1131-
{
1132-
throw new {{InvalidOperationExceptionTypeRef}}(string.Format("{{ExceptionMessages.IncompatibleConverterType}}", converter.GetType(), type));
1133-
}
11341148
11351149
return converter;
11361150
}
11371151
""");
1152+
1153+
if (emitGetConverterForNullablePropertyMethod)
1154+
{
1155+
writer.WriteLine($$"""
1156+
1157+
private static {{JsonConverterTypeRef}}<{{TypeParameter}}?> {{GetConverterForNullablePropertyMethodName}}<{{TypeParameter}}>({{JsonConverterTypeRef}} converter, {{JsonSerializerOptionsTypeRef}} options)
1158+
where {{TypeParameter}} : struct
1159+
{
1160+
if (converter.CanConvert(typeof({{TypeParameter}}?)))
1161+
{
1162+
return ({{JsonConverterTypeRef}}<{{TypeParameter}}?>){{ExpandConverterMethodName}}(typeof({{TypeParameter}}?), converter, options, validateCanConvert: false);
1163+
}
1164+
1165+
converter = {{ExpandConverterMethodName}}(typeof({{TypeParameter}}), converter, options);
1166+
{{JsonTypeInfoTypeRef}}<{{TypeParameter}}> typeInfo = {{JsonMetadataServicesTypeRef}}.{{CreateValueInfoMethodName}}<{{TypeParameter}}>(options, converter);
1167+
return {{JsonMetadataServicesTypeRef}}.GetNullableConverter<{{TypeParameter}}>(typeInfo);
1168+
}
1169+
""");
1170+
}
11381171
}
11391172

11401173
private static SourceText GetGetTypeInfoImplementation(ContextGenerationSpec contextSpec)

src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/ContextClasses.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ public interface ITestContext
4949
public JsonTypeInfo<StructWithCustomConverterProperty> StructWithCustomConverterProperty { get; }
5050
public JsonTypeInfo<ClassWithCustomConverterFactoryProperty> ClassWithCustomConverterFactoryProperty { get; }
5151
public JsonTypeInfo<StructWithCustomConverterFactoryProperty> StructWithCustomConverterFactoryProperty { get; }
52+
public JsonTypeInfo<ClassWithCustomConverterNullableProperty> ClassWithCustomConverterNullableProperty { get; }
53+
public JsonTypeInfo<ClassWithCustomConverterFactoryNullableProperty> ClassWithCustomConverterFactoryNullableProperty { get; }
5254
public JsonTypeInfo<ClassWithBadCustomConverter> ClassWithBadCustomConverter { get; }
5355
public JsonTypeInfo<StructWithBadCustomConverter> StructWithBadCustomConverter { get; }
5456
public JsonTypeInfo<PersonStruct?> NullablePersonStruct { get; }

src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/MetadataAndSerializationContextTests.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ namespace System.Text.Json.SourceGeneration.Tests
4545
[JsonSerializable(typeof(StructWithCustomConverterProperty))]
4646
[JsonSerializable(typeof(ClassWithCustomConverterFactoryProperty))]
4747
[JsonSerializable(typeof(StructWithCustomConverterFactoryProperty))]
48+
[JsonSerializable(typeof(ClassWithCustomConverterNullableProperty))]
49+
[JsonSerializable(typeof(ClassWithCustomConverterFactoryNullableProperty))]
4850
[JsonSerializable(typeof(ClassWithBadCustomConverter))]
4951
[JsonSerializable(typeof(StructWithBadCustomConverter))]
5052
[JsonSerializable(typeof(PersonStruct?))]

src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/MetadataContextTests.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ namespace System.Text.Json.SourceGeneration.Tests
4444
[JsonSerializable(typeof(StructWithCustomConverterProperty), GenerationMode = JsonSourceGenerationMode.Metadata)]
4545
[JsonSerializable(typeof(ClassWithCustomConverterFactoryProperty), GenerationMode = JsonSourceGenerationMode.Metadata)]
4646
[JsonSerializable(typeof(StructWithCustomConverterFactoryProperty), GenerationMode = JsonSourceGenerationMode.Metadata)]
47+
[JsonSerializable(typeof(ClassWithCustomConverterNullableProperty), GenerationMode = JsonSourceGenerationMode.Metadata)]
48+
[JsonSerializable(typeof(ClassWithCustomConverterFactoryNullableProperty), GenerationMode = JsonSourceGenerationMode.Metadata)]
4749
[JsonSerializable(typeof(ClassWithBadCustomConverter), GenerationMode = JsonSourceGenerationMode.Metadata)]
4850
[JsonSerializable(typeof(StructWithBadCustomConverter), GenerationMode = JsonSourceGenerationMode.Metadata)]
4951
[JsonSerializable(typeof(PersonStruct?), GenerationMode = JsonSourceGenerationMode.Metadata)]
@@ -144,6 +146,8 @@ public override void EnsureFastPathGeneratedAsExpected()
144146
[JsonSerializable(typeof(StructWithCustomConverterProperty))]
145147
[JsonSerializable(typeof(ClassWithCustomConverterFactoryProperty))]
146148
[JsonSerializable(typeof(StructWithCustomConverterFactoryProperty))]
149+
[JsonSerializable(typeof(ClassWithCustomConverterNullableProperty))]
150+
[JsonSerializable(typeof(ClassWithCustomConverterFactoryNullableProperty))]
147151
[JsonSerializable(typeof(ClassWithBadCustomConverter))]
148152
[JsonSerializable(typeof(StructWithBadCustomConverter))]
149153
[JsonSerializable(typeof(PersonStruct?))]

src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/MixedModeContextTests.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ namespace System.Text.Json.SourceGeneration.Tests
4545
[JsonSerializable(typeof(StructWithCustomConverterProperty), GenerationMode = JsonSourceGenerationMode.Metadata | JsonSourceGenerationMode.Serialization)]
4646
[JsonSerializable(typeof(ClassWithCustomConverterFactoryProperty), GenerationMode = JsonSourceGenerationMode.Metadata | JsonSourceGenerationMode.Serialization)]
4747
[JsonSerializable(typeof(StructWithCustomConverterFactoryProperty), GenerationMode = JsonSourceGenerationMode.Metadata | JsonSourceGenerationMode.Serialization)]
48+
[JsonSerializable(typeof(ClassWithCustomConverterNullableProperty), GenerationMode = JsonSourceGenerationMode.Metadata | JsonSourceGenerationMode.Serialization)]
49+
[JsonSerializable(typeof(ClassWithCustomConverterFactoryNullableProperty), GenerationMode = JsonSourceGenerationMode.Metadata | JsonSourceGenerationMode.Serialization)]
4850
[JsonSerializable(typeof(ClassWithBadCustomConverter), GenerationMode = JsonSourceGenerationMode.Metadata | JsonSourceGenerationMode.Serialization)]
4951
[JsonSerializable(typeof(StructWithBadCustomConverter), GenerationMode = JsonSourceGenerationMode.Metadata | JsonSourceGenerationMode.Serialization)]
5052
[JsonSerializable(typeof(PersonStruct?), GenerationMode = JsonSourceGenerationMode.Metadata | JsonSourceGenerationMode.Serialization)]

src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/RealWorldContextTests.cs

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
using System.Linq;
77
using System.Text.Json.Serialization;
88
using System.Text.Json.Serialization.Metadata;
9-
using System.Text.Json.Serialization.Tests;
109
using Xunit;
1110

1211
namespace System.Text.Json.SourceGeneration.Tests
@@ -257,6 +256,60 @@ public virtual void RoundtripWithCustomConverterProperty_Class()
257256
Assert.Equal(42, obj.Property.Value);
258257
}
259258

259+
[Fact]
260+
public virtual void RoundTripWithCustomConverterNullableProperty()
261+
{
262+
const string Json = "{\"TimeSpan\":42}";
263+
264+
var obj = new ClassWithCustomConverterNullableProperty
265+
{
266+
TimeSpan = TimeSpan.FromSeconds(42)
267+
};
268+
269+
// Types with properties in custom converters do not support fast path serialization.
270+
Assert.True(DefaultContext.ClassWithCustomConverterNullableProperty.SerializeHandler is null);
271+
272+
if (DefaultContext.JsonSourceGenerationMode == JsonSourceGenerationMode.Serialization)
273+
{
274+
Assert.Throws<InvalidOperationException>(() => JsonSerializer.Serialize(obj, DefaultContext.ClassWithCustomConverterNullableProperty));
275+
}
276+
else
277+
{
278+
string json = JsonSerializer.Serialize(obj, DefaultContext.ClassWithCustomConverterNullableProperty);
279+
Assert.Equal(Json, json);
280+
281+
obj = JsonSerializer.Deserialize(Json, DefaultContext.ClassWithCustomConverterNullableProperty);
282+
Assert.Equal(42, obj.TimeSpan.Value.TotalSeconds);
283+
}
284+
}
285+
286+
[Fact]
287+
public virtual void RoundTripWithCustomConverterFactoryNullableProperty()
288+
{
289+
const string Json = "{\"MyEnum\":\"Two\"}";
290+
291+
var obj = new ClassWithCustomConverterFactoryNullableProperty
292+
{
293+
MyEnum = SourceGenSampleEnum.Two
294+
};
295+
296+
// Types with properties in custom converters do not support fast path serialization.
297+
Assert.True(DefaultContext.ClassWithCustomConverterFactoryNullableProperty.SerializeHandler is null);
298+
299+
if (DefaultContext.JsonSourceGenerationMode == JsonSourceGenerationMode.Serialization)
300+
{
301+
Assert.Throws<InvalidOperationException>(() => JsonSerializer.Serialize(obj, DefaultContext.ClassWithCustomConverterFactoryNullableProperty));
302+
}
303+
else
304+
{
305+
string json = JsonSerializer.Serialize(obj, DefaultContext.ClassWithCustomConverterFactoryNullableProperty);
306+
Assert.Equal(Json, json);
307+
308+
obj = JsonSerializer.Deserialize(Json, DefaultContext.ClassWithCustomConverterFactoryNullableProperty);
309+
Assert.Equal(SourceGenSampleEnum.Two, obj.MyEnum.Value);
310+
}
311+
}
312+
260313
[Fact]
261314
public virtual void RoundtripWithCustomConverterProperty_Struct()
262315
{

0 commit comments

Comments
 (0)