[release/6.0] Support JsonConverterFactory with src-gen (#58652)
authorgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Tue, 7 Sep 2021 18:18:08 +0000 (11:18 -0700)
committerGitHub <noreply@github.com>
Tue, 7 Sep 2021 18:18:08 +0000 (11:18 -0700)
* Support JsonConverterFactory with src-gen

* Support property factory converters

* Fixup merge ordering; add tests

* Ensure context helper methods properly generated for implicit context types

Co-authored-by: Steve Harter <steveharter@users.noreply.github.com>
13 files changed:
src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs
src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs
src/libraries/System.Text.Json/gen/PropertyGenerationSpec.cs
src/libraries/System.Text.Json/gen/TypeGenerationSpec.cs
src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/ContextClasses.cs
src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/MetadataAndSerializationContextTests.cs
src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/MetadataContextTests.cs
src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/MixedModeContextTests.cs
src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/RealWorldContextTests.cs
src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/SerializationContextTests.cs
src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/System.Text.Json.SourceGeneration.Tests.csproj
src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/TestClasses.CustomConverters.cs [new file with mode: 0644]
src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/TestClasses.cs

index 77f8e78..b2b3488 100644 (file)
@@ -19,17 +19,19 @@ namespace System.Text.Json.SourceGeneration
         private sealed partial class Emitter
         {
             // Literals in generated source
-            private const string RuntimeCustomConverterFetchingMethodName = "GetRuntimeProvidedCustomConverter";
-            private const string OptionsInstanceVariableName = "Options";
-            private const string PropInitMethodNameSuffix = "PropInit";
-            private const string CtorParamInitMethodNameSuffix = "CtorParamInit";
-            private const string SerializeMethodNameSuffix = "Serialize";
             private const string CreateValueInfoMethodName = "CreateValueInfo";
+            private const string CtorParamInitMethodNameSuffix = "CtorParamInit";
             private const string DefaultOptionsStaticVarName = "s_defaultOptions";
             private const string DefaultContextBackingStaticVarName = "s_defaultContext";
-            private const string WriterVarName = "writer";
-            private const string ValueVarName = "value";
+            internal const string GetConverterFromFactoryMethodName = "GetConverterFromFactory";
             private const string JsonSerializerContextName = "JsonSerializerContext";
+            internal const string JsonContextVarName = "jsonContext";
+            private const string OptionsInstanceVariableName = "Options";
+            private const string PropInitMethodNameSuffix = "PropInit";
+            private const string RuntimeCustomConverterFetchingMethodName = "GetRuntimeProvidedCustomConverter";
+            private const string SerializeMethodNameSuffix = "Serialize";
+            private const string ValueVarName = "value";
+            private const string WriterVarName = "writer";
 
             private static AssemblyName _assemblyName = typeof(Emitter).Assembly.GetName();
             private static readonly string s_generatedCodeAttributeSource = $@"
@@ -99,15 +101,24 @@ namespace System.Text.Json.SourceGeneration
                 {
                     _currentContext = contextGenerationSpec;
 
+                    bool generateGetConverterMethodForTypes = false;
+                    bool generateGetConverterMethodForProperties = false;
+
                     foreach (TypeGenerationSpec typeGenerationSpec in _currentContext.RootSerializableTypes)
                     {
                         GenerateTypeInfo(typeGenerationSpec);
+
+                        generateGetConverterMethodForTypes |= typeGenerationSpec.HasTypeFactoryConverter;
+                        generateGetConverterMethodForProperties |= typeGenerationSpec.HasPropertyFactoryConverters;
                     }
 
                     string contextName = _currentContext.ContextType.Name;
 
                     // Add root context implementation.
-                    AddSource($"{contextName}.g.cs", GetRootJsonContextImplementation(), isRootContextDef: true);
+                    AddSource(
+                        $"{contextName}.g.cs",
+                        GetRootJsonContextImplementation(generateGetConverterMethodForTypes, generateGetConverterMethodForProperties),
+                        isRootContextDef: true);
 
                     // Add GetJsonTypeInfo override implementation.
                     AddSource($"{contextName}.GetJsonTypeInfo.g.cs", GetGetTypeInfoImplementation());
@@ -283,54 +294,42 @@ namespace {@namespace}
                 string typeCompilableName = typeMetadata.TypeRef;
                 string typeFriendlyName = typeMetadata.TypeInfoPropertyName;
 
-                string metadataInitSource;
-
                 // TODO (https://github.com/dotnet/runtime/issues/52218): consider moving this verification source to common helper.
+                StringBuilder metadataInitSource = new(
+                    $@"{JsonConverterTypeRef} converter = {typeMetadata.ConverterInstantiationLogic};
+                    {TypeTypeRef} typeToConvert = typeof({typeCompilableName});");
+
                 if (typeMetadata.IsValueType)
                 {
-                    metadataInitSource = $@"{JsonConverterTypeRef} converter = {typeMetadata.ConverterInstantiationLogic};
-                    {TypeTypeRef} typeToConvert = typeof({typeCompilableName});
-                    if (!converter.CanConvert(typeToConvert))
-                    {{
-                        {TypeTypeRef}? underlyingType = {NullableTypeRef}.GetUnderlyingType(typeToConvert);
-                        if (underlyingType != null && converter.CanConvert(underlyingType))
+                    metadataInitSource.Append($@"
+                        if (!converter.CanConvert(typeToConvert))
                         {{
-                            {JsonConverterTypeRef}? actualConverter = converter;
-
-                            if (converter is {JsonConverterFactoryTypeRef} converterFactory)
+                            {TypeTypeRef}? underlyingType = {NullableTypeRef}.GetUnderlyingType(typeToConvert);
+                            if (underlyingType != null && converter.CanConvert(underlyingType))
                             {{
-                                actualConverter = converterFactory.CreateConverter(underlyingType, {OptionsInstanceVariableName});
-
-                                if (actualConverter == null || actualConverter is {JsonConverterFactoryTypeRef})
-                                {{
-                                    throw new {InvalidOperationExceptionTypeRef}($""JsonConverterFactory '{{converter}} cannot return a 'null' or 'JsonConverterFactory' value."");
-                                }}
+                                // Allow nullable handling to forward to the underlying type's converter.
+                                converter = {JsonMetadataServicesTypeRef}.GetNullableConverter<{typeCompilableName}>(this.{typeFriendlyName})!;
+                                converter = (({ JsonConverterFactoryTypeRef })converter).CreateConverter(typeToConvert, { OptionsInstanceVariableName })!;
                             }}
-
-                            // Allow nullable handling to forward to the underlying type's converter.
-                            converter = {JsonMetadataServicesTypeRef}.GetNullableConverter<{typeCompilableName}>(this.{typeFriendlyName});
-                        }}
-                        else
-                        {{
-                            throw new {InvalidOperationExceptionTypeRef}($""The converter '{{converter.GetType()}}' is not compatible with the type '{{typeToConvert}}'."");
-                        }}
-                    }}
-
-                    _{typeFriendlyName} = {JsonMetadataServicesTypeRef}.{GetCreateValueInfoMethodRef(typeCompilableName)}({OptionsInstanceVariableName}, converter);";
+                            else
+                            {{
+                                throw new {InvalidOperationExceptionTypeRef}($""The converter '{{converter.GetType()}}' is not compatible with the type '{{typeToConvert}}'."");
+                            }}
+                        }}");
                 }
                 else
                 {
-                    metadataInitSource = $@"{JsonConverterTypeRef} converter = {typeMetadata.ConverterInstantiationLogic};
-                    {TypeTypeRef} typeToConvert = typeof({typeCompilableName});
-                    if (!converter.CanConvert(typeToConvert))
-                    {{
-                        throw new {InvalidOperationExceptionTypeRef}($""The converter '{{converter.GetType()}}' is not compatible with the type '{{typeToConvert}}'."");
-                    }}
-
-                    _{typeFriendlyName} = {JsonMetadataServicesTypeRef}.{GetCreateValueInfoMethodRef(typeCompilableName)}({OptionsInstanceVariableName}, converter);";
+                    metadataInitSource.Append($@"
+                        if (!converter.CanConvert(typeToConvert))
+                        {{
+                            throw new {InvalidOperationExceptionTypeRef}($""The converter '{{converter.GetType()}}' is not compatible with the type '{{typeToConvert}}'."");
+                        }}");
                 }
 
-                return GenerateForType(typeMetadata, metadataInitSource);
+                metadataInitSource.Append($@"
+                    _{typeFriendlyName} = { JsonMetadataServicesTypeRef }.{ GetCreateValueInfoMethodRef(typeCompilableName)} ({ OptionsInstanceVariableName}, converter); ");
+
+                return GenerateForType(typeMetadata, metadataInitSource.ToString());
             }
 
             private string GenerateForNullable(TypeGenerationSpec typeMetadata)
@@ -484,7 +483,7 @@ namespace {@namespace}
 
                 Type elementType = valueTypeGenerationSpec.Type;
                 string? writerMethodToCall = GetWriterMethod(elementType);
-                
+
                 string iterationLogic;
                 string valueToWrite;
 
@@ -638,7 +637,6 @@ namespace {@namespace}
             private string GeneratePropMetadataInitFunc(TypeGenerationSpec typeGenerationSpec)
             {
                 const string PropVarName = "properties";
-                const string JsonContextVarName = "jsonContext";
 
                 List<PropertyGenerationSpec> properties = typeGenerationSpec.PropertyGenSpecList!;
 
@@ -1093,7 +1091,9 @@ public {typeInfoPropertyTypeRef} {typeFriendlyName}
                 }}";
             }
 
-            private string GetRootJsonContextImplementation()
+            private string GetRootJsonContextImplementation(
+                bool generateGetConverterMethodForTypes,
+                bool generateGetConverterMethodForProperties)
             {
                 string contextTypeRef = _currentContext.ContextTypeRef;
                 string contextTypeName = _currentContext.ContextType.Name;
@@ -1115,6 +1115,16 @@ public {contextTypeName}({JsonSerializerOptionsTypeRef} options) : base(options,
 
 {GetFetchLogicForRuntimeSpecifiedCustomConverter()}");
 
+                if (generateGetConverterMethodForProperties)
+                {
+                    sb.Append(GetFetchLogicForGetCustomConverter_PropertiesWithFactories());
+                }
+
+                if (generateGetConverterMethodForProperties || generateGetConverterMethodForTypes)
+                {
+                    sb.Append(GetFetchLogicForGetCustomConverter_TypesWithFactories());
+                }
+
                 return sb.ToString();
             }
 
@@ -1173,6 +1183,32 @@ private static {JsonSerializerOptionsTypeRef} {DefaultOptionsStaticVarName} {{ g
 }}";
             }
 
+            private string GetFetchLogicForGetCustomConverter_PropertiesWithFactories()
+            {
+                return @$"
+
+private {JsonConverterTypeRef}<T> {GetConverterFromFactoryMethodName}<T>({JsonConverterFactoryTypeRef} factory)
+{{
+    return ({JsonConverterTypeRef}<T>) {GetConverterFromFactoryMethodName}(typeof(T), factory);
+}}";
+            }
+
+            private string GetFetchLogicForGetCustomConverter_TypesWithFactories()
+            {
+                return @$"
+
+private {JsonConverterTypeRef} {GetConverterFromFactoryMethodName}({TypeTypeRef} type, {JsonConverterFactoryTypeRef} factory)
+{{
+    {JsonConverterTypeRef}? converter = factory.CreateConverter(type, {Emitter.OptionsInstanceVariableName});
+    if (converter == null || converter is {JsonConverterFactoryTypeRef})
+    {{
+        throw new {InvalidOperationExceptionTypeRef}($""The converter '{{factory.GetType()}}' cannot return null or a JsonConverterFactory instance."");
+    }}
+     
+    return converter;
+}}";
+            }
+
             private string GetGetTypeInfoImplementation()
             {
                 StringBuilder sb = new();
index cd47bca..fecdb37 100644 (file)
@@ -25,6 +25,7 @@ namespace System.Text.Json.SourceGeneration
             private const string SystemTextJsonNamespace = "System.Text.Json";
             private const string JsonConverterAttributeFullName = "System.Text.Json.Serialization.JsonConverterAttribute";
             private const string JsonElementFullName = "System.Text.Json.JsonElement";
+            private const string JsonConverterFactoryFullName = "System.Text.Json.Serialization.JsonConverterFactory";
             private const string JsonIgnoreAttributeFullName = "System.Text.Json.Serialization.JsonIgnoreAttribute";
             private const string JsonIgnoreConditionFullName = "System.Text.Json.Serialization.JsonIgnoreCondition";
             private const string JsonIncludeAttributeFullName = "System.Text.Json.Serialization.JsonIncludeAttribute";
@@ -571,6 +572,8 @@ namespace System.Text.Json.SourceGeneration
                 string? converterInstatiationLogic = null;
                 bool implementsIJsonOnSerialized = false;
                 bool implementsIJsonOnSerializing = false;
+                bool hasTypeFactoryConverter = false;
+                bool hasPropertyFactoryConverters = false;
 
                 IList<CustomAttributeData> attributeDataList = CustomAttributeData.GetCustomAttributes(type);
                 foreach (CustomAttributeData attributeData in attributeDataList)
@@ -585,7 +588,11 @@ namespace System.Text.Json.SourceGeneration
                     else if (!foundDesignTimeCustomConverter && attributeType.GetCompatibleBaseClass(JsonConverterAttributeFullName) != null)
                     {
                         foundDesignTimeCustomConverter = true;
-                        converterInstatiationLogic = GetConverterInstantiationLogic(attributeData);
+                        converterInstatiationLogic = GetConverterInstantiationLogic(
+                            type,
+                            attributeData,
+                            forType: true,
+                            ref hasTypeFactoryConverter);
                     }
                 }
 
@@ -816,6 +823,8 @@ namespace System.Text.Json.SourceGeneration
 
                         for (Type? currentType = type; currentType != null; currentType = currentType.BaseType)
                         {
+                            PropertyGenerationSpec spec;
+
                             foreach (PropertyInfo propertyInfo in currentType.GetProperties(bindingFlags))
                             {
                                 bool isVirtual = propertyInfo.IsVirtual();
@@ -826,9 +835,8 @@ namespace System.Text.Json.SourceGeneration
                                     continue;
                                 }
 
-                                PropertyGenerationSpec spec = GetPropertyGenerationSpec(propertyInfo, isVirtual, generationMode);
-                                CacheMember(spec, ref propGenSpecList, ref ignoredMembers);
-                                propertyOrderSpecified |= spec.Order != 0;
+                                spec = GetPropertyGenerationSpec(propertyInfo, isVirtual, generationMode);
+                                CacheMemberHelper();
                             }
 
                             foreach (FieldInfo fieldInfo in currentType.GetFields(bindingFlags))
@@ -838,9 +846,19 @@ namespace System.Text.Json.SourceGeneration
                                     continue;
                                 }
 
-                                PropertyGenerationSpec spec = GetPropertyGenerationSpec(fieldInfo, isVirtual: false, generationMode);
+                                spec = GetPropertyGenerationSpec(fieldInfo, isVirtual: false, generationMode);
+                                CacheMemberHelper();
+                            }
+
+                            void CacheMemberHelper()
+                            {
                                 CacheMember(spec, ref propGenSpecList, ref ignoredMembers);
+
                                 propertyOrderSpecified |= spec.Order != 0;
+                                hasPropertyFactoryConverters |= spec.HasFactoryConverter;
+
+                                // The property type may be implicitly in the context, so add that as well.
+                                hasTypeFactoryConverter |= spec.TypeGenerationSpec.HasTypeFactoryConverter;
                             }
                         }
 
@@ -864,8 +882,10 @@ namespace System.Text.Json.SourceGeneration
                     constructionStrategy,
                     nullableUnderlyingTypeMetadata: nullableUnderlyingTypeGenSpec,
                     converterInstatiationLogic,
-                    implementsIJsonOnSerialized,
-                    implementsIJsonOnSerializing);
+                    implementsIJsonOnSerialized : implementsIJsonOnSerialized,
+                    implementsIJsonOnSerializing : implementsIJsonOnSerializing,
+                    hasTypeFactoryConverter : hasTypeFactoryConverter,
+                    hasPropertyFactoryConverters : hasPropertyFactoryConverters);
 
                 return typeMetadata;
             }
@@ -905,16 +925,95 @@ namespace System.Text.Json.SourceGeneration
 
             private PropertyGenerationSpec GetPropertyGenerationSpec(MemberInfo memberInfo, bool isVirtual, JsonSourceGenerationMode generationMode)
             {
+                Type memberCLRType = GetMemberClrType(memberInfo);
                 IList<CustomAttributeData> attributeDataList = CustomAttributeData.GetCustomAttributes(memberInfo);
 
-                bool hasJsonInclude = false;
-                JsonIgnoreCondition? ignoreCondition = null;
-                JsonNumberHandling? numberHandling = null;
-                string? jsonPropertyName = null;
+                ProcessCustomAttributes(
+                    attributeDataList,
+                    memberCLRType,
+                    out bool hasJsonInclude,
+                    out string? jsonPropertyName,
+                    out JsonIgnoreCondition? ignoreCondition,
+                    out JsonNumberHandling? numberHandling,
+                    out string? converterInstantiationLogic,
+                    out int order,
+                    out bool hasFactoryConverter);
+
+                ProcessMember(
+                    memberInfo,
+                    memberCLRType,
+                    hasJsonInclude,
+                    out bool isReadOnly,
+                    out bool isPublic,
+                    out bool canUseGetter,
+                    out bool canUseSetter,
+                    out bool getterIsVirtual,
+                    out bool setterIsVirtual);
+
+                string clrName = memberInfo.Name;
+                string runtimePropertyName = DetermineRuntimePropName(clrName, jsonPropertyName, _currentContextNamingPolicy);
+                string propertyNameVarName = DeterminePropNameIdentifier(runtimePropertyName);
+
+                return new PropertyGenerationSpec
+                {
+                    ClrName = clrName,
+                    IsProperty = memberInfo.MemberType == MemberTypes.Property,
+                    IsPublic = isPublic,
+                    IsVirtual = isVirtual,
+                    JsonPropertyName = jsonPropertyName,
+                    RuntimePropertyName = runtimePropertyName,
+                    PropertyNameVarName = propertyNameVarName,
+                    IsReadOnly = isReadOnly,
+                    CanUseGetter = canUseGetter,
+                    CanUseSetter = canUseSetter,
+                    GetterIsVirtual = getterIsVirtual,
+                    SetterIsVirtual = setterIsVirtual,
+                    DefaultIgnoreCondition = ignoreCondition,
+                    NumberHandling = numberHandling,
+                    Order = order,
+                    HasJsonInclude = hasJsonInclude,
+                    TypeGenerationSpec = GetOrAddTypeGenerationSpec(memberCLRType, generationMode),
+                    DeclaringTypeRef = memberInfo.DeclaringType.GetCompilableName(),
+                    ConverterInstantiationLogic = converterInstantiationLogic,
+                    HasFactoryConverter = hasFactoryConverter
+                };
+            }
+
+            private Type GetMemberClrType(MemberInfo memberInfo)
+            {
+                if (memberInfo is PropertyInfo propertyInfo)
+                {
+                    return propertyInfo.PropertyType;
+                }
+
+                if (memberInfo is FieldInfo fieldInfo)
+                {
+                    return fieldInfo.FieldType;
+                }
+
+                throw new InvalidOperationException();
+            }
+
+            private void ProcessCustomAttributes(
+                IList<CustomAttributeData> attributeDataList,
+                Type memberCLRType,
+                out bool hasJsonInclude,
+                out string? jsonPropertyName,
+                out JsonIgnoreCondition? ignoreCondition,
+                out JsonNumberHandling? numberHandling,
+                out string? converterInstantiationLogic,
+                out int order,
+                out bool hasFactoryConverter)
+            {
+                hasJsonInclude = false;
+                jsonPropertyName = null;
+                ignoreCondition = default;
+                numberHandling = default;
+                converterInstantiationLogic = null;
+                order = 0;
 
                 bool foundDesignTimeCustomConverter = false;
-                string? converterInstantiationLogic = null;
-                int order = 0;
+                hasFactoryConverter = false;
 
                 foreach (CustomAttributeData attributeData in attributeDataList)
                 {
@@ -923,7 +1022,11 @@ namespace System.Text.Json.SourceGeneration
                     if (!foundDesignTimeCustomConverter && attributeType.GetCompatibleBaseClass(JsonConverterAttributeFullName) != null)
                     {
                         foundDesignTimeCustomConverter = true;
-                        converterInstantiationLogic = GetConverterInstantiationLogic(attributeData);
+                        converterInstantiationLogic = GetConverterInstantiationLogic(
+                            memberCLRType,
+                            attributeData,
+                            forType: false,
+                            ref hasFactoryConverter);
                     }
                     else if (attributeType.Assembly.FullName == SystemTextJsonNamespace)
                     {
@@ -974,21 +1077,29 @@ namespace System.Text.Json.SourceGeneration
                         }
                     }
                 }
+            }
 
-                Type memberCLRType;
-                bool isReadOnly;
-                bool isPublic = false;
-                bool canUseGetter = false;
-                bool canUseSetter = false;
-                bool getterIsVirtual = false;
-                bool setterIsVirtual = false;
+            private static void ProcessMember(
+                MemberInfo memberInfo,
+                Type memberClrType,
+                bool hasJsonInclude,
+                out bool isReadOnly,
+                out bool isPublic,
+                out bool canUseGetter,
+                out bool canUseSetter,
+                out bool getterIsVirtual,
+                out bool setterIsVirtual)
+            {
+                isPublic = false;
+                canUseGetter = false;
+                canUseSetter = false;
+                getterIsVirtual = false;
+                setterIsVirtual = false;
 
                 switch (memberInfo)
                 {
                     case PropertyInfo propertyInfo:
                         {
-                            memberCLRType = propertyInfo.PropertyType;
-
                             MethodInfo? getMethod = propertyInfo.GetMethod;
                             MethodInfo? setMethod = propertyInfo.SetMethod;
 
@@ -1031,7 +1142,6 @@ namespace System.Text.Json.SourceGeneration
                         break;
                     case FieldInfo fieldInfo:
                         {
-                            memberCLRType = fieldInfo.FieldType;
                             isPublic = fieldInfo.IsPublic;
                             isReadOnly = fieldInfo.IsInitOnly;
 
@@ -1045,39 +1155,15 @@ namespace System.Text.Json.SourceGeneration
                     default:
                         throw new InvalidOperationException();
                 }
-
-                string clrName = memberInfo.Name;
-                string runtimePropertyName = DetermineRuntimePropName(clrName, jsonPropertyName, _currentContextNamingPolicy);
-                string propertyNameVarName = DeterminePropNameIdentifier(runtimePropertyName);
-
-                return new PropertyGenerationSpec
-                {
-                    ClrName = clrName,
-                    IsProperty = memberInfo.MemberType == MemberTypes.Property,
-                    IsPublic = isPublic,
-                    IsVirtual = isVirtual,
-                    JsonPropertyName = jsonPropertyName,
-                    RuntimePropertyName = runtimePropertyName,
-                    PropertyNameVarName = propertyNameVarName,
-                    IsReadOnly = isReadOnly,
-                    CanUseGetter = canUseGetter,
-                    CanUseSetter = canUseSetter,
-                    GetterIsVirtual = getterIsVirtual,
-                    SetterIsVirtual = setterIsVirtual,
-                    DefaultIgnoreCondition = ignoreCondition,
-                    NumberHandling = numberHandling,
-                    Order = order,
-                    HasJsonInclude = hasJsonInclude,
-                    TypeGenerationSpec = GetOrAddTypeGenerationSpec(memberCLRType, generationMode),
-                    DeclaringTypeRef = memberInfo.DeclaringType.GetCompilableName(),
-                    ConverterInstantiationLogic = converterInstantiationLogic
-                };
             }
 
             private static bool PropertyAccessorCanBeReferenced(MethodInfo? accessor)
                 => accessor != null && (accessor.IsPublic || accessor.IsAssembly);
 
-            private string? GetConverterInstantiationLogic(CustomAttributeData attributeData)
+            private string? GetConverterInstantiationLogic(
+                Type type, CustomAttributeData attributeData,
+                bool forType, // whether for a type or a property
+                ref bool hasFactoryConverter)
             {
                 if (attributeData.AttributeType.FullName != JsonConverterAttributeFullName)
                 {
@@ -1092,6 +1178,20 @@ namespace System.Text.Json.SourceGeneration
                     return null;
                 }
 
+                if (converterType.GetCompatibleBaseClass(JsonConverterFactoryFullName) != null)
+                {
+                    hasFactoryConverter = true;
+
+                    if (forType)
+                    {
+                        return $"{Emitter.GetConverterFromFactoryMethodName}(typeof({type.GetCompilableName()}), new {converterType.GetCompilableName()}())";
+                    }
+                    else
+                    {
+                        return $"{Emitter.JsonContextVarName}.{Emitter.GetConverterFromFactoryMethodName}<{type.GetCompilableName()}>(new {converterType.GetCompilableName()}())";
+                    }
+                }
+
                 return $"new {converterType.GetCompilableName()}()";
             }
 
index 74740cc..886381a 100644 (file)
@@ -89,5 +89,7 @@ namespace System.Text.Json.SourceGeneration
         /// Source code to instantiate design-time specified custom converter.
         /// </summary>
         public string? ConverterInstantiationLogic { get; init; }
+
+        public bool HasFactoryConverter { get; init; }
     }
 }
index 2164861..be51f94 100644 (file)
@@ -60,6 +60,10 @@ namespace System.Text.Json.SourceGeneration
 
         public string? ConverterInstantiationLogic { get; private set; }
 
+        // Only generate certain helper methods if necessary.
+        public bool HasPropertyFactoryConverters { get; private set; }
+        public bool HasTypeFactoryConverter { get; private set; }
+
         public string FastPathSerializeMethodName
         {
             get
@@ -107,7 +111,9 @@ namespace System.Text.Json.SourceGeneration
             TypeGenerationSpec? nullableUnderlyingTypeMetadata,
             string? converterInstantiationLogic,
             bool implementsIJsonOnSerialized,
-            bool implementsIJsonOnSerializing)
+            bool implementsIJsonOnSerializing,
+            bool hasTypeFactoryConverter,
+            bool hasPropertyFactoryConverters)
         {
             GenerationMode = generationMode;
             TypeRef = type.GetCompilableName();
@@ -127,6 +133,8 @@ namespace System.Text.Json.SourceGeneration
             ConverterInstantiationLogic = converterInstantiationLogic;
             ImplementsIJsonOnSerialized = implementsIJsonOnSerialized;
             ImplementsIJsonOnSerializing = implementsIJsonOnSerializing;
+            HasTypeFactoryConverter = hasTypeFactoryConverter;
+            HasPropertyFactoryConverters = hasPropertyFactoryConverters;
         }
 
         public bool TryFilterSerializableProps(
index ab93522..47bfe5a 100644 (file)
@@ -33,8 +33,12 @@ namespace System.Text.Json.SourceGeneration.Tests
         public JsonTypeInfo<RealWorldContextTests.ClassWithEnumAndNullable> ClassWithEnumAndNullable { get; }
         public JsonTypeInfo<ClassWithCustomConverter> ClassWithCustomConverter { get; }
         public JsonTypeInfo<StructWithCustomConverter> StructWithCustomConverter { get; }
+        public JsonTypeInfo<ClassWithCustomConverterFactory> ClassWithCustomConverterFactory { get; }
+        public JsonTypeInfo<StructWithCustomConverterFactory> StructWithCustomConverterFactory { get; }
         public JsonTypeInfo<ClassWithCustomConverterProperty> ClassWithCustomConverterProperty { get; }
         public JsonTypeInfo<StructWithCustomConverterProperty> StructWithCustomConverterProperty { get; }
+        public JsonTypeInfo<ClassWithCustomConverterPropertyFactory> ClassWithCustomConverterPropertyFactory { get; }
+        public JsonTypeInfo<StructWithCustomConverterPropertyFactory> StructWithCustomConverterPropertyFactory { get; }
         public JsonTypeInfo<ClassWithBadCustomConverter> ClassWithBadCustomConverter { get; }
         public JsonTypeInfo<StructWithBadCustomConverter> StructWithBadCustomConverter { get; }
     }
index 6db87ac..201ef07 100644 (file)
@@ -29,8 +29,12 @@ namespace System.Text.Json.SourceGeneration.Tests
     [JsonSerializable(typeof(RealWorldContextTests.ClassWithEnumAndNullable))]
     [JsonSerializable(typeof(ClassWithCustomConverter))]
     [JsonSerializable(typeof(StructWithCustomConverter))]
+    [JsonSerializable(typeof(ClassWithCustomConverterFactory))]
+    [JsonSerializable(typeof(StructWithCustomConverterFactory))]
     [JsonSerializable(typeof(ClassWithCustomConverterProperty))]
     [JsonSerializable(typeof(StructWithCustomConverterProperty))]
+    [JsonSerializable(typeof(ClassWithCustomConverterPropertyFactory))]
+    [JsonSerializable(typeof(StructWithCustomConverterPropertyFactory))]
     [JsonSerializable(typeof(ClassWithBadCustomConverter))]
     [JsonSerializable(typeof(StructWithBadCustomConverter))]
     internal partial class MetadataAndSerializationContext : JsonSerializerContext, ITestContext
@@ -63,12 +67,17 @@ namespace System.Text.Json.SourceGeneration.Tests
             Assert.NotNull(MetadataAndSerializationContext.Default.MyNestedClass.Serialize);
             Assert.NotNull(MetadataAndSerializationContext.Default.MyNestedNestedClass.Serialize);
             Assert.Null(MetadataAndSerializationContext.Default.ObjectArray.Serialize);
+            Assert.Null(MetadataAndSerializationContext.Default.SampleEnum.Serialize);
             Assert.Null(MetadataAndSerializationContext.Default.String.Serialize);
             Assert.NotNull(MetadataAndSerializationContext.Default.ClassWithEnumAndNullable.Serialize);
             Assert.NotNull(MetadataAndSerializationContext.Default.ClassWithCustomConverter);
             Assert.NotNull(MetadataAndSerializationContext.Default.StructWithCustomConverter);
+            Assert.NotNull(MetadataAndSerializationContext.Default.ClassWithCustomConverterFactory);
+            Assert.NotNull(MetadataAndSerializationContext.Default.StructWithCustomConverterFactory);
             Assert.NotNull(MetadataAndSerializationContext.Default.ClassWithCustomConverterProperty);
             Assert.NotNull(MetadataAndSerializationContext.Default.StructWithCustomConverterProperty);
+            Assert.NotNull(MetadataAndSerializationContext.Default.ClassWithCustomConverterPropertyFactory);
+            Assert.NotNull(MetadataAndSerializationContext.Default.StructWithCustomConverterPropertyFactory);
             Assert.Throws<InvalidOperationException>(() => MetadataAndSerializationContext.Default.ClassWithBadCustomConverter);
             Assert.Throws<InvalidOperationException>(() => MetadataAndSerializationContext.Default.StructWithBadCustomConverter);
         }
index 50f52d6..945082d 100644 (file)
@@ -28,10 +28,12 @@ namespace System.Text.Json.SourceGeneration.Tests
     [JsonSerializable(typeof(RealWorldContextTests.ClassWithEnumAndNullable), GenerationMode = JsonSourceGenerationMode.Metadata)]
     [JsonSerializable(typeof(ClassWithCustomConverter), GenerationMode = JsonSourceGenerationMode.Metadata)]
     [JsonSerializable(typeof(StructWithCustomConverter), GenerationMode = JsonSourceGenerationMode.Metadata)]
-    [JsonSerializable(typeof(ClassWithCustomConverter), GenerationMode = JsonSourceGenerationMode.Metadata)]
-    [JsonSerializable(typeof(StructWithCustomConverter), GenerationMode = JsonSourceGenerationMode.Metadata)]
+    [JsonSerializable(typeof(ClassWithCustomConverterFactory), GenerationMode = JsonSourceGenerationMode.Metadata)]
+    [JsonSerializable(typeof(StructWithCustomConverterFactory), GenerationMode = JsonSourceGenerationMode.Metadata)]
     [JsonSerializable(typeof(ClassWithCustomConverterProperty), GenerationMode = JsonSourceGenerationMode.Metadata)]
     [JsonSerializable(typeof(StructWithCustomConverterProperty), GenerationMode = JsonSourceGenerationMode.Metadata)]
+    [JsonSerializable(typeof(ClassWithCustomConverterPropertyFactory), GenerationMode = JsonSourceGenerationMode.Metadata)]
+    [JsonSerializable(typeof(StructWithCustomConverterPropertyFactory), GenerationMode = JsonSourceGenerationMode.Metadata)]
     [JsonSerializable(typeof(ClassWithBadCustomConverter), GenerationMode = JsonSourceGenerationMode.Metadata)]
     [JsonSerializable(typeof(StructWithBadCustomConverter), GenerationMode = JsonSourceGenerationMode.Metadata)]
     internal partial class MetadataWithPerTypeAttributeContext : JsonSerializerContext, ITestContext
@@ -62,10 +64,17 @@ namespace System.Text.Json.SourceGeneration.Tests
             Assert.Null(MetadataWithPerTypeAttributeContext.Default.MyNestedClass.Serialize);
             Assert.Null(MetadataWithPerTypeAttributeContext.Default.MyNestedNestedClass.Serialize);
             Assert.Null(MetadataWithPerTypeAttributeContext.Default.ObjectArray.Serialize);
+            Assert.Null(MetadataWithPerTypeAttributeContext.Default.SampleEnum.Serialize);
             Assert.Null(MetadataWithPerTypeAttributeContext.Default.String.Serialize);
             Assert.Null(MetadataWithPerTypeAttributeContext.Default.ClassWithEnumAndNullable.Serialize);
             Assert.Null(MetadataWithPerTypeAttributeContext.Default.ClassWithCustomConverter.Serialize);
             Assert.Null(MetadataWithPerTypeAttributeContext.Default.StructWithCustomConverter.Serialize);
+            Assert.Null(MetadataWithPerTypeAttributeContext.Default.ClassWithCustomConverterFactory.Serialize);
+            Assert.Null(MetadataWithPerTypeAttributeContext.Default.StructWithCustomConverterFactory.Serialize);
+            Assert.Null(MetadataWithPerTypeAttributeContext.Default.ClassWithCustomConverterProperty.Serialize);
+            Assert.Null(MetadataWithPerTypeAttributeContext.Default.StructWithCustomConverterProperty.Serialize);
+            Assert.Null(MetadataWithPerTypeAttributeContext.Default.ClassWithCustomConverterPropertyFactory.Serialize);
+            Assert.Null(MetadataWithPerTypeAttributeContext.Default.StructWithCustomConverterPropertyFactory.Serialize);
             Assert.Throws<InvalidOperationException>(() => MetadataWithPerTypeAttributeContext.Default.ClassWithBadCustomConverter.Serialize);
             Assert.Throws<InvalidOperationException>(() => MetadataWithPerTypeAttributeContext.Default.StructWithBadCustomConverter.Serialize);
         }
@@ -94,8 +103,12 @@ namespace System.Text.Json.SourceGeneration.Tests
     [JsonSerializable(typeof(RealWorldContextTests.ClassWithEnumAndNullable))]
     [JsonSerializable(typeof(ClassWithCustomConverter))]
     [JsonSerializable(typeof(StructWithCustomConverter))]
+    [JsonSerializable(typeof(ClassWithCustomConverterFactory))]
+    [JsonSerializable(typeof(StructWithCustomConverterFactory))]
     [JsonSerializable(typeof(ClassWithCustomConverterProperty))]
     [JsonSerializable(typeof(StructWithCustomConverterProperty))]
+    [JsonSerializable(typeof(ClassWithCustomConverterPropertyFactory))]
+    [JsonSerializable(typeof(StructWithCustomConverterPropertyFactory))]
     [JsonSerializable(typeof(ClassWithBadCustomConverter))]
     [JsonSerializable(typeof(StructWithBadCustomConverter))]
     internal partial class MetadataContext : JsonSerializerContext, ITestContext
@@ -103,6 +116,27 @@ namespace System.Text.Json.SourceGeneration.Tests
         public JsonSourceGenerationMode JsonSourceGenerationMode => JsonSourceGenerationMode.Metadata;
     }
 
+    [JsonConverter(typeof(JsonStringEnumConverter))]
+    public enum EnumWrittenAsString
+    {
+        A = 1
+    }
+
+    [JsonSerializable(typeof(EnumWrittenAsString))]
+    public partial class ContextWithExplicitStringEnum : JsonSerializerContext
+    {
+    }
+
+    public class PocoWithEnum
+    {
+        public EnumWrittenAsString MyEnum { get; set; }
+    }
+
+    [JsonSerializable(typeof(PocoWithEnum))]
+    public partial class ContextWithImplicitStringEnum : JsonSerializerContext
+    {
+    }
+
     public sealed class MetadataContextTests : RealWorldContextTests
     {
         public MetadataContextTests() : base(MetadataContext.Default, (options) => new MetadataContext(options)) { }
@@ -128,14 +162,56 @@ namespace System.Text.Json.SourceGeneration.Tests
             Assert.Null(MetadataContext.Default.MyNestedClass.Serialize);
             Assert.Null(MetadataContext.Default.MyNestedNestedClass.Serialize);
             Assert.Null(MetadataContext.Default.ObjectArray.Serialize);
+            Assert.Null(MetadataContext.Default.SampleEnum.Serialize);
             Assert.Null(MetadataContext.Default.String.Serialize);
             Assert.Null(MetadataContext.Default.ClassWithEnumAndNullable.Serialize);
             Assert.Null(MetadataContext.Default.ClassWithCustomConverter.Serialize);
             Assert.Null(MetadataContext.Default.StructWithCustomConverter.Serialize);
+            Assert.Null(MetadataContext.Default.ClassWithCustomConverterFactory.Serialize);
+            Assert.Null(MetadataContext.Default.StructWithCustomConverterFactory.Serialize);
             Assert.Null(MetadataContext.Default.ClassWithCustomConverterProperty.Serialize);
             Assert.Null(MetadataContext.Default.StructWithCustomConverterProperty.Serialize);
+            Assert.Null(MetadataContext.Default.ClassWithCustomConverterPropertyFactory.Serialize);
+            Assert.Null(MetadataContext.Default.StructWithCustomConverterPropertyFactory.Serialize);
             Assert.Throws<InvalidOperationException>(() => MetadataContext.Default.ClassWithBadCustomConverter.Serialize);
             Assert.Throws<InvalidOperationException>(() => MetadataContext.Default.StructWithBadCustomConverter.Serialize);
         }
+
+        [Fact]
+        public void EnsureHelperMethodGenerated_TypeFactory()
+        {
+            // There are 2 helper methods generated for obtaining a converter from a factory:
+            // - JsonConverter<T> version that is property-based (that calls the one below)
+            // - JsonConverter version that is Type-based
+            // and this test verifies the latter one is generated. Other tests also have property-level
+            // factories and thus verify both are created.
+
+            const string Json = "\"A\"";
+
+            EnumWrittenAsString obj = EnumWrittenAsString.A;
+
+            string json = JsonSerializer.Serialize(obj, ContextWithExplicitStringEnum.Default.EnumWrittenAsString);
+            Assert.Equal(Json, json);
+
+            obj = JsonSerializer.Deserialize(Json, ContextWithExplicitStringEnum.Default.EnumWrittenAsString);
+            Assert.Equal(EnumWrittenAsString.A, obj);
+        }
+
+        [Fact]
+        public void EnsureHelperMethodGenerated_ImplicitPropertyFactory()
+        {
+            // ContextWithImplicitStringEnum does not have an entry for EnumWrittenAsString since it is
+            // implictly added by PocoWithEnum. Verify helper methods are still being created properly.
+
+            const string Json = "{\"MyEnum\":\"A\"}";
+
+            PocoWithEnum obj = new() { MyEnum = EnumWrittenAsString.A };
+
+            string json = JsonSerializer.Serialize(obj, ContextWithImplicitStringEnum.Default.PocoWithEnum);
+            Assert.Equal(Json, json);
+
+            obj = JsonSerializer.Deserialize(Json, ContextWithImplicitStringEnum.Default.PocoWithEnum);
+            Assert.Equal(EnumWrittenAsString.A, obj.MyEnum);
+        }
     }
 }
index d459963..ae0c289 100644 (file)
@@ -28,8 +28,12 @@ namespace System.Text.Json.SourceGeneration.Tests
     [JsonSerializable(typeof(RealWorldContextTests.ClassWithEnumAndNullable), GenerationMode = JsonSourceGenerationMode.Metadata | JsonSourceGenerationMode.Serialization)]
     [JsonSerializable(typeof(ClassWithCustomConverter), GenerationMode = JsonSourceGenerationMode.Metadata | JsonSourceGenerationMode.Serialization)]
     [JsonSerializable(typeof(StructWithCustomConverter), GenerationMode = JsonSourceGenerationMode.Metadata | JsonSourceGenerationMode.Serialization)]
+    [JsonSerializable(typeof(ClassWithCustomConverterFactory), GenerationMode = JsonSourceGenerationMode.Metadata | JsonSourceGenerationMode.Serialization)]
+    [JsonSerializable(typeof(StructWithCustomConverterFactory), GenerationMode = JsonSourceGenerationMode.Metadata | JsonSourceGenerationMode.Serialization)]
     [JsonSerializable(typeof(ClassWithCustomConverterProperty), GenerationMode = JsonSourceGenerationMode.Metadata | JsonSourceGenerationMode.Serialization)]
     [JsonSerializable(typeof(StructWithCustomConverterProperty), GenerationMode = JsonSourceGenerationMode.Metadata | JsonSourceGenerationMode.Serialization)]
+    [JsonSerializable(typeof(ClassWithCustomConverterPropertyFactory), GenerationMode = JsonSourceGenerationMode.Metadata | JsonSourceGenerationMode.Serialization)]
+    [JsonSerializable(typeof(StructWithCustomConverterPropertyFactory), GenerationMode = JsonSourceGenerationMode.Metadata | JsonSourceGenerationMode.Serialization)]
     [JsonSerializable(typeof(ClassWithBadCustomConverter), GenerationMode = JsonSourceGenerationMode.Metadata | JsonSourceGenerationMode.Serialization)]
     [JsonSerializable(typeof(StructWithBadCustomConverter), GenerationMode = JsonSourceGenerationMode.Metadata | JsonSourceGenerationMode.Serialization)]
     internal partial class MixedModeContext : JsonSerializerContext, ITestContext
@@ -61,12 +65,17 @@ namespace System.Text.Json.SourceGeneration.Tests
             Assert.NotNull(MixedModeContext.Default.MyNestedClass.Serialize);
             Assert.NotNull(MixedModeContext.Default.MyNestedNestedClass.Serialize);
             Assert.Null(MixedModeContext.Default.ObjectArray.Serialize);
+            Assert.Null(MixedModeContext.Default.SampleEnum.Serialize);
             Assert.Null(MixedModeContext.Default.String.Serialize);
             Assert.NotNull(MixedModeContext.Default.ClassWithEnumAndNullable.Serialize);
             Assert.Null(MixedModeContext.Default.ClassWithCustomConverter.Serialize);
             Assert.Null(MixedModeContext.Default.StructWithCustomConverter.Serialize);
+            Assert.Null(MixedModeContext.Default.ClassWithCustomConverterFactory.Serialize);
+            Assert.Null(MixedModeContext.Default.StructWithCustomConverterFactory.Serialize);
             Assert.Null(MixedModeContext.Default.ClassWithCustomConverterProperty.Serialize);
             Assert.Null(MixedModeContext.Default.StructWithCustomConverterProperty.Serialize);
+            Assert.Null(MixedModeContext.Default.ClassWithCustomConverterPropertyFactory.Serialize);
+            Assert.Null(MixedModeContext.Default.StructWithCustomConverterPropertyFactory.Serialize);
             Assert.Throws<InvalidOperationException>(() => MixedModeContext.Default.ClassWithBadCustomConverter.Serialize);
             Assert.Throws<InvalidOperationException>(() => MixedModeContext.Default.StructWithBadCustomConverter.Serialize);
         }
index 5fc9bf7..fef2ddd 100644 (file)
@@ -1,4 +1,4 @@
-// Licensed to the .NET Foundation under one or more agreements.
+// Licensed to the .NET Foundation under one or more agreements.
 // The .NET Foundation licenses this file to you under the MIT license.
 
 using System.Collections.Generic;
@@ -128,6 +128,23 @@ namespace System.Text.Json.SourceGeneration.Tests
         }
 
         [Fact]
+        public virtual void RoundTripWithCustomConverterFactory_Class()
+        {
+            const string Json = "{\"MyInt\":142}";
+
+            ClassWithCustomConverterFactory obj = new()
+            {
+                MyInt = 42
+            };
+
+            string json = JsonSerializer.Serialize(obj, DefaultContext.ClassWithCustomConverterFactory);
+            Assert.Equal(Json, json);
+
+            obj = JsonSerializer.Deserialize(Json, DefaultContext.ClassWithCustomConverterFactory);
+            Assert.Equal(42, obj.MyInt);
+        }
+
+        [Fact]
         public virtual void RoundTripWithCustomConverter_Struct()
         {
             const string Json = "{\"MyInt\":142}";
@@ -199,6 +216,68 @@ namespace System.Text.Json.SourceGeneration.Tests
         }
 
         [Fact]
+        public virtual void RoundTripWithCustomPropertyConverterFactory_Class()
+        {
+            const string Json = "{\"MyEnum\":\"A\"}";
+
+            ClassWithCustomConverterPropertyFactory obj = new()
+            {
+                MyEnum = SampleEnum.A
+            };
+
+            if (DefaultContext.JsonSourceGenerationMode == JsonSourceGenerationMode.Serialization)
+            {
+                Assert.Throws<InvalidOperationException>(() => JsonSerializer.Serialize(obj, DefaultContext.ClassWithCustomConverterPropertyFactory));
+            }
+            else
+            {
+                string json = JsonSerializer.Serialize(obj, DefaultContext.ClassWithCustomConverterPropertyFactory);
+                Assert.Equal(Json, json);
+            }
+
+            if (DefaultContext.JsonSourceGenerationMode == JsonSourceGenerationMode.Serialization)
+            {
+                Assert.Throws<InvalidOperationException>(() => JsonSerializer.Serialize(obj, DefaultContext.ClassWithCustomConverterPropertyFactory));
+            }
+            else
+            {
+                obj = JsonSerializer.Deserialize(Json, DefaultContext.ClassWithCustomConverterPropertyFactory);
+                Assert.Equal(SampleEnum.A, obj.MyEnum);
+            }
+        }
+
+        [Fact]
+        public virtual void RoundTripWithCustomPropertyConverterFactory_Struct()
+        {
+            const string Json = "{\"MyEnum\":\"A\"}";
+
+            StructWithCustomConverterPropertyFactory obj = new()
+            {
+                MyEnum = SampleEnum.A
+            };
+
+            if (DefaultContext.JsonSourceGenerationMode == JsonSourceGenerationMode.Serialization)
+            {
+                Assert.Throws<InvalidOperationException>(() => JsonSerializer.Serialize(obj, DefaultContext.StructWithCustomConverterPropertyFactory));
+            }
+            else
+            {
+                string json = JsonSerializer.Serialize(obj, DefaultContext.StructWithCustomConverterPropertyFactory);
+                Assert.Equal(Json, json);
+            }
+
+            if (DefaultContext.JsonSourceGenerationMode == JsonSourceGenerationMode.Serialization)
+            {
+                Assert.Throws<InvalidOperationException>(() => JsonSerializer.Serialize(obj, DefaultContext.StructWithCustomConverterPropertyFactory));
+            }
+            else
+            {
+                obj = JsonSerializer.Deserialize(Json, DefaultContext.StructWithCustomConverterPropertyFactory);
+                Assert.Equal(SampleEnum.A, obj.MyEnum);
+            }
+        }
+
+        [Fact]
         public virtual void BadCustomConverter_Class()
         {
             const string Json = "{\"MyInt\":142}";
index ae87218..501d671 100644 (file)
@@ -29,8 +29,12 @@ namespace System.Text.Json.SourceGeneration.Tests
     [JsonSerializable(typeof(RealWorldContextTests.ClassWithEnumAndNullable))]
     [JsonSerializable(typeof(ClassWithCustomConverter))]
     [JsonSerializable(typeof(StructWithCustomConverter))]
+    [JsonSerializable(typeof(ClassWithCustomConverterFactory))]
+    [JsonSerializable(typeof(StructWithCustomConverterFactory))]
     [JsonSerializable(typeof(ClassWithCustomConverterProperty))]
     [JsonSerializable(typeof(StructWithCustomConverterProperty))]
+    [JsonSerializable(typeof(ClassWithCustomConverterPropertyFactory))]
+    [JsonSerializable(typeof(StructWithCustomConverterPropertyFactory))]
     [JsonSerializable(typeof(ClassWithBadCustomConverter))]
     [JsonSerializable(typeof(StructWithBadCustomConverter))]
     internal partial class SerializationContext : JsonSerializerContext, ITestContext
@@ -60,8 +64,12 @@ namespace System.Text.Json.SourceGeneration.Tests
     [JsonSerializable(typeof(RealWorldContextTests.ClassWithEnumAndNullable), GenerationMode = JsonSourceGenerationMode.Serialization)]
     [JsonSerializable(typeof(ClassWithCustomConverter), GenerationMode = JsonSourceGenerationMode.Serialization)]
     [JsonSerializable(typeof(StructWithCustomConverter), GenerationMode = JsonSourceGenerationMode.Serialization)]
+    [JsonSerializable(typeof(ClassWithCustomConverterFactory), GenerationMode = JsonSourceGenerationMode.Serialization)]
+    [JsonSerializable(typeof(StructWithCustomConverterFactory), GenerationMode = JsonSourceGenerationMode.Serialization)]
     [JsonSerializable(typeof(ClassWithCustomConverterProperty), GenerationMode = JsonSourceGenerationMode.Serialization)]
     [JsonSerializable(typeof(StructWithCustomConverterProperty), GenerationMode = JsonSourceGenerationMode.Serialization)]
+    [JsonSerializable(typeof(ClassWithCustomConverterPropertyFactory), GenerationMode = JsonSourceGenerationMode.Serialization)]
+    [JsonSerializable(typeof(StructWithCustomConverterPropertyFactory), GenerationMode = JsonSourceGenerationMode.Serialization)]
     [JsonSerializable(typeof(ClassWithBadCustomConverter), GenerationMode = JsonSourceGenerationMode.Serialization)]
     [JsonSerializable(typeof(StructWithBadCustomConverter), GenerationMode = JsonSourceGenerationMode.Serialization)]
     internal partial class SerializationWithPerTypeAttributeContext : JsonSerializerContext, ITestContext
@@ -92,8 +100,12 @@ namespace System.Text.Json.SourceGeneration.Tests
     [JsonSerializable(typeof(RealWorldContextTests.ClassWithEnumAndNullable), GenerationMode = JsonSourceGenerationMode.Serialization)]
     [JsonSerializable(typeof(ClassWithCustomConverter), GenerationMode = JsonSourceGenerationMode.Serialization)]
     [JsonSerializable(typeof(StructWithCustomConverter), GenerationMode = JsonSourceGenerationMode.Serialization)]
+    [JsonSerializable(typeof(ClassWithCustomConverterFactory), GenerationMode = JsonSourceGenerationMode.Serialization)]
+    [JsonSerializable(typeof(StructWithCustomConverterFactory), GenerationMode = JsonSourceGenerationMode.Serialization)]
     [JsonSerializable(typeof(ClassWithCustomConverterProperty), GenerationMode = JsonSourceGenerationMode.Serialization)]
     [JsonSerializable(typeof(StructWithCustomConverterProperty), GenerationMode = JsonSourceGenerationMode.Serialization)]
+    [JsonSerializable(typeof(ClassWithCustomConverterPropertyFactory), GenerationMode = JsonSourceGenerationMode.Serialization)]
+    [JsonSerializable(typeof(StructWithCustomConverterPropertyFactory), GenerationMode = JsonSourceGenerationMode.Serialization)]
     [JsonSerializable(typeof(ClassWithBadCustomConverter), GenerationMode = JsonSourceGenerationMode.Serialization)]
     [JsonSerializable(typeof(StructWithBadCustomConverter), GenerationMode = JsonSourceGenerationMode.Serialization)]
     internal partial class SerializationContextWithCamelCase : JsonSerializerContext, ITestContext
@@ -135,6 +147,8 @@ namespace System.Text.Json.SourceGeneration.Tests
             Assert.NotNull(SerializationContext.Default.ClassWithEnumAndNullable.Serialize);
             Assert.Null(SerializationContext.Default.ClassWithCustomConverter.Serialize);
             Assert.Null(SerializationContext.Default.StructWithCustomConverter.Serialize);
+            Assert.Null(SerializationContext.Default.ClassWithCustomConverterFactory.Serialize);
+            Assert.Null(SerializationContext.Default.StructWithCustomConverterFactory.Serialize);
             Assert.Null(SerializationContext.Default.ClassWithCustomConverterProperty.Serialize);
             Assert.Null(SerializationContext.Default.StructWithCustomConverterProperty.Serialize);
             Assert.Throws<InvalidOperationException>(() => SerializationContext.Default.ClassWithBadCustomConverter.Serialize);
@@ -395,12 +409,17 @@ namespace System.Text.Json.SourceGeneration.Tests
             Assert.NotNull(SerializationWithPerTypeAttributeContext.Default.MyNestedClass.Serialize);
             Assert.NotNull(SerializationWithPerTypeAttributeContext.Default.MyNestedNestedClass.Serialize);
             Assert.Null(SerializationWithPerTypeAttributeContext.Default.ObjectArray.Serialize);
+            Assert.Null(SerializationWithPerTypeAttributeContext.Default.SampleEnum.Serialize);
             Assert.Null(SerializationWithPerTypeAttributeContext.Default.String.Serialize);
             Assert.NotNull(SerializationWithPerTypeAttributeContext.Default.ClassWithEnumAndNullable.Serialize);
             Assert.Null(SerializationWithPerTypeAttributeContext.Default.ClassWithCustomConverter.Serialize);
             Assert.Null(SerializationWithPerTypeAttributeContext.Default.StructWithCustomConverter.Serialize);
+            Assert.Null(SerializationWithPerTypeAttributeContext.Default.ClassWithCustomConverterFactory.Serialize);
+            Assert.Null(SerializationWithPerTypeAttributeContext.Default.StructWithCustomConverterFactory.Serialize);
             Assert.Null(SerializationWithPerTypeAttributeContext.Default.ClassWithCustomConverterProperty.Serialize);
             Assert.Null(SerializationWithPerTypeAttributeContext.Default.StructWithCustomConverterProperty.Serialize);
+            Assert.Null(SerializationWithPerTypeAttributeContext.Default.ClassWithCustomConverterPropertyFactory.Serialize);
+            Assert.Null(SerializationWithPerTypeAttributeContext.Default.StructWithCustomConverterPropertyFactory.Serialize);
             Assert.Throws<InvalidOperationException>(() => SerializationWithPerTypeAttributeContext.Default.ClassWithBadCustomConverter.Serialize);
             Assert.Throws<InvalidOperationException>(() => SerializationWithPerTypeAttributeContext.Default.StructWithBadCustomConverter.Serialize);
         }
index e848e3e..610accb 100644 (file)
     <Compile Include="MetadataAndSerializationContextTests.cs" />
     <Compile Include="MetadataContextTests.cs" />
     <Compile Include="MixedModeContextTests.cs" />
+    <Compile Include="RealWorldContextTests.cs" />
     <Compile Include="SerializationContextTests.cs" />
     <Compile Include="SerializationLogicTests.cs" />
     <Compile Include="Serialization\PropertyNameTests.cs" />
     <Compile Include="Serialization\PropertyVisibilityTests.cs" />
     <Compile Include="TestClasses.cs" />
-    <Compile Include="RealWorldContextTests.cs" />
+    <Compile Include="TestClasses.CustomConverters.cs" />
   </ItemGroup>
 
   <Target Name="FixIncrementalCoreCompileWithAnalyzers" BeforeTargets="CoreCompile">
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
new file mode 100644 (file)
index 0000000..27cf7fb
--- /dev/null
@@ -0,0 +1,277 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Text.Json.Serialization;
+
+namespace System.Text.Json.SourceGeneration.Tests
+{
+    /// <summary>
+    /// Custom converter that adds\substract 100 from MyIntProperty.
+    /// </summary>
+    public class CustomConverter_ClassWithCustomConverter : JsonConverter<ClassWithCustomConverter>
+    {
+        public override ClassWithCustomConverter Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+        {
+            if (reader.TokenType != JsonTokenType.StartObject)
+            {
+                throw new JsonException("No StartObject");
+            }
+
+            ClassWithCustomConverter obj = new();
+
+            reader.Read();
+            if (reader.TokenType != JsonTokenType.PropertyName &&
+                reader.GetString() != "MyInt")
+            {
+                throw new JsonException("Wrong property name");
+            }
+
+            reader.Read();
+            obj.MyInt = reader.GetInt32() - 100;
+
+            reader.Read();
+            if (reader.TokenType != JsonTokenType.EndObject)
+            {
+                throw new JsonException("No EndObject");
+            }
+
+            return obj;
+        }
+
+        public override void Write(Utf8JsonWriter writer, ClassWithCustomConverter value, JsonSerializerOptions options)
+        {
+            writer.WriteStartObject();
+            writer.WriteNumber(nameof(ClassWithCustomConverter.MyInt), value.MyInt + 100);
+            writer.WriteEndObject();
+        }
+    }
+
+    /// <summary>
+    /// Custom converter that adds\substract 100 from MyIntProperty.
+    /// </summary>
+    public class CustomConverter_ClassWithCustomConverterFactory : JsonConverter<ClassWithCustomConverterFactory>
+    {
+        public override ClassWithCustomConverterFactory Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+        {
+            if (reader.TokenType != JsonTokenType.StartObject)
+            {
+                throw new JsonException("No StartObject");
+            }
+
+            ClassWithCustomConverterFactory obj = new();
+
+            reader.Read();
+            if (reader.TokenType != JsonTokenType.PropertyName &&
+                reader.GetString() != "MyInt")
+            {
+                throw new JsonException("Wrong property name");
+            }
+
+            reader.Read();
+            obj.MyInt = reader.GetInt32() - 100;
+
+            reader.Read();
+            if (reader.TokenType != JsonTokenType.EndObject)
+            {
+                throw new JsonException("No EndObject");
+            }
+
+            return obj;
+        }
+
+        public override void Write(Utf8JsonWriter writer, ClassWithCustomConverterFactory value, JsonSerializerOptions options)
+        {
+            writer.WriteStartObject();
+            writer.WriteNumber(nameof(ClassWithCustomConverterFactory.MyInt), value.MyInt + 100);
+            writer.WriteEndObject();
+        }
+    }
+
+    /// <summary>
+    /// Custom converter that adds\substract 100 from MyIntProperty.
+    /// </summary>
+    public class CustomConverter_StructWithCustomConverter : JsonConverter<StructWithCustomConverter>
+    {
+        public override StructWithCustomConverter Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+        {
+            if (reader.TokenType != JsonTokenType.StartObject)
+            {
+                throw new JsonException("No StartObject");
+            }
+
+            StructWithCustomConverter obj = new();
+
+            reader.Read();
+            if (reader.TokenType != JsonTokenType.PropertyName &&
+                reader.GetString() != "MyInt")
+            {
+                throw new JsonException("Wrong property name");
+            }
+
+            reader.Read();
+            obj.MyInt = reader.GetInt32() - 100;
+
+            reader.Read();
+            if (reader.TokenType != JsonTokenType.EndObject)
+            {
+                throw new JsonException("No EndObject");
+            }
+
+            return obj;
+        }
+
+        public override void Write(Utf8JsonWriter writer, StructWithCustomConverter value, JsonSerializerOptions options)
+        {
+            writer.WriteStartObject();
+            writer.WriteNumber(nameof(StructWithCustomConverter.MyInt), value.MyInt + 100);
+            writer.WriteEndObject();
+        }
+    }
+
+    /// <summary>
+    /// Custom converter that adds\substract 100 from MyIntProperty.
+    /// </summary>
+    public class CustomConverter_StructWithCustomConverterFactory : JsonConverter<StructWithCustomConverterFactory>
+    {
+        public override StructWithCustomConverterFactory Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+        {
+            if (reader.TokenType != JsonTokenType.StartObject)
+            {
+                throw new JsonException("No StartObject");
+            }
+
+            StructWithCustomConverterFactory obj = new();
+
+            reader.Read();
+            if (reader.TokenType != JsonTokenType.PropertyName &&
+                reader.GetString() != "MyInt")
+            {
+                throw new JsonException("Wrong property name");
+            }
+
+            reader.Read();
+            obj.MyInt = reader.GetInt32() - 100;
+
+            reader.Read();
+            if (reader.TokenType != JsonTokenType.EndObject)
+            {
+                throw new JsonException("No EndObject");
+            }
+
+            return obj;
+        }
+
+        public override void Write(Utf8JsonWriter writer, StructWithCustomConverterFactory value, JsonSerializerOptions options)
+        {
+            writer.WriteStartObject();
+            writer.WriteNumber(nameof(StructWithCustomConverterFactory.MyInt), value.MyInt + 100);
+            writer.WriteEndObject();
+        }
+    }
+
+    public class CustomConverterFactory : JsonConverterFactory
+    {
+        public CustomConverterFactory()
+        {
+        }
+
+        public override bool CanConvert(Type typeToConvert)
+        {
+            return (
+                typeToConvert == typeof(StructWithCustomConverterFactory) ||
+                typeToConvert == typeof(ClassWithCustomConverterFactory));
+        }
+
+        public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options)
+        {
+            if (typeToConvert == typeof(StructWithCustomConverterFactory))
+            {
+                return new CustomConverter_StructWithCustomConverterFactory();
+            }
+
+            if (typeToConvert == typeof(ClassWithCustomConverterFactory))
+            {
+                return new CustomConverter_ClassWithCustomConverterFactory();
+            }
+
+            throw new InvalidOperationException("Not expected.");
+        }
+    }
+
+    [JsonConverter(typeof(CustomConverter_ClassWithCustomConverter))]
+    public class ClassWithCustomConverter
+    {
+        public int MyInt { get; set; }
+    }
+
+    [JsonConverter(typeof(CustomConverter_StructWithCustomConverter))]
+    public struct StructWithCustomConverter
+    {
+        public int MyInt { get; set; }
+    }
+
+    [JsonConverter(typeof(CustomConverterFactory))]
+    public class ClassWithCustomConverterFactory
+    {
+        public int MyInt { get; set; }
+    }
+
+    [JsonConverter(typeof(CustomConverterFactory))]
+    public struct StructWithCustomConverterFactory
+    {
+        public int MyInt { get; set; }
+    }
+
+    public class ClassWithCustomConverterProperty
+    {
+        [JsonConverter(typeof(NestedPocoCustomConverter))]
+        public NestedPoco Property { get; set; }
+
+        public class NestedPoco
+        {
+            public int Value { get; set; }
+        }
+
+        public class NestedPocoCustomConverter : JsonConverter<NestedPoco>
+        {
+            public override NestedPoco? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => new NestedPoco { Value = reader.GetInt32() };
+            public override void Write(Utf8JsonWriter writer, NestedPoco value, JsonSerializerOptions options) => writer.WriteNumberValue(value.Value);
+        }
+    }
+
+    public struct StructWithCustomConverterProperty
+    {
+        [JsonConverter(typeof(ClassWithCustomConverterProperty.NestedPocoCustomConverter))]
+        public ClassWithCustomConverterProperty.NestedPoco Property { get; set; }
+    }
+
+    public struct ClassWithCustomConverterPropertyFactory
+    {
+        [JsonConverter(typeof(JsonStringEnumConverter))] // This converter is a JsonConverterFactory
+        public SampleEnum MyEnum { get; set; }
+    }
+
+    public struct StructWithCustomConverterPropertyFactory
+    {
+        [JsonConverter(typeof(JsonStringEnumConverter))] // This converter is a JsonConverterFactory
+        public SampleEnum MyEnum { get; set; }
+    }
+
+    public enum SampleEnum
+    {
+        A = 1,
+        B = 2
+    }
+
+    [JsonConverter(typeof(CustomConverter_StructWithCustomConverter))] // Invalid
+    public class ClassWithBadCustomConverter
+    {
+        public int MyInt { get; set; }
+    }
+
+    [JsonConverter(typeof(CustomConverter_StructWithCustomConverter))] // Invalid
+    public struct StructWithBadCustomConverter
+    {
+        public int MyInt { get; set; }
+    }
+}
index 05e3b0e..887f65b 100644 (file)
@@ -151,133 +151,4 @@ namespace System.Text.Json.SourceGeneration.Tests
     }
 
     internal struct MyStruct { }
-
-    /// <summary>
-    /// Custom converter that adds\substract 100 from MyIntProperty.
-    /// </summary>
-    public class CustomConverterForClass : JsonConverter<ClassWithCustomConverter>
-    {
-        public override ClassWithCustomConverter Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
-        {
-            if (reader.TokenType != JsonTokenType.StartObject)
-            {
-                throw new JsonException("No StartObject");
-            }
-
-            ClassWithCustomConverter obj = new();
-
-            reader.Read();
-            if (reader.TokenType != JsonTokenType.PropertyName &&
-                reader.GetString() != "MyInt")
-            {
-                throw new JsonException("Wrong property name");
-            }
-
-            reader.Read();
-            obj.MyInt = reader.GetInt32() - 100;
-
-            reader.Read();
-            if (reader.TokenType != JsonTokenType.EndObject)
-            {
-                throw new JsonException("No EndObject");
-            }
-
-            return obj;
-        }
-
-        public override void Write(Utf8JsonWriter writer, ClassWithCustomConverter value, JsonSerializerOptions options)
-        {
-            writer.WriteStartObject();
-            writer.WriteNumber(nameof(ClassWithCustomConverter.MyInt), value.MyInt + 100);
-            writer.WriteEndObject();
-        }
-    }
-
-    [JsonConverter(typeof(CustomConverterForClass))]
-    public class ClassWithCustomConverter
-    {
-        public int MyInt { get; set; }
-    }
-
-    public class ClassWithCustomConverterProperty
-    {
-        [JsonConverter(typeof(NestedPocoCustomConverter))]
-        public NestedPoco Property { get; set; }
-
-        public class NestedPoco
-        {
-            public int Value { get; set; }
-        }
-
-        public class NestedPocoCustomConverter : JsonConverter<NestedPoco>
-        {
-            public override NestedPoco? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => new NestedPoco { Value = reader.GetInt32() };
-            public override void Write(Utf8JsonWriter writer, NestedPoco value, JsonSerializerOptions options) => writer.WriteNumberValue(value.Value);
-        }
-    }
-
-    public struct StructWithCustomConverterProperty
-    {
-        [JsonConverter(typeof(ClassWithCustomConverterProperty.NestedPocoCustomConverter))]
-        public ClassWithCustomConverterProperty.NestedPoco Property { get; set; }
-    }
-
-    [JsonConverter(typeof(CustomConverterForStruct))] // Invalid
-    public class ClassWithBadCustomConverter
-    {
-        public int MyInt { get; set; }
-    }
-
-    /// <summary>
-    /// Custom converter that adds\substract 100 from MyIntProperty.
-    /// </summary>
-    public class CustomConverterForStruct : JsonConverter<StructWithCustomConverter>
-    {
-        public override StructWithCustomConverter Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
-        {
-            if (reader.TokenType != JsonTokenType.StartObject)
-            {
-                throw new JsonException("No StartObject");
-            }
-
-            StructWithCustomConverter obj = new();
-
-            reader.Read();
-            if (reader.TokenType != JsonTokenType.PropertyName &&
-                reader.GetString() != "MyInt")
-            {
-                throw new JsonException("Wrong property name");
-            }
-
-            reader.Read();
-            obj.MyInt = reader.GetInt32() - 100;
-
-            reader.Read();
-            if (reader.TokenType != JsonTokenType.EndObject)
-            {
-                throw new JsonException("No EndObject");
-            }
-
-            return obj;
-        }
-
-        public override void Write(Utf8JsonWriter writer, StructWithCustomConverter value, JsonSerializerOptions options)
-        {
-            writer.WriteStartObject();
-            writer.WriteNumber(nameof(StructWithCustomConverter.MyInt), value.MyInt + 100);
-            writer.WriteEndObject();
-        }
-    }
-
-    [JsonConverter(typeof(CustomConverterForStruct))]
-    public struct StructWithCustomConverter
-    {
-        public int MyInt { get; set; }
-    }
-
-    [JsonConverter(typeof(CustomConverterForClass))] // Invalid
-    public struct StructWithBadCustomConverter
-    {
-        public int MyInt { get; set; }
-    }
 }