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";
private readonly Dictionary<string, string> _propertyNames = new();
/// <summary>
+ /// Indicates that the type graph contains a nullable property with a design-time custom converter declaration.
+ /// </summary>
+ private bool _emitGetConverterForNullablePropertyMethod;
+
+ /// <summary>
/// The SourceText emit implementation filled by the individual Roslyn versions.
/// </summary>
private partial void AddSource(string hintName, SourceText sourceText);
{
Debug.Assert(_typeIndex.Count == 0);
Debug.Assert(_propertyNames.Count == 0);
+ Debug.Assert(!_emitGetConverterForNullablePropertyMethod);
foreach (TypeGenerationSpec spec in contextGenerationSpec.GeneratedTypes)
{
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));
// Add property name initialization.
AddSource($"{contextName}.PropertyNames.g.cs", GetPropertyNameInitialization(contextGenerationSpec));
+ _emitGetConverterForNullablePropertyMethod = false;
_propertyNames.Clear();
_typeIndex.Clear();
}
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<PropertyGenerationSpec> properties = typeGenerationSpec.PropertyGenSpecs;
? $"{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}}>()
IsPublic = {{FormatBool(property.IsPublic)}},
IsVirtual = {{FormatBool(property.IsVirtual)}},
DeclaringType = typeof({{property.DeclaringType.FullyQualifiedName}}),
- Converter = {{converterInstantiationExpr}},
+ Converter = {{converterInstantiationExpr ?? "null"}},
Getter = {{getterValue}},
Setter = {{setterValue}},
IgnoreCondition = {{ignoreConditionNamedArg}},
""");
}
- private static SourceText GetRootJsonContextImplementation(ContextGenerationSpec contextSpec)
+ private static SourceText GetRootJsonContextImplementation(ContextGenerationSpec contextSpec, bool emitGetConverterForNullablePropertyMethod)
{
string contextTypeRef = contextSpec.ContextType.FullyQualifiedName;
string contextTypeName = contextSpec.ContextType.Name;
writer.WriteLine();
- GenerateConverterHelpers(writer);
+ GenerateConverterHelpers(writer, emitGetConverterForNullablePropertyMethod);
return CompleteSourceFileAndReturnText(writer);
}
""");
}
- 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.
{{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);
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)
public JsonTypeInfo<StructWithCustomConverterProperty> StructWithCustomConverterProperty { get; }
public JsonTypeInfo<ClassWithCustomConverterFactoryProperty> ClassWithCustomConverterFactoryProperty { get; }
public JsonTypeInfo<StructWithCustomConverterFactoryProperty> StructWithCustomConverterFactoryProperty { get; }
+ public JsonTypeInfo<ClassWithCustomConverterNullableProperty> ClassWithCustomConverterNullableProperty { get; }
+ public JsonTypeInfo<ClassWithCustomConverterFactoryNullableProperty> ClassWithCustomConverterFactoryNullableProperty { get; }
public JsonTypeInfo<ClassWithBadCustomConverter> ClassWithBadCustomConverter { get; }
public JsonTypeInfo<StructWithBadCustomConverter> StructWithBadCustomConverter { get; }
public JsonTypeInfo<PersonStruct?> NullablePersonStruct { get; }
[JsonSerializable(typeof(StructWithCustomConverterProperty))]
[JsonSerializable(typeof(ClassWithCustomConverterFactoryProperty))]
[JsonSerializable(typeof(StructWithCustomConverterFactoryProperty))]
+ [JsonSerializable(typeof(ClassWithCustomConverterNullableProperty))]
+ [JsonSerializable(typeof(ClassWithCustomConverterFactoryNullableProperty))]
[JsonSerializable(typeof(ClassWithBadCustomConverter))]
[JsonSerializable(typeof(StructWithBadCustomConverter))]
[JsonSerializable(typeof(PersonStruct?))]
[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)]
[JsonSerializable(typeof(StructWithCustomConverterProperty))]
[JsonSerializable(typeof(ClassWithCustomConverterFactoryProperty))]
[JsonSerializable(typeof(StructWithCustomConverterFactoryProperty))]
+ [JsonSerializable(typeof(ClassWithCustomConverterNullableProperty))]
+ [JsonSerializable(typeof(ClassWithCustomConverterFactoryNullableProperty))]
[JsonSerializable(typeof(ClassWithBadCustomConverter))]
[JsonSerializable(typeof(StructWithBadCustomConverter))]
[JsonSerializable(typeof(PersonStruct?))]
[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)]
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
}
[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<InvalidOperationException>(() => 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<InvalidOperationException>(() => 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()
{
const string ExpectedJson = "{\"Property\":42}";
[JsonSerializable(typeof(StructWithCustomConverterProperty))]
[JsonSerializable(typeof(ClassWithCustomConverterFactoryProperty))]
[JsonSerializable(typeof(StructWithCustomConverterFactoryProperty))]
+ [JsonSerializable(typeof(ClassWithCustomConverterNullableProperty))]
+ [JsonSerializable(typeof(ClassWithCustomConverterFactoryNullableProperty))]
[JsonSerializable(typeof(ClassWithBadCustomConverter))]
[JsonSerializable(typeof(StructWithBadCustomConverter))]
[JsonSerializable(typeof(PersonStruct?))]
[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)]
[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)]
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<TimeSpan>
+ {
+ 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
{
}
[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<DateTimeOffset>
+ {
+ 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()
{
string source = """