Implement JsonStringEnumConverter<TEnum> (#87224)
authorEirik Tsarpalis <eirik.tsarpalis@gmail.com>
Wed, 7 Jun 2023 23:20:34 +0000 (00:20 +0100)
committerGitHub <noreply@github.com>
Wed, 7 Jun 2023 23:20:34 +0000 (00:20 +0100)
* Fix NativeAOT support for JsonStringEnumConverter.

* Fix merge issue.

* Implement JsonStringEnumConverter<TEnum>.

* Remove leftover comments.

* Revert unnecessary suppression.

* Update compatibility suppressions.

* Add missing XML docs

* Fix capitalization.

* add local project compatibility suppressions

* apply suggestion in all location

* Fix failing test.

* Ensure both converter factories throw an appropriate exception when passed invalid inputs.

38 files changed:
src/libraries/System.Text.Json/gen/Helpers/KnownTypeSymbols.cs
src/libraries/System.Text.Json/gen/JsonSourceGenerator.DiagnosticDescriptors.cs
src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs
src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs
src/libraries/System.Text.Json/gen/Resources/Strings.resx
src/libraries/System.Text.Json/gen/Resources/xlf/Strings.cs.xlf
src/libraries/System.Text.Json/gen/Resources/xlf/Strings.de.xlf
src/libraries/System.Text.Json/gen/Resources/xlf/Strings.es.xlf
src/libraries/System.Text.Json/gen/Resources/xlf/Strings.fr.xlf
src/libraries/System.Text.Json/gen/Resources/xlf/Strings.it.xlf
src/libraries/System.Text.Json/gen/Resources/xlf/Strings.ja.xlf
src/libraries/System.Text.Json/gen/Resources/xlf/Strings.ko.xlf
src/libraries/System.Text.Json/gen/Resources/xlf/Strings.pl.xlf
src/libraries/System.Text.Json/gen/Resources/xlf/Strings.pt-BR.xlf
src/libraries/System.Text.Json/gen/Resources/xlf/Strings.ru.xlf
src/libraries/System.Text.Json/gen/Resources/xlf/Strings.tr.xlf
src/libraries/System.Text.Json/gen/Resources/xlf/Strings.zh-Hans.xlf
src/libraries/System.Text.Json/gen/Resources/xlf/Strings.zh-Hant.xlf
src/libraries/System.Text.Json/ref/System.Text.Json.cs
src/libraries/System.Text.Json/src/CompatibilitySuppressions.xml [new file with mode: 0644]
src/libraries/System.Text.Json/src/Resources/Strings.resx
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/EnumConverterFactory.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/NullableConverterFactory.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/UnsupportedTypeConverterFactory.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonStringEnumConverter.cs
src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.cs
src/libraries/System.Text.Json/tests/Common/ConstructorTests/ConstructorTests.ParameterMatching.cs
src/libraries/System.Text.Json/tests/Common/PropertyVisibilityTests.cs
src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/JsonSerializerContextTests.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/System.Text.Json.SourceGeneration.Tests.targets
src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/TestClasses.CustomConverters.cs
src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Unit.Tests/CompilationHelper.cs
src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Unit.Tests/JsonSourceGeneratorDiagnosticsTests.cs
src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Unit.Tests/JsonSourceGeneratorTests.cs
src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/CustomConverterTests/CustomConverterTests.BadConverters.cs
src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/EnumConverterTests.cs
src/libraries/apicompat/ApiCompatBaseline.NetCoreAppLatestStable.xml

index 9d02fa2..73d5bbd 100644 (file)
@@ -197,6 +197,9 @@ namespace System.Text.Json.SourceGeneration
         public INamedTypeSymbol? JsonUnmappedMemberHandlingAttributeType => GetOrResolveType("System.Text.Json.Serialization.JsonUnmappedMemberHandlingAttribute", ref _JsonUnmappedMemberHandlingAttributeType);
         private Option<INamedTypeSymbol?> _JsonUnmappedMemberHandlingAttributeType;
 
+        public INamedTypeSymbol? JsonStringEnumConverterType => GetOrResolveType("System.Text.Json.Serialization.JsonStringEnumConverter", ref _JsonStringEnumConverterType);
+        private Option<INamedTypeSymbol?> _JsonStringEnumConverterType;
+
         // Unsupported types
         public INamedTypeSymbol? DelegateType => _DelegateType ??= Compilation.GetSpecialType(SpecialType.System_Delegate);
         private INamedTypeSymbol? _DelegateType;
index 17d7dd5..bd0128b 100644 (file)
@@ -72,6 +72,14 @@ namespace System.Text.Json.SourceGeneration
                 category: JsonConstants.SystemTextJsonSourceGenerationName,
                 defaultSeverity: DiagnosticSeverity.Warning,
                 isEnabledByDefault: true);
+
+            public static DiagnosticDescriptor JsonStringEnumConverterNotSupportedInAot { get; } = new DiagnosticDescriptor(
+                id: "SYSLIB1040",
+                title: new LocalizableResourceString(nameof(SR.JsonStringEnumConverterNotSupportedTitle), SR.ResourceManager, typeof(FxResources.System.Text.Json.SourceGeneration.SR)),
+                messageFormat: new LocalizableResourceString(nameof(SR.JsonStringEnumConverterNotSupportedMessageFormat), SR.ResourceManager, typeof(FxResources.System.Text.Json.SourceGeneration.SR)),
+                category: JsonConstants.SystemTextJsonSourceGenerationName,
+                defaultSeverity: DiagnosticSeverity.Warning,
+                isEnabledByDefault: true);
         }
     }
 }
index d94b13e..ef46c40 100644 (file)
@@ -225,8 +225,13 @@ namespace System.Text.Json.SourceGeneration
 
                 string typeFQN = typeMetadata.TypeRef.FullyQualifiedName;
                 string typeInfoPropertyName = typeMetadata.TypeInfoPropertyName;
-                string metadataInitSource = $"{JsonTypeInfoReturnValueLocalVariableName} = {JsonMetadataServicesTypeRef}.{GetCreateValueInfoMethodRef(typeFQN)}({OptionsLocalVariableName}, {JsonMetadataServicesTypeRef}.{typeInfoPropertyName}Converter);";
-                GenerateTypeInfoProperty(writer, typeMetadata, metadataInitSource);
+
+                GenerateTypeInfoFactoryHeader(writer, typeMetadata);
+                writer.WriteLine($"""
+                    {JsonTypeInfoReturnValueLocalVariableName} = {JsonMetadataServicesTypeRef}.{GetCreateValueInfoMethodRef(typeFQN)}({OptionsLocalVariableName}, {JsonMetadataServicesTypeRef}.{typeInfoPropertyName}Converter);
+                    """);
+
+                GenerateTypeInfoFactoryFooter(writer);
 
                 return CompleteSourceFileAndReturnText(writer);
             }
@@ -238,12 +243,16 @@ namespace System.Text.Json.SourceGeneration
                 SourceWriter writer = CreateSourceWriterWithContextHeader(contextSpec);
 
                 string typeFQN = typeMetadata.TypeRef.FullyQualifiedName;
-                string metadataInitSource = $$"""
-                    {{JsonConverterTypeRef}} converter = {{ExpandConverterMethodName}}(typeof({{typeFQN}}), new {{typeMetadata.ConverterType.FullyQualifiedName}}(), {{OptionsLocalVariableName}});
-                    {{JsonTypeInfoReturnValueLocalVariableName}} = {{JsonMetadataServicesTypeRef}}.{{GetCreateValueInfoMethodRef(typeFQN)}} ({{OptionsLocalVariableName}}, converter);
-                    """;
+                string converterFQN = typeMetadata.ConverterType.FullyQualifiedName;
+
+                GenerateTypeInfoFactoryHeader(writer, typeMetadata);
 
-                GenerateTypeInfoProperty(writer, typeMetadata, metadataInitSource);
+                writer.WriteLine($"""
+                    {JsonConverterTypeRef} converter = {ExpandConverterMethodName}(typeof({typeFQN}), new {converterFQN}(), {OptionsLocalVariableName});
+                    {JsonTypeInfoReturnValueLocalVariableName} = {JsonMetadataServicesTypeRef}.{GetCreateValueInfoMethodRef(typeFQN)} ({OptionsLocalVariableName}, converter);
+                    """);
+
+                GenerateTypeInfoFactoryFooter(writer);
 
                 return CompleteSourceFileAndReturnText(writer);
             }
@@ -257,13 +266,14 @@ namespace System.Text.Json.SourceGeneration
                 string typeFQN = typeMetadata.TypeRef.FullyQualifiedName;
                 string underlyingTypeFQN = typeMetadata.NullableUnderlyingType.FullyQualifiedName;
 
-                string metadataInitSource = $$"""
-                    {{JsonTypeInfoReturnValueLocalVariableName}} = {{JsonMetadataServicesTypeRef}}.{{GetCreateValueInfoMethodRef(typeFQN)}}(
-                        {{OptionsLocalVariableName}},
-                        {{JsonMetadataServicesTypeRef}}.GetNullableConverter<{{underlyingTypeFQN}}>({{OptionsLocalVariableName}}));
-                    """;
+                GenerateTypeInfoFactoryHeader(writer, typeMetadata);
+
+                writer.WriteLine($$"""
+                    {{JsonConverterTypeRef}} converter = {{JsonMetadataServicesTypeRef}}.GetNullableConverter<{{underlyingTypeFQN}}>({{OptionsLocalVariableName}});
+                    {{JsonTypeInfoReturnValueLocalVariableName}} = {{JsonMetadataServicesTypeRef}}.{{GetCreateValueInfoMethodRef(typeFQN)}}({{OptionsLocalVariableName}}, converter);
+                    """);
 
-                GenerateTypeInfoProperty(writer, typeMetadata, metadataInitSource);
+                GenerateTypeInfoFactoryFooter(writer);
 
                 return CompleteSourceFileAndReturnText(writer);
             }
@@ -273,9 +283,13 @@ namespace System.Text.Json.SourceGeneration
                 SourceWriter writer = CreateSourceWriterWithContextHeader(contextSpec);
 
                 string typeFQN = typeMetadata.TypeRef.FullyQualifiedName;
-                string metadataInitSource = $"{JsonTypeInfoReturnValueLocalVariableName} = {JsonMetadataServicesTypeRef}.{GetCreateValueInfoMethodRef(typeFQN)}({OptionsLocalVariableName}, {JsonMetadataServicesTypeRef}.GetUnsupportedTypeConverter<{typeFQN}>());";
 
-                GenerateTypeInfoProperty(writer, typeMetadata, metadataInitSource);
+                GenerateTypeInfoFactoryHeader(writer, typeMetadata);
+                writer.WriteLine($"""
+                    {JsonTypeInfoReturnValueLocalVariableName} = {JsonMetadataServicesTypeRef}.{GetCreateValueInfoMethodRef(typeFQN)}({OptionsLocalVariableName}, {JsonMetadataServicesTypeRef}.GetUnsupportedTypeConverter<{typeFQN}>());
+                    """);
+
+                GenerateTypeInfoFactoryFooter(writer);
 
                 return CompleteSourceFileAndReturnText(writer);
             }
@@ -285,8 +299,13 @@ namespace System.Text.Json.SourceGeneration
                 SourceWriter writer = CreateSourceWriterWithContextHeader(contextSpec);
 
                 string typeFQN = typeMetadata.TypeRef.FullyQualifiedName;
-                string metadataInitSource = $"{JsonTypeInfoReturnValueLocalVariableName} = {JsonMetadataServicesTypeRef}.{GetCreateValueInfoMethodRef(typeFQN)}({OptionsLocalVariableName}, {JsonMetadataServicesTypeRef}.GetEnumConverter<{typeFQN}>({OptionsLocalVariableName}));";
-                GenerateTypeInfoProperty(writer, typeMetadata, metadataInitSource);
+
+                GenerateTypeInfoFactoryHeader(writer, typeMetadata);
+                writer.WriteLine($"""
+                    {JsonTypeInfoReturnValueLocalVariableName} = {JsonMetadataServicesTypeRef}.{GetCreateValueInfoMethodRef(typeFQN)}({OptionsLocalVariableName}, {JsonMetadataServicesTypeRef}.GetEnumConverter<{typeFQN}>({OptionsLocalVariableName}));
+                    """);
+
+                GenerateTypeInfoFactoryFooter(writer);
 
                 return CompleteSourceFileAndReturnText(writer);
             }
@@ -349,8 +368,10 @@ namespace System.Text.Json.SourceGeneration
                         break;
                 }
 
-                string metadataInitSource = $$"""
-                    var {{InfoVarName}} = new {{JsonCollectionInfoValuesTypeRef}}<{{typeFQN}}>()
+                GenerateTypeInfoFactoryHeader(writer, typeGenerationSpec);
+
+                writer.WriteLine($$"""
+                    var {{InfoVarName}} = new {{JsonCollectionInfoValuesTypeRef}}<{{typeFQN}}>
                     {
                         {{ObjectCreatorPropName}} = {{FormatDefaultConstructorExpr(typeGenerationSpec)}},
                         {{NumberHandlingPropName}} = {{GetNumberHandlingAsStr(typeGenerationSpec.NumberHandling)}},
@@ -358,9 +379,9 @@ namespace System.Text.Json.SourceGeneration
                     };
 
                     {{JsonTypeInfoReturnValueLocalVariableName}} = {{JsonMetadataServicesTypeRef}}.{{createCollectionMethodExpr}};
-                    """;
+                    """);
 
-                GenerateTypeInfoProperty(writer, typeGenerationSpec, metadataInitSource);
+                GenerateTypeInfoFactoryFooter(writer);
 
                 if (serializeMethodName != null)
                 {
@@ -491,8 +512,10 @@ namespace System.Text.Json.SourceGeneration
                 const string ObjectInfoVarName = "objectInfo";
                 string genericArg = typeMetadata.TypeRef.FullyQualifiedName;
 
-                string metadataInitSource = $$"""
-                    var {{ObjectInfoVarName}} = new {{JsonObjectInfoValuesTypeRef}}<{{genericArg}}>()
+                GenerateTypeInfoFactoryHeader(writer, typeMetadata);
+
+                writer.WriteLine($$"""
+                    var {{ObjectInfoVarName}} = new {{JsonObjectInfoValuesTypeRef}}<{{genericArg}}>
                     {
                         {{ObjectCreatorPropName}} = {{creatorInvocation}},
                         ObjectWithParameterizedConstructorCreator = {{parameterizedCreatorInvocation}},
@@ -503,25 +526,24 @@ namespace System.Text.Json.SourceGeneration
                     };
 
                     {{JsonTypeInfoReturnValueLocalVariableName}} = {{JsonMetadataServicesTypeRef}}.CreateObjectInfo<{{typeMetadata.TypeRef.FullyQualifiedName}}>({{OptionsLocalVariableName}}, {{ObjectInfoVarName}});
-                    """;
+                    """);
 
-                if (typeMetadata.UnmappedMemberHandling != null)
+                if (typeMetadata is { UnmappedMemberHandling: not null } or { PreferredPropertyObjectCreationHandling: not null })
                 {
-                    metadataInitSource += $"""
-
-                        {JsonTypeInfoReturnValueLocalVariableName}.{UnmappedMemberHandlingPropName} = {GetUnmappedMemberHandlingAsStr(typeMetadata.UnmappedMemberHandling.Value)};
-                        """;
-                }
+                    writer.WriteLine();
 
-                if (typeMetadata.PreferredPropertyObjectCreationHandling != null)
-                {
-                    metadataInitSource += $"""
+                    if (typeMetadata.UnmappedMemberHandling != null)
+                    {
+                        writer.WriteLine($"{JsonTypeInfoReturnValueLocalVariableName}.{UnmappedMemberHandlingPropName} = {GetUnmappedMemberHandlingAsStr(typeMetadata.UnmappedMemberHandling.Value)};");
+                    }
 
-                        {JsonTypeInfoReturnValueLocalVariableName}.{PreferredPropertyObjectCreationHandlingPropName} = {GetObjectCreationHandlingAsStr(typeMetadata.PreferredPropertyObjectCreationHandling.Value)};
-                        """;
+                    if (typeMetadata.PreferredPropertyObjectCreationHandling != null)
+                    {
+                        writer.WriteLine($"{JsonTypeInfoReturnValueLocalVariableName}.{PreferredPropertyObjectCreationHandlingPropName} = {GetObjectCreationHandlingAsStr(typeMetadata.PreferredPropertyObjectCreationHandling.Value)};");
+                    }
                 }
 
-                GenerateTypeInfoProperty(writer, typeMetadata, metadataInitSource);
+                GenerateTypeInfoFactoryFooter(writer);
 
                 if (propInitMethodName != null)
                 {
@@ -596,15 +618,17 @@ namespace System.Text.Json.SourceGeneration
                     string? converterInstantiationExpr = null;
                     if (property.ConverterType != null)
                     {
+                        string converterFQN = property.ConverterType.FullyQualifiedName;
                         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})";
+                            ? $"{GetConverterForNullablePropertyMethodName}<{nullableUnderlyingType.FullyQualifiedName}>(new {converterFQN}(), {OptionsLocalVariableName})"
+                            : $"({JsonConverterTypeRef}<{propertyTypeFQN}>){ExpandConverterMethodName}(typeof({propertyTypeFQN}), new {converterFQN}(), {OptionsLocalVariableName})";
                     }
 
                     writer.WriteLine($$"""
-                        var {{InfoVarName}}{{i}} = new {{JsonPropertyInfoValuesTypeRef}}<{{propertyTypeFQN}}>()
+                        var {{InfoVarName}}{{i}} = new {{JsonPropertyInfoValuesTypeRef}}<{{propertyTypeFQN}}>
                         {
                             IsProperty = {{FormatBool(property.IsProperty)}},
                             IsPublic = {{FormatBool(property.IsPublic)}},
@@ -986,7 +1010,7 @@ namespace System.Text.Json.SourceGeneration
                 };
             }
 
-            private static void GenerateTypeInfoProperty(SourceWriter writer, TypeGenerationSpec typeMetadata, string metadataInitSource)
+            private static void GenerateTypeInfoFactoryHeader(SourceWriter writer, TypeGenerationSpec typeMetadata)
             {
                 string typeFQN = typeMetadata.TypeRef.FullyQualifiedName;
                 string typeInfoPropertyName = typeMetadata.TypeInfoPropertyName;
@@ -1010,7 +1034,10 @@ namespace System.Text.Json.SourceGeneration
                     """);
 
                 writer.Indentation += 2;
-                writer.WriteLine(metadataInitSource);
+            }
+
+            private static void GenerateTypeInfoFactoryFooter(SourceWriter writer)
+            {
                 writer.Indentation -= 2;
 
                 // NB OriginatingResolver should be the last property set by the source generator.
@@ -1018,7 +1045,7 @@ namespace System.Text.Json.SourceGeneration
                         }
 
                         {{JsonTypeInfoReturnValueLocalVariableName}}.{{OriginatingResolverPropertyName}} = this;
-                        return jsonTypeInfo;
+                        return {{JsonTypeInfoReturnValueLocalVariableName}};
                     }
                     """);
             }
index 38b1fc4..eba9ca2 100644 (file)
@@ -549,7 +549,7 @@ namespace System.Text.Json.SourceGeneration
                     }
                     else if (!foundDesignTimeCustomConverter && _knownSymbols.JsonConverterAttributeType.IsAssignableFrom(attributeType))
                     {
-                        converterType = GetConverterTypeFromAttribute(contextType, attributeData);
+                        converterType = GetConverterTypeFromAttribute(contextType, type, attributeData);
                         foundDesignTimeCustomConverter = true;
                     }
 
@@ -1075,15 +1075,15 @@ namespace System.Text.Json.SourceGeneration
                     out bool isRequired,
                     out bool canUseGetter,
                     out bool canUseSetter,
-                    out bool isJsonIncludeInaccessible,
+                    out bool hasJsonIncludeButIsInaccessible,
                     out bool setterIsInitOnly);
 
-                if (isJsonIncludeInaccessible)
+                if (hasJsonIncludeButIsInaccessible)
                 {
                     ReportDiagnostic(DiagnosticDescriptors.InaccessibleJsonIncludePropertiesNotSupported, memberInfo.GetDiagnosticLocation(), new string[] { declaringType.Name, memberInfo.Name });
                 }
 
-                if ((!canUseGetter && !canUseSetter && !isJsonIncludeInaccessible) ||
+                if ((!canUseGetter && !canUseSetter && !hasJsonIncludeButIsInaccessible) ||
                     !IsSymbolAccessibleWithin(memberType, within: contextType))
                 {
                     // Skip the member if either of the two conditions hold
@@ -1160,7 +1160,7 @@ namespace System.Text.Json.SourceGeneration
 
                     if (converterType is null && _knownSymbols.JsonConverterAttributeType.IsAssignableFrom(attributeType))
                     {
-                        converterType = GetConverterTypeFromAttribute(contextType, attributeData);
+                        converterType = GetConverterTypeFromAttribute(contextType, memberInfo, attributeData);
                     }
                     else if (attributeType.ContainingAssembly.Name == SystemTextJsonNamespace)
                     {
@@ -1237,7 +1237,7 @@ namespace System.Text.Json.SourceGeneration
                 out bool isRequired,
                 out bool canUseGetter,
                 out bool canUseSetter,
-                out bool isJsonIncludeInaccessible,
+                out bool hasJsonIncludeButIsInaccessible,
                 out bool isSetterInitOnly)
             {
                 isPublic = false;
@@ -1245,7 +1245,7 @@ namespace System.Text.Json.SourceGeneration
                 isRequired = false;
                 canUseGetter = false;
                 canUseSetter = false;
-                isJsonIncludeInaccessible = false;
+                hasJsonIncludeButIsInaccessible = false;
                 isSetterInitOnly = false;
 
                 switch (memberInfo)
@@ -1271,7 +1271,7 @@ namespace System.Text.Json.SourceGeneration
                                 }
                                 else
                                 {
-                                    isJsonIncludeInaccessible = hasJsonInclude;
+                                    hasJsonIncludeButIsInaccessible = hasJsonInclude;
                                 }
                             }
 
@@ -1290,7 +1290,7 @@ namespace System.Text.Json.SourceGeneration
                                 }
                                 else
                                 {
-                                    isJsonIncludeInaccessible = hasJsonInclude;
+                                    hasJsonIncludeButIsInaccessible = hasJsonInclude;
                                 }
                             }
                             else
@@ -1314,7 +1314,7 @@ namespace System.Text.Json.SourceGeneration
                             else
                             {
                                 // Unlike properties JsonIncludeAttribute is not supported for internal fields.
-                                isJsonIncludeInaccessible = hasJsonInclude;
+                                hasJsonIncludeButIsInaccessible = hasJsonInclude;
                             }
                         }
                         break;
@@ -1324,7 +1324,7 @@ namespace System.Text.Json.SourceGeneration
                 }
             }
 
-            private TypeRef? GetConverterTypeFromAttribute(INamedTypeSymbol contextType, AttributeData attributeData)
+            private TypeRef? GetConverterTypeFromAttribute(INamedTypeSymbol contextType, ISymbol declaringSymbol, AttributeData attributeData)
             {
                 Debug.Assert(_knownSymbols.JsonConverterAttributeType.IsAssignableFrom(attributeData.AttributeClass));
                 var converterType = (INamedTypeSymbol?)attributeData.ConstructorArguments[0].Value;
@@ -1336,6 +1336,11 @@ namespace System.Text.Json.SourceGeneration
                     return null;
                 }
 
+                if (_knownSymbols.JsonStringEnumConverterType.IsAssignableFrom(converterType))
+                {
+                    ReportDiagnostic(DiagnosticDescriptors.JsonStringEnumConverterNotSupportedInAot, declaringSymbol.GetDiagnosticLocation(), declaringSymbol.ToDisplayString());
+                }
+
                 return new TypeRef(converterType);
             }
 
index 4b0e51d..0af8900 100644 (file)
   <data name="FastPathPolymorphismNotSupportedMessageFormat" xml:space="preserve">
     <value>Type '{0}' is annotated with 'JsonDerivedTypeAttribute' which is not supported in 'JsonSourceGenerationMode.Serialization'.</value>
   </data>
+  <data name="JsonStringEnumConverterNotSupportedTitle" xml:space="preserve">
+    <value>The non-generic 'JsonStringEnumConverter' requires dynamic code.</value>
+  </data>
+  <data name="JsonStringEnumConverterNotSupportedMessageFormat" xml:space="preserve">
+    <value>The member '{0}' has been annotated with 'JsonStringEnumConverter' which is not supported in native AOT. Consider using the generic 'JsonStringEnumConverter&lt;TEnum&gt;' instead.</value>
+  </data>
 </root>
index 80c44da..f1fb86b 100644 (file)
         <target state="translated">Deserializace vlastností pouze pro inicializaci se v současnosti v režimu generování zdroje nepodporuje.</target>
         <note />
       </trans-unit>
+      <trans-unit id="JsonStringEnumConverterNotSupportedMessageFormat">
+        <source>The member '{0}' has been annotated with 'JsonStringEnumConverter' which is not supported in native AOT. Consider using the generic 'JsonStringEnumConverter&lt;TEnum&gt;' instead.</source>
+        <target state="new">The member '{0}' has been annotated with 'JsonStringEnumConverter' which is not supported in native AOT. Consider using the generic 'JsonStringEnumConverter&lt;TEnum&gt;' instead.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="JsonStringEnumConverterNotSupportedTitle">
+        <source>The non-generic 'JsonStringEnumConverter' requires dynamic code.</source>
+        <target state="new">The non-generic 'JsonStringEnumConverter' requires dynamic code.</target>
+        <note />
+      </trans-unit>
       <trans-unit id="MultipleJsonConstructorAttributeFormat">
         <source>Type '{0}' has multiple constructors annotated with 'JsonConstructorAttribute'.</source>
         <target state="translated">Typ {0} má více konstruktorů anotovaných s JsonConstructorAttribute. </target>
index 14848a3..e50a30a 100644 (file)
         <target state="translated">Die Deserialisierung von reinen init-Eigenschaften wird im Quellgenerierungsmodus derzeit nicht unterstützt.</target>
         <note />
       </trans-unit>
+      <trans-unit id="JsonStringEnumConverterNotSupportedMessageFormat">
+        <source>The member '{0}' has been annotated with 'JsonStringEnumConverter' which is not supported in native AOT. Consider using the generic 'JsonStringEnumConverter&lt;TEnum&gt;' instead.</source>
+        <target state="new">The member '{0}' has been annotated with 'JsonStringEnumConverter' which is not supported in native AOT. Consider using the generic 'JsonStringEnumConverter&lt;TEnum&gt;' instead.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="JsonStringEnumConverterNotSupportedTitle">
+        <source>The non-generic 'JsonStringEnumConverter' requires dynamic code.</source>
+        <target state="new">The non-generic 'JsonStringEnumConverter' requires dynamic code.</target>
+        <note />
+      </trans-unit>
       <trans-unit id="MultipleJsonConstructorAttributeFormat">
         <source>Type '{0}' has multiple constructors annotated with 'JsonConstructorAttribute'.</source>
         <target state="translated">Typ "{0}" weist mehrere Konstruktoren mit dem Kommentar "JsonConstructorAttribute" auf.</target>
index 8a97e74..d3c4116 100644 (file)
         <target state="translated">Actualmente no se admite la deserialización de propiedades de solo inicialización en el modo de generación de origen.</target>
         <note />
       </trans-unit>
+      <trans-unit id="JsonStringEnumConverterNotSupportedMessageFormat">
+        <source>The member '{0}' has been annotated with 'JsonStringEnumConverter' which is not supported in native AOT. Consider using the generic 'JsonStringEnumConverter&lt;TEnum&gt;' instead.</source>
+        <target state="new">The member '{0}' has been annotated with 'JsonStringEnumConverter' which is not supported in native AOT. Consider using the generic 'JsonStringEnumConverter&lt;TEnum&gt;' instead.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="JsonStringEnumConverterNotSupportedTitle">
+        <source>The non-generic 'JsonStringEnumConverter' requires dynamic code.</source>
+        <target state="new">The non-generic 'JsonStringEnumConverter' requires dynamic code.</target>
+        <note />
+      </trans-unit>
       <trans-unit id="MultipleJsonConstructorAttributeFormat">
         <source>Type '{0}' has multiple constructors annotated with 'JsonConstructorAttribute'.</source>
         <target state="translated">El tipo '{0}' tiene varios constructores anotados con 'JsonConstructorAttribute'.</target>
index fb3db75..304d94c 100644 (file)
         <target state="translated">La désérialisation des propriétés d’initialisation uniquement n’est actuellement pas prise en charge en mode de génération de source.</target>
         <note />
       </trans-unit>
+      <trans-unit id="JsonStringEnumConverterNotSupportedMessageFormat">
+        <source>The member '{0}' has been annotated with 'JsonStringEnumConverter' which is not supported in native AOT. Consider using the generic 'JsonStringEnumConverter&lt;TEnum&gt;' instead.</source>
+        <target state="new">The member '{0}' has been annotated with 'JsonStringEnumConverter' which is not supported in native AOT. Consider using the generic 'JsonStringEnumConverter&lt;TEnum&gt;' instead.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="JsonStringEnumConverterNotSupportedTitle">
+        <source>The non-generic 'JsonStringEnumConverter' requires dynamic code.</source>
+        <target state="new">The non-generic 'JsonStringEnumConverter' requires dynamic code.</target>
+        <note />
+      </trans-unit>
       <trans-unit id="MultipleJsonConstructorAttributeFormat">
         <source>Type '{0}' has multiple constructors annotated with 'JsonConstructorAttribute'.</source>
         <target state="translated">Le type' {0} 'a plusieurs constructeurs annotés avec’JsonConstructorAttribute'.</target>
index 596de18..d3aca5c 100644 (file)
         <target state="translated">La deserializzazione delle proprietà di sola inizializzazione al momento non è supportata nella modalità di generazione di origine.</target>
         <note />
       </trans-unit>
+      <trans-unit id="JsonStringEnumConverterNotSupportedMessageFormat">
+        <source>The member '{0}' has been annotated with 'JsonStringEnumConverter' which is not supported in native AOT. Consider using the generic 'JsonStringEnumConverter&lt;TEnum&gt;' instead.</source>
+        <target state="new">The member '{0}' has been annotated with 'JsonStringEnumConverter' which is not supported in native AOT. Consider using the generic 'JsonStringEnumConverter&lt;TEnum&gt;' instead.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="JsonStringEnumConverterNotSupportedTitle">
+        <source>The non-generic 'JsonStringEnumConverter' requires dynamic code.</source>
+        <target state="new">The non-generic 'JsonStringEnumConverter' requires dynamic code.</target>
+        <note />
+      </trans-unit>
       <trans-unit id="MultipleJsonConstructorAttributeFormat">
         <source>Type '{0}' has multiple constructors annotated with 'JsonConstructorAttribute'.</source>
         <target state="translated">Il tipo '{0}' contiene più costruttori che presentano l'annotazione 'JsonConstructorAttribute'.</target>
index 8988ba0..fe079b2 100644 (file)
         <target state="translated">現在、ソース生成モードでは init-only プロパティの逆シリアル化はサポートされていません。</target>
         <note />
       </trans-unit>
+      <trans-unit id="JsonStringEnumConverterNotSupportedMessageFormat">
+        <source>The member '{0}' has been annotated with 'JsonStringEnumConverter' which is not supported in native AOT. Consider using the generic 'JsonStringEnumConverter&lt;TEnum&gt;' instead.</source>
+        <target state="new">The member '{0}' has been annotated with 'JsonStringEnumConverter' which is not supported in native AOT. Consider using the generic 'JsonStringEnumConverter&lt;TEnum&gt;' instead.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="JsonStringEnumConverterNotSupportedTitle">
+        <source>The non-generic 'JsonStringEnumConverter' requires dynamic code.</source>
+        <target state="new">The non-generic 'JsonStringEnumConverter' requires dynamic code.</target>
+        <note />
+      </trans-unit>
       <trans-unit id="MultipleJsonConstructorAttributeFormat">
         <source>Type '{0}' has multiple constructors annotated with 'JsonConstructorAttribute'.</source>
         <target state="translated">型 '{0}' には、'JsonConstructorAttribute' で注釈が付けられた複数のコンストラクターがあります。</target>
index 193d95b..81b0590 100644 (file)
         <target state="translated">초기화 전용 속성의 역직렬화는 현재 원본 생성 모드에서 지원되지 않습니다.</target>
         <note />
       </trans-unit>
+      <trans-unit id="JsonStringEnumConverterNotSupportedMessageFormat">
+        <source>The member '{0}' has been annotated with 'JsonStringEnumConverter' which is not supported in native AOT. Consider using the generic 'JsonStringEnumConverter&lt;TEnum&gt;' instead.</source>
+        <target state="new">The member '{0}' has been annotated with 'JsonStringEnumConverter' which is not supported in native AOT. Consider using the generic 'JsonStringEnumConverter&lt;TEnum&gt;' instead.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="JsonStringEnumConverterNotSupportedTitle">
+        <source>The non-generic 'JsonStringEnumConverter' requires dynamic code.</source>
+        <target state="new">The non-generic 'JsonStringEnumConverter' requires dynamic code.</target>
+        <note />
+      </trans-unit>
       <trans-unit id="MultipleJsonConstructorAttributeFormat">
         <source>Type '{0}' has multiple constructors annotated with 'JsonConstructorAttribute'.</source>
         <target state="translated">'{0}' 형식에 'JsonConstructorAttribute'로 주석이 추가된 여러 생성자가 있습니다.</target>
index e769635..23db1ec 100644 (file)
         <target state="translated">Deserializacja właściwości tylko do inicjowania nie jest obecnie obsługiwana w trybie generowania źródła.</target>
         <note />
       </trans-unit>
+      <trans-unit id="JsonStringEnumConverterNotSupportedMessageFormat">
+        <source>The member '{0}' has been annotated with 'JsonStringEnumConverter' which is not supported in native AOT. Consider using the generic 'JsonStringEnumConverter&lt;TEnum&gt;' instead.</source>
+        <target state="new">The member '{0}' has been annotated with 'JsonStringEnumConverter' which is not supported in native AOT. Consider using the generic 'JsonStringEnumConverter&lt;TEnum&gt;' instead.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="JsonStringEnumConverterNotSupportedTitle">
+        <source>The non-generic 'JsonStringEnumConverter' requires dynamic code.</source>
+        <target state="new">The non-generic 'JsonStringEnumConverter' requires dynamic code.</target>
+        <note />
+      </trans-unit>
       <trans-unit id="MultipleJsonConstructorAttributeFormat">
         <source>Type '{0}' has multiple constructors annotated with 'JsonConstructorAttribute'.</source>
         <target state="translated">Typ "{0}" ma wiele konstruktorów z adnotacją "JsonConstructorAttribute".</target>
index 5ec4bae..8339db7 100644 (file)
         <target state="translated">A desserialização de propriedades apenas de inicialização não é atualmente suportada no modo de geração de origem.</target>
         <note />
       </trans-unit>
+      <trans-unit id="JsonStringEnumConverterNotSupportedMessageFormat">
+        <source>The member '{0}' has been annotated with 'JsonStringEnumConverter' which is not supported in native AOT. Consider using the generic 'JsonStringEnumConverter&lt;TEnum&gt;' instead.</source>
+        <target state="new">The member '{0}' has been annotated with 'JsonStringEnumConverter' which is not supported in native AOT. Consider using the generic 'JsonStringEnumConverter&lt;TEnum&gt;' instead.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="JsonStringEnumConverterNotSupportedTitle">
+        <source>The non-generic 'JsonStringEnumConverter' requires dynamic code.</source>
+        <target state="new">The non-generic 'JsonStringEnumConverter' requires dynamic code.</target>
+        <note />
+      </trans-unit>
       <trans-unit id="MultipleJsonConstructorAttributeFormat">
         <source>Type '{0}' has multiple constructors annotated with 'JsonConstructorAttribute'.</source>
         <target state="translated">O tipo '{0}' tem vários construtores anotados com 'JsonConstructorAttribute'.</target>
index 5de2a80..e5ef15b 100644 (file)
         <target state="translated">Десериализация свойств, предназначенных только для инициализации, сейчас не поддерживается в режиме создания исходного кода.</target>
         <note />
       </trans-unit>
+      <trans-unit id="JsonStringEnumConverterNotSupportedMessageFormat">
+        <source>The member '{0}' has been annotated with 'JsonStringEnumConverter' which is not supported in native AOT. Consider using the generic 'JsonStringEnumConverter&lt;TEnum&gt;' instead.</source>
+        <target state="new">The member '{0}' has been annotated with 'JsonStringEnumConverter' which is not supported in native AOT. Consider using the generic 'JsonStringEnumConverter&lt;TEnum&gt;' instead.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="JsonStringEnumConverterNotSupportedTitle">
+        <source>The non-generic 'JsonStringEnumConverter' requires dynamic code.</source>
+        <target state="new">The non-generic 'JsonStringEnumConverter' requires dynamic code.</target>
+        <note />
+      </trans-unit>
       <trans-unit id="MultipleJsonConstructorAttributeFormat">
         <source>Type '{0}' has multiple constructors annotated with 'JsonConstructorAttribute'.</source>
         <target state="translated">Тип "{0}" имеет несколько конструкторов, аннотированных с использованием JsonConstructorAttribute.</target>
index f65e768..8076c29 100644 (file)
         <target state="translated">Yalnızca başlangıç özelliklerini seri durumdan çıkarma şu anda kaynak oluşturma modunda desteklenmiyor.</target>
         <note />
       </trans-unit>
+      <trans-unit id="JsonStringEnumConverterNotSupportedMessageFormat">
+        <source>The member '{0}' has been annotated with 'JsonStringEnumConverter' which is not supported in native AOT. Consider using the generic 'JsonStringEnumConverter&lt;TEnum&gt;' instead.</source>
+        <target state="new">The member '{0}' has been annotated with 'JsonStringEnumConverter' which is not supported in native AOT. Consider using the generic 'JsonStringEnumConverter&lt;TEnum&gt;' instead.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="JsonStringEnumConverterNotSupportedTitle">
+        <source>The non-generic 'JsonStringEnumConverter' requires dynamic code.</source>
+        <target state="new">The non-generic 'JsonStringEnumConverter' requires dynamic code.</target>
+        <note />
+      </trans-unit>
       <trans-unit id="MultipleJsonConstructorAttributeFormat">
         <source>Type '{0}' has multiple constructors annotated with 'JsonConstructorAttribute'.</source>
         <target state="translated">'{0}' türünün 'JsonConstructorAttribute' ile açıklanan birden çok oluşturucusu var.</target>
index 6f6087b..4b238b2 100644 (file)
         <target state="translated">源生成模式当前不支持仅初始化属性的反序列化。</target>
         <note />
       </trans-unit>
+      <trans-unit id="JsonStringEnumConverterNotSupportedMessageFormat">
+        <source>The member '{0}' has been annotated with 'JsonStringEnumConverter' which is not supported in native AOT. Consider using the generic 'JsonStringEnumConverter&lt;TEnum&gt;' instead.</source>
+        <target state="new">The member '{0}' has been annotated with 'JsonStringEnumConverter' which is not supported in native AOT. Consider using the generic 'JsonStringEnumConverter&lt;TEnum&gt;' instead.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="JsonStringEnumConverterNotSupportedTitle">
+        <source>The non-generic 'JsonStringEnumConverter' requires dynamic code.</source>
+        <target state="new">The non-generic 'JsonStringEnumConverter' requires dynamic code.</target>
+        <note />
+      </trans-unit>
       <trans-unit id="MultipleJsonConstructorAttributeFormat">
         <source>Type '{0}' has multiple constructors annotated with 'JsonConstructorAttribute'.</source>
         <target state="translated">类型“{0}”具有用 “JsonConstructorAttribute” 批注的多个构造函数。</target>
index 45bce60..fa04417 100644 (file)
         <target state="translated">來源產生模式目前不支援 init-only 屬性的還原序列化。</target>
         <note />
       </trans-unit>
+      <trans-unit id="JsonStringEnumConverterNotSupportedMessageFormat">
+        <source>The member '{0}' has been annotated with 'JsonStringEnumConverter' which is not supported in native AOT. Consider using the generic 'JsonStringEnumConverter&lt;TEnum&gt;' instead.</source>
+        <target state="new">The member '{0}' has been annotated with 'JsonStringEnumConverter' which is not supported in native AOT. Consider using the generic 'JsonStringEnumConverter&lt;TEnum&gt;' instead.</target>
+        <note />
+      </trans-unit>
+      <trans-unit id="JsonStringEnumConverterNotSupportedTitle">
+        <source>The non-generic 'JsonStringEnumConverter' requires dynamic code.</source>
+        <target state="new">The non-generic 'JsonStringEnumConverter' requires dynamic code.</target>
+        <note />
+      </trans-unit>
       <trans-unit id="MultipleJsonConstructorAttributeFormat">
         <source>Type '{0}' has multiple constructors annotated with 'JsonConstructorAttribute'.</source>
         <target state="translated">類型 '{0}' 包含多個以 'JsonConstructorAttribute' 註解的建構函式。</target>
index e25bbc8..7974eb6 100644 (file)
@@ -1040,7 +1040,7 @@ namespace System.Text.Json.Serialization
         public System.Text.Json.Serialization.JsonKnownNamingPolicy PropertyNamingPolicy { get { throw null; } set { } }
         public bool WriteIndented { get { throw null; } set { } }
     }
-    [System.Diagnostics.CodeAnalysis.RequiresDynamicCodeAttribute("JsonStringEnumConverter cannot be statically analyzed and requires runtime code generation. Consider authoring a custom converter that is not a factory to work around the issue. See https://github.com/dotnet/runtime/issues/73124.")]
+    [System.Diagnostics.CodeAnalysis.RequiresDynamicCodeAttribute("JsonStringEnumConverter cannot be statically analyzed and requires runtime code generation. Applications should use the generic JsonStringEnumConverter<TEnum> instead.")]
     public partial class JsonStringEnumConverter : System.Text.Json.Serialization.JsonConverterFactory
     {
         public JsonStringEnumConverter() { }
@@ -1048,6 +1048,13 @@ namespace System.Text.Json.Serialization
         public sealed override bool CanConvert(System.Type typeToConvert) { throw null; }
         public sealed override System.Text.Json.Serialization.JsonConverter CreateConverter(System.Type typeToConvert, System.Text.Json.JsonSerializerOptions options) { throw null; }
     }
+    public partial class JsonStringEnumConverter<TEnum> : System.Text.Json.Serialization.JsonConverterFactory where TEnum : struct, System.Enum
+    {
+        public JsonStringEnumConverter() { }
+        public JsonStringEnumConverter(System.Text.Json.JsonNamingPolicy? namingPolicy = null, bool allowIntegerValues = true) { }
+        public sealed override bool CanConvert(System.Type typeToConvert) { throw null; }
+        public sealed override System.Text.Json.Serialization.JsonConverter CreateConverter(System.Type typeToConvert, System.Text.Json.JsonSerializerOptions options) { throw null; }
+    }
     public enum JsonUnknownDerivedTypeHandling
     {
         FailSerialization = 0,
@@ -1183,7 +1190,7 @@ namespace System.Text.Json.Serialization.Metadata
         public static System.Text.Json.Serialization.Metadata.JsonTypeInfo<TCollection> CreateStackInfo<TCollection>(System.Text.Json.JsonSerializerOptions options, System.Text.Json.Serialization.Metadata.JsonCollectionInfoValues<TCollection> collectionInfo, System.Action<TCollection, object?> addFunc) where TCollection : System.Collections.IEnumerable { throw null; }
         public static System.Text.Json.Serialization.Metadata.JsonTypeInfo<TCollection> CreateStackInfo<TCollection, TElement>(System.Text.Json.JsonSerializerOptions options, System.Text.Json.Serialization.Metadata.JsonCollectionInfoValues<TCollection> collectionInfo) where TCollection : System.Collections.Generic.Stack<TElement> { throw null; }
         public static System.Text.Json.Serialization.Metadata.JsonTypeInfo<T> CreateValueInfo<T>(System.Text.Json.JsonSerializerOptions options, System.Text.Json.Serialization.JsonConverter converter) { throw null; }
-        public static System.Text.Json.Serialization.JsonConverter<T> GetEnumConverter<T>(System.Text.Json.JsonSerializerOptions options) where T : struct { throw null; }
+        public static System.Text.Json.Serialization.JsonConverter<T> GetEnumConverter<T>(System.Text.Json.JsonSerializerOptions options) where T : struct, System.Enum { throw null; }
         public static System.Text.Json.Serialization.JsonConverter<T?> GetNullableConverter<T>(System.Text.Json.JsonSerializerOptions options) where T : struct { throw null; }
         public static System.Text.Json.Serialization.JsonConverter<T?> GetNullableConverter<T>(System.Text.Json.Serialization.Metadata.JsonTypeInfo<T> underlyingTypeInfo) where T : struct { throw null; }
         public static System.Text.Json.Serialization.JsonConverter<T> GetUnsupportedTypeConverter<T>() { throw null; }
diff --git a/src/libraries/System.Text.Json/src/CompatibilitySuppressions.xml b/src/libraries/System.Text.Json/src/CompatibilitySuppressions.xml
new file mode 100644 (file)
index 0000000..1de43a6
--- /dev/null
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- https://learn.microsoft.com/en-us/dotnet/fundamentals/package-validation/diagnostic-ids -->
+<Suppressions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
+  <Suppression>
+    <DiagnosticId>CP0015</DiagnosticId>
+    <Target>T:System.Text.Json.Serialization.JsonStringEnumConverter:[T:System.Diagnostics.CodeAnalysis.RequiresDynamicCodeAttribute]</Target>
+    <Left>lib/net7.0/System.Text.Json.dll</Left>
+    <Right>lib/net7.0/System.Text.Json.dll</Right>
+    <IsBaselineSuppression>true</IsBaselineSuppression>
+  </Suppression>
+</Suppressions>
\ No newline at end of file
index 2793687..064094d 100644 (file)
   <data name="SerializerConverterFactoryReturnsJsonConverterFactory" xml:space="preserve">
     <value>The converter '{0}' cannot return an instance of JsonConverterFactory.</value>
   </data>
+  <data name="SerializerConverterFactoryInvalidArgument" xml:space="preserve">
+    <value>The type '{0}' is not supported by the current JsonConverterFactory.</value>
+  </data>
   <data name="NodeElementWrongType" xml:space="preserve">
     <value>The element must be of type '{0}'</value>
   </data>
index b23623f..ed49845 100644 (file)
@@ -1,6 +1,7 @@
 // Licensed to the .NET Foundation under one or more agreements.
 // The .NET Foundation licenses this file to you under the MIT license.
 
+using System.Diagnostics;
 using System.Diagnostics.CodeAnalysis;
 
 namespace System.Text.Json.Serialization.Converters
@@ -18,7 +19,10 @@ namespace System.Text.Json.Serialization.Converters
         }
 
         public override JsonConverter CreateConverter(Type type, JsonSerializerOptions options)
-            => Create(type, EnumConverterOptions.AllowNumbers, namingPolicy: null, options);
+        {
+            Debug.Assert(CanConvert(type));
+            return Create(type, EnumConverterOptions.AllowNumbers, namingPolicy: null, options);
+        }
 
         internal static JsonConverter Create(Type enumType, EnumConverterOptions converterOptions, JsonNamingPolicy? namingPolicy, JsonSerializerOptions options)
         {
index a674f80..53aa155 100644 (file)
@@ -18,12 +18,10 @@ namespace System.Text.Json.Serialization.Converters
 
         public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
         {
-            Debug.Assert(typeToConvert.GetGenericArguments().Length > 0);
+            Debug.Assert(typeToConvert.IsNullableOfT());
 
             Type valueTypeToConvert = typeToConvert.GetGenericArguments()[0];
-
             JsonConverter valueConverter = options.GetConverterInternal(valueTypeToConvert);
-            Debug.Assert(valueConverter != null);
 
             // If the value type has an interface or object converter, just return that converter directly.
             if (!valueConverter.TypeToConvert.IsValueType && valueTypeToConvert.IsValueType)
@@ -36,6 +34,7 @@ namespace System.Text.Json.Serialization.Converters
 
         public static JsonConverter CreateValueConverter(Type valueTypeToConvert, JsonConverter valueConverter)
         {
+            Debug.Assert(valueTypeToConvert.IsValueType && !valueTypeToConvert.IsNullableOfT());
             return (JsonConverter)Activator.CreateInstance(
                 GetNullableConverterType(valueTypeToConvert),
                 BindingFlags.Instance | BindingFlags.Public,
index bd88a2b..698e2c3 100644 (file)
@@ -44,7 +44,10 @@ namespace System.Text.Json.Serialization.Converters
         }
 
         public override JsonConverter CreateConverter(Type type, JsonSerializerOptions options)
-            => CreateUnsupportedConverterForType(type);
+        {
+            Debug.Assert(CanConvert(type));
+            return CreateUnsupportedConverterForType(type);
+        }
 
         internal static JsonConverter CreateUnsupportedConverterForType(Type type, string? errorMessage = null)
         {
index 8dfe234..4210d66 100644 (file)
@@ -12,10 +12,64 @@ namespace System.Text.Json.Serialization
     /// <remarks>
     /// Reading is case insensitive, writing can be customized via a <see cref="JsonNamingPolicy" />.
     /// </remarks>
+    /// <typeparam name="TEnum">The enum type that this converter targets.</typeparam>
+    public class JsonStringEnumConverter<TEnum> : JsonConverterFactory
+        where TEnum : struct, Enum
+    {
+        private readonly JsonNamingPolicy? _namingPolicy;
+        private readonly EnumConverterOptions _converterOptions;
+
+        /// <summary>
+        /// Constructor. Creates the <see cref="JsonStringEnumConverter"/> with the
+        /// default naming policy and allows integer values.
+        /// </summary>
+        public JsonStringEnumConverter() : this(namingPolicy: null, allowIntegerValues: true)
+        {
+            // An empty constructor is needed for construction via attributes
+        }
+
+        /// <summary>
+        /// Constructor.
+        /// </summary>
+        /// <param name="namingPolicy">
+        /// Optional naming policy for writing enum values.
+        /// </param>
+        /// <param name="allowIntegerValues">
+        /// True to allow undefined enum values. When true, if an enum value isn't
+        /// defined it will output as a number rather than a string.
+        /// </param>
+        public JsonStringEnumConverter(JsonNamingPolicy? namingPolicy = null, bool allowIntegerValues = true)
+        {
+            _namingPolicy = namingPolicy;
+            _converterOptions = allowIntegerValues
+                ? EnumConverterOptions.AllowNumbers | EnumConverterOptions.AllowStrings
+                : EnumConverterOptions.AllowStrings;
+        }
+
+        /// <inheritdoc />
+        public override bool CanConvert(Type typeToConvert) => typeToConvert == typeof(TEnum);
+
+        /// <inheritdoc />
+        public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options)
+        {
+            if (typeToConvert != typeof(TEnum))
+            {
+                ThrowHelper.ThrowArgumentOutOfRangeException_JsonConverterFactory_TypeNotSupported(typeToConvert);
+            }
+
+            return new EnumConverter<TEnum>(_converterOptions, _namingPolicy, options);
+        }
+    }
+
+    /// <summary>
+    /// Converter to convert enums to and from strings.
+    /// </summary>
+    /// <remarks>
+    /// Reading is case insensitive, writing can be customized via a <see cref="JsonNamingPolicy" />.
+    /// </remarks>
     [RequiresDynamicCode(
         "JsonStringEnumConverter cannot be statically analyzed and requires runtime code generation. " +
-        "Consider authoring a custom converter that is not a factory to work around the issue. " +
-        "See https://github.com/dotnet/runtime/issues/73124.")]
+        "Applications should use the generic JsonStringEnumConverter<TEnum> instead.")]
     public class JsonStringEnumConverter : JsonConverterFactory
     {
         private readonly JsonNamingPolicy? _namingPolicy;
@@ -25,8 +79,7 @@ namespace System.Text.Json.Serialization
         /// Constructor. Creates the <see cref="JsonStringEnumConverter"/> with the
         /// default naming policy and allows integer values.
         /// </summary>
-        public JsonStringEnumConverter()
-            : this(namingPolicy: null, allowIntegerValues: true)
+        public JsonStringEnumConverter() : this(namingPolicy: null, allowIntegerValues: true)
         {
             // An empty constructor is needed for construction via attributes
         }
@@ -56,7 +109,14 @@ namespace System.Text.Json.Serialization
         }
 
         /// <inheritdoc />
-        public sealed override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) =>
-            EnumConverterFactory.Create(typeToConvert, _converterOptions, _namingPolicy, options);
+        public sealed override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
+        {
+            if (!typeToConvert.IsEnum)
+            {
+                ThrowHelper.ThrowArgumentOutOfRangeException_JsonConverterFactory_TypeNotSupported(typeToConvert);
+            }
+
+            return EnumConverterFactory.Create(typeToConvert, _converterOptions, _namingPolicy, options);
+        }
     }
 }
index 417ed4c..b2e96d7 100644 (file)
@@ -36,6 +36,12 @@ namespace System.Text.Json
         }
 
         [DoesNotReturn]
+        public static void ThrowArgumentOutOfRangeException_JsonConverterFactory_TypeNotSupported(Type typeToConvert)
+        {
+            throw new ArgumentOutOfRangeException(nameof(typeToConvert), SR.Format(SR.SerializerConverterFactoryInvalidArgument, typeToConvert.FullName));
+        }
+
+        [DoesNotReturn]
         public static void ThrowArgumentException_ArrayTooSmall(string paramName)
         {
             throw new ArgumentException(SR.ArrayTooSmall, paramName);
index 8e84113..b06f0ad 100644 (file)
@@ -1544,7 +1544,6 @@ namespace System.Text.Json.Serialization.Tests
         }
 
         [Fact]
-        [ActiveIssue("https://github.com/dotnet/runtime/issues/79311", typeof(PlatformDetection), nameof(PlatformDetection.IsNativeAot))]
         public async Task TestTypeWithEnumParameters()
         {
             // Regression test for https://github.com/dotnet/runtime/issues/68647
@@ -1576,7 +1575,7 @@ namespace System.Text.Json.Serialization.Tests
             }
         }
 
-        [JsonConverter(typeof(JsonStringEnumConverter))]
+        [JsonConverter(typeof(JsonStringEnumConverter<MyEnum>))]
         public enum MyEnum
         {
             One = 1,
index 72dccfa..4c0b796 100644 (file)
@@ -3283,7 +3283,7 @@ namespace System.Text.Json.Serialization.Tests
                 => new DictionaryWithPrivateKeyAndValueType { _values = { [PrivateEnum.A] = PrivateEnum.B, [PrivateEnum.B] = PrivateEnum.C } };
 
             public void Validate() => Assert.Equal(new KeyValuePair<PrivateEnum, PrivateEnum>[] { new(PrivateEnum.A, PrivateEnum.B), new(PrivateEnum.B, PrivateEnum.C) }, this);
-            public string GetExpectedJson() => """{"A":1,"B":2}"""; // cf. https://github.com/dotnet/runtime/issues/87129
+            public string GetExpectedJson() => """{"A":1,"B":2}""";
 
             PrivateEnum IDictionary<PrivateEnum, PrivateEnum>.this[PrivateEnum key] { get => _values[key]; set => _values[key] = value; }
             ICollection<PrivateEnum> IDictionary<PrivateEnum, PrivateEnum>.Keys => _values.Keys;
index 460ba8d..9047bf0 100644 (file)
@@ -705,20 +705,19 @@ namespace System.Text.Json.SourceGeneration.Tests
         {
         }
 
-        // Regression test for https://github.com/dotnet/runtime/issues/61860
         [Fact]
-        [ActiveIssue("https://github.com/dotnet/runtime/issues/79311", typeof(PlatformDetection), nameof(PlatformDetection.IsNativeAot))]
         public static void SupportsGenericParameterWithCustomConverterFactory()
         {
+            // Regression test for https://github.com/dotnet/runtime/issues/61860
             var value = new List<TestEnum> { TestEnum.Cee };
             string json = JsonSerializer.Serialize(value, GenericParameterWithCustomConverterFactoryContext.Default.ListTestEnum);
             Assert.Equal(@"[""Cee""]", json);
         }
 
-        // Regression test for https://github.com/dotnet/runtime/issues/74652
         [Fact]
         public static void ClassWithStringValuesRoundtrips()
         {
+            // Regression test for https://github.com/dotnet/runtime/issues/74652
             JsonSerializerOptions options = ClassWithStringValuesContext.Default.Options;
 
             ClassWithStringValues obj = new()
@@ -730,10 +729,10 @@ namespace System.Text.Json.SourceGeneration.Tests
             Assert.Equal("""{"StringValuesProperty":["abc","def"]}""", json);
         }
 
-        // Regression test for https://github.com/dotnet/runtime/issues/61734
         [Fact]
         public static void ClassWithDictionaryPropertyRoundtrips()
         {
+            // Regression test for https://github.com/dotnet/runtime/issues/61734
             JsonSerializerOptions options = ClassWithDictionaryPropertyContext.Default.Options;
 
             ClassWithDictionaryProperty obj = new(new Dictionary<string, object?>()
@@ -746,7 +745,7 @@ namespace System.Text.Json.SourceGeneration.Tests
             Assert.Equal("""{"DictionaryProperty":{"foo":"bar","test":"baz"}}""", json);
         }
 
-        [JsonConverter(typeof(JsonStringEnumConverter))]
+        [JsonConverter(typeof(JsonStringEnumConverter<TestEnum>))]
         public enum TestEnum
         {
             Aye, Bee, Cee
index 4fcb6c1..5100371 100644 (file)
@@ -160,7 +160,7 @@ namespace System.Text.Json.SourceGeneration.Tests
         public bool IsIncludeFieldsEnabled => GetType().GetCustomAttribute<JsonSourceGenerationOptionsAttribute>()?.IncludeFields ?? false;
     }
 
-    [JsonConverter(typeof(JsonStringEnumConverter))]
+    [JsonConverter(typeof(JsonStringEnumConverter<EnumWrittenAsString>))]
     public enum EnumWrittenAsString
     {
         A = 1
@@ -233,7 +233,6 @@ namespace System.Text.Json.SourceGeneration.Tests
         }
 
         [Fact]
-        [ActiveIssue("https://github.com/dotnet/runtime/issues/79311", typeof(PlatformDetection), nameof(PlatformDetection.IsNativeAot))]
         public void EnsureHelperMethodGenerated_TypeFactory()
         {
             // There are 2 helper methods generated for obtaining a converter from a factory:
@@ -254,7 +253,6 @@ namespace System.Text.Json.SourceGeneration.Tests
         }
 
         [Fact]
-        [ActiveIssue("https://github.com/dotnet/runtime/issues/79311", typeof(PlatformDetection), nameof(PlatformDetection.IsNativeAot))]
         public void EnsureHelperMethodGenerated_ImplicitPropertyFactory()
         {
             // ContextWithImplicitStringEnum does not have an entry for EnumWrittenAsString since it is
index 6c5d287..704790d 100644 (file)
@@ -8,7 +8,8 @@
     <!-- SYSLIB1037: Suppress init-only property deserialization warning -->
     <!-- SYSLIB1038: Suppress JsonInclude on inaccessible members warning -->
     <!-- SYSLIB1039: Suppress Polymorphic types not supported warning -->
-    <NoWarn>$(NoWarn);SYSLIB0020;SYSLIB0049;SYSLIB1037;SYSLIB1038;SYSLIB1039</NoWarn>
+    <!-- SYSLIB1040: Suppress JsonStringEnumConverter use warnings -->
+    <NoWarn>$(NoWarn);SYSLIB0020;SYSLIB0049;SYSLIB1037;SYSLIB1038;SYSLIB1039;SYSLIB1040</NoWarn>
     <IgnoreForCI Condition="'$(TargetsMobile)' == 'true' or '$(TargetsLinuxBionic)' == 'true' or '$(TargetArchitecture)' == 'ARMv6'">true</IgnoreForCI> 
   </PropertyGroup>
 
index 7ed8c5e..0a4ac1a 100644 (file)
@@ -247,19 +247,19 @@ namespace System.Text.Json.SourceGeneration.Tests
 
     public class ClassWithCustomConverterFactoryProperty
     {
-        [JsonConverter(typeof(JsonStringEnumConverter))] // This converter is a JsonConverterFactory
+        [JsonConverter(typeof(JsonStringEnumConverter<SourceGenSampleEnum>))] // This converter is a JsonConverterFactory
         public SourceGenSampleEnum MyEnum { get; set; }
     }
 
     public struct StructWithCustomConverterFactoryProperty
     {
-        [JsonConverter(typeof(JsonStringEnumConverter))] // This converter is a JsonConverterFactory
+        [JsonConverter(typeof(JsonStringEnumConverter<SourceGenSampleEnum>))] // This converter is a JsonConverterFactory
         public SourceGenSampleEnum MyEnum { get; set; }
     }
 
     public class ClassWithCustomConverterFactoryNullableProperty
     {
-        [JsonConverter(typeof(JsonStringEnumConverter))] // This converter is a JsonConverterFactory
+        [JsonConverter(typeof(JsonStringEnumConverter<SourceGenSampleEnum>))] // This converter is a JsonConverterFactory
         public SourceGenSampleEnum? MyEnum { get; set; }
     }
 
index 922311a..dca6239 100644 (file)
@@ -617,40 +617,60 @@ namespace System.Text.Json.SourceGeneration.UnitTests
             return CreateCompilation(source);
         }
 
-        internal static void CheckDiagnosticMessages(
-            DiagnosticSeverity level,
-            ImmutableArray<Diagnostic> diagnostics,
-            (Location Location, string Message)[] expectedDiags,
-            bool sort = true)
+        public static Compilation CreateTypesAnnotatedWithJsonStringEnumConverter()
         {
-            ((string FileName, TextSpan, LinePositionSpan), string)[] actualDiags = diagnostics
-                .Where(diagnostic => diagnostic.Severity == level)
-                .Select(diagnostic => (GetLocationNormalForm(diagnostic.Location), diagnostic.GetMessage()))
-                .ToArray();
-
-            if (CultureInfo.CurrentUICulture.Name.StartsWith("en", StringComparison.OrdinalIgnoreCase))
-            {
-                ((string FileName, TextSpan, LinePositionSpan), string Message)[] expectedDiagsNormalized = expectedDiags
-                    .Select(diag => (GetLocationNormalForm(diag.Location), diag.Message))
-                    .ToArray();
+            string source = """
+                using System.Text.Json.Serialization;
 
-                if (sort)
+                namespace HelloWorld
                 {
-                    // Can't depend on reflection order when generating type metadata.
-                    Array.Sort(actualDiags);
-                    Array.Sort(expectedDiagsNormalized);
+                    [JsonSerializable(typeof(MyClass))]
+                    internal partial class JsonContext : JsonSerializerContext
+                    {
+                    }
+
+                    public class MyClass
+                    {
+                        public Enum1 Enum1Prop { get; set; }
+
+                        [JsonConverter(typeof(JsonStringEnumConverter))]
+                        public Enum2 Enum2Prop { get; set; }
+                    }
+
+                    [JsonConverter(typeof(JsonStringEnumConverter))]
+                    public enum Enum1 { A, B, C };
+                    
+                    public enum Enum2 { A, B, C };
                 }
+                """;
 
-                Assert.Equal(expectedDiagsNormalized, actualDiags);
-            }
-            else
-            {
-                // for non-English runs, just compare the number of messages are the same
-                Assert.Equal(expectedDiags.Length, actualDiags.Length);
-            }
+            return CreateCompilation(source);
+        }
 
-            static (string FileName, TextSpan, LinePositionSpan) GetLocationNormalForm(Location location)
-                => (location.SourceTree?.FilePath ?? "", location.SourceSpan, location.GetLineSpan().Span);
+        internal static void AssertEqualDiagnosticMessages(
+            IEnumerable<DiagnosticData> expectedDiags,
+            IEnumerable<Diagnostic> actualDiags)
+        {
+            HashSet<DiagnosticData> expectedSet = new(expectedDiags);
+            HashSet<DiagnosticData> actualSet = new(actualDiags.Select(d => new DiagnosticData(d.Severity, d.Location, d.GetMessage())));
+            AssertExtensions.Equal(expectedSet, actualSet);
         }
     }
+
+    public record struct DiagnosticData(
+        DiagnosticSeverity Severity,
+        string FilePath,
+        LinePositionSpan LinePositionSpan,
+        string Message)
+    {
+        public DiagnosticData(DiagnosticSeverity severity, Location location, string message)
+            : this(severity, location.SourceTree?.FilePath ?? "", location.GetLineSpan().Span, TrimCultureSensitiveMessage(message))
+        {
+        }
+
+        // for non-English runs, trim the message content since it might be translated.
+        private static string TrimCultureSensitiveMessage(string message) => s_IsEnglishCulture ? message : "";
+        private readonly static bool s_IsEnglishCulture = CultureInfo.CurrentUICulture.Name.StartsWith("en", StringComparison.OrdinalIgnoreCase);
+        public override string ToString() => $"{Severity}, {Message}, {FilePath}@{LinePositionSpan}";
+    }
 }
index 4f055c5..fcc2195 100644 (file)
@@ -79,11 +79,8 @@ namespace System.Text.Json.SourceGeneration.UnitTests
             using var emitStream = new MemoryStream();
             using var xmlStream = new MemoryStream();
             var result = sourceGenResult.NewCompilation.Emit(emitStream, xmlDocumentationStream: xmlStream);
-            var diagnostics = result.Diagnostics;
 
-            CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Info, diagnostics, Array.Empty<(Location, string)>());
-            CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Warning, diagnostics, Array.Empty<(Location, string)>());
-            CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Error, diagnostics, Array.Empty<(Location, string)>());
+            Assert.Empty(result.Diagnostics);
         }
 
         [Fact]
@@ -129,9 +126,7 @@ namespace System.Text.Json.SourceGeneration.UnitTests
 
             JsonSourceGeneratorResult result = CompilationHelper.RunJsonSourceGenerator(compilation);
 
-            CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Info, result.Diagnostics, Array.Empty<(Location, string)>());
-            CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Warning, result.Diagnostics, Array.Empty<(Location, string)>());
-            CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Error, result.Diagnostics, Array.Empty<(Location, string)>());
+            Assert.Empty(result.Diagnostics);
         }
 
         [Fact]
@@ -174,32 +169,13 @@ namespace System.Text.Json.SourceGeneration.UnitTests
                     """;
 
                 MetadataReference[] additionalReferences = {
-                MetadataReference.CreateFromImage(campaignImage),
-                MetadataReference.CreateFromImage(eventImage),
-            };
+                    MetadataReference.CreateFromImage(campaignImage),
+                    MetadataReference.CreateFromImage(eventImage),
+                };
 
                 Compilation compilation = CompilationHelper.CreateCompilation(source, additionalReferences);
-
                 JsonSourceGeneratorResult result = CompilationHelper.RunJsonSourceGenerator(compilation);
-
-                Location location;
-                if (explicitRef)
-                {
-                    // Unsupported type is not in compiling assembly, but is indicated directly with [JsonSerializable], so location points to attribute application.
-                    INamedTypeSymbol symbol = (INamedTypeSymbol)compilation.GetSymbolsWithName("JsonContext").FirstOrDefault();
-                    SyntaxReference syntaxReference = symbol.GetAttributes().First().ApplicationSyntaxReference;
-                    TextSpan textSpan = syntaxReference.Span;
-                    location = syntaxReference.SyntaxTree.GetLocation(textSpan)!;
-                }
-                else
-                {
-                    // Unsupported type is not in compiling assembly, and isn't indicated directly with [JsonSerializable], so location points to context type.
-                    location = compilation.GetSymbolsWithName("JsonContext").First().Locations[0];
-                }
-
-                CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Info, result.Diagnostics, Array.Empty<(Location, string)>());
-                CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Warning, result.Diagnostics, Array.Empty<(Location, string)>());
-                CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Error, result.Diagnostics, Array.Empty<(Location, string)>());
+                Assert.Empty(result.Diagnostics);
             }
 
             RunTest(explicitRef: true);
@@ -218,22 +194,18 @@ namespace System.Text.Json.SourceGeneration.UnitTests
             TextSpan textSpan = syntaxReference.Span;
             Location location = syntaxReference.SyntaxTree.GetLocation(textSpan)!;
 
-            (Location, string)[] expectedWarningDiagnostics = new (Location, string)[]
+            var expectedDiagnostics = new DiagnosticData[]
             {
-                (location, "There are multiple types named Location. Source was generated for the first one detected. Use 'JsonSerializableAttribute.TypeInfoPropertyName' to resolve this collision.")
+                new(DiagnosticSeverity.Warning, location, "There are multiple types named Location. Source was generated for the first one detected. Use 'JsonSerializableAttribute.TypeInfoPropertyName' to resolve this collision.")
             };
 
-            CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Info, result.Diagnostics, Array.Empty<(Location, string)>());
-            CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Warning, result.Diagnostics, expectedWarningDiagnostics);
-            CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Error, result.Diagnostics, Array.Empty<(Location, string)>());
+            CompilationHelper.AssertEqualDiagnosticMessages(expectedDiagnostics, result.Diagnostics);
 
             // With resolution.
             compilation = CompilationHelper.CreateRepeatedLocationsWithResolutionCompilation();
             result = CompilationHelper.RunJsonSourceGenerator(compilation);
 
-            CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Info, result.Diagnostics, Array.Empty<(Location, string)>());
-            CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Warning, result.Diagnostics, Array.Empty<(Location, string)>());
-            CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Error, result.Diagnostics, Array.Empty<(Location, string)>());
+            Assert.Empty(result.Diagnostics);
         }
 
         [Fact]
@@ -255,9 +227,7 @@ namespace System.Text.Json.SourceGeneration.UnitTests
             Compilation compilation = CompilationHelper.CreateCompilation(source);
             JsonSourceGeneratorResult result = CompilationHelper.RunJsonSourceGenerator(compilation);
 
-            CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Info, result.Diagnostics, Array.Empty<(Location, string)>());
-            CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Warning, result.Diagnostics, Array.Empty<(Location, string)>());
-            CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Error, result.Diagnostics, Array.Empty<(Location, string)>());
+            Assert.Empty(result.Diagnostics);
 
             // With STJ usage.
             source = """
@@ -275,9 +245,7 @@ namespace System.Text.Json.SourceGeneration.UnitTests
             compilation = CompilationHelper.CreateCompilation(source);
             result = CompilationHelper.RunJsonSourceGenerator(compilation);
 
-            CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Info, result.Diagnostics, Array.Empty<(Location, string)>());
-            CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Warning, result.Diagnostics, Array.Empty<(Location, string)>());
-            CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Error, result.Diagnostics, Array.Empty<(Location, string)>());
+            Assert.Empty(result.Diagnostics);
         }
 
         [Fact]
@@ -286,9 +254,7 @@ namespace System.Text.Json.SourceGeneration.UnitTests
             Compilation compilation = CompilationHelper.CreateCompilationWithInitOnlyProperties();
             JsonSourceGeneratorResult result = CompilationHelper.RunJsonSourceGenerator(compilation);
 
-            CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Info, result.Diagnostics, Array.Empty<(Location, string)>());
-            CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Warning, result.Diagnostics, Array.Empty<(Location, string)>());
-            CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Error, result.Diagnostics, Array.Empty<(Location, string)>());
+            Assert.Empty(result.Diagnostics);
         }
 
         [Fact]
@@ -297,9 +263,7 @@ namespace System.Text.Json.SourceGeneration.UnitTests
             Compilation compilation = CompilationHelper.CreateCompilationWithConstructorInitOnlyProperties();
             JsonSourceGeneratorResult result = CompilationHelper.RunJsonSourceGenerator(compilation);
 
-            CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Info, result.Diagnostics, Array.Empty<(Location, string)>());
-            CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Warning, result.Diagnostics, Array.Empty<(Location, string)>());
-            CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Error, result.Diagnostics, Array.Empty<(Location, string)>());
+            Assert.Empty(result.Diagnostics);
         }
         
         [Fact]
@@ -308,9 +272,7 @@ namespace System.Text.Json.SourceGeneration.UnitTests
             Compilation compilation = CompilationHelper.CreateCompilationWithMixedInitOnlyProperties();
             JsonSourceGeneratorResult result = CompilationHelper.RunJsonSourceGenerator(compilation);
 
-            CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Info, result.Diagnostics, Array.Empty<(Location, string)>());
-            CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Warning, result.Diagnostics, Array.Empty<(Location, string)>());
-            CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Error, result.Diagnostics, Array.Empty<(Location, string)>());
+            Assert.Empty(result.Diagnostics);
         }
 
         [Fact]
@@ -319,9 +281,7 @@ namespace System.Text.Json.SourceGeneration.UnitTests
             Compilation compilation = CompilationHelper.CreateCompilationWithRecordPositionalParameters();
             JsonSourceGeneratorResult result = CompilationHelper.RunJsonSourceGenerator(compilation);
 
-            CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Info, result.Diagnostics, Array.Empty<(Location, string)>());
-            CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Warning, result.Diagnostics, Array.Empty<(Location, string)>());
-            CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Error, result.Diagnostics, Array.Empty<(Location, string)>());
+            Assert.Empty(result.Diagnostics);
         }
 
         [Fact]
@@ -330,9 +290,7 @@ namespace System.Text.Json.SourceGeneration.UnitTests
             Compilation compilation = CompilationHelper.CreateCompilationWithRequiredProperties();
             JsonSourceGeneratorResult result = CompilationHelper.RunJsonSourceGenerator(compilation);
 
-            CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Info, result.Diagnostics, Array.Empty<(Location, string)>());
-            CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Warning, result.Diagnostics, Array.Empty<(Location, string)>());
-            CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Error, result.Diagnostics, Array.Empty<(Location, string)>());
+            Assert.Empty(result.Diagnostics);
         }
 
         [Fact]
@@ -347,18 +305,16 @@ namespace System.Text.Json.SourceGeneration.UnitTests
             Location internalFieldLocation = compilation.GetSymbolsWithName("internalField").First().Locations[0];
             Location privateFieldLocation = compilation.GetSymbolsWithName("privateField").First().Locations[0];
 
-            (Location, string)[] expectedWarningDiagnostics = new (Location, string)[]
+            var expectedDiagnostics = new DiagnosticData[]
             {
-                (idLocation, "The member 'Location.Id' has been annotated with the JsonIncludeAttribute but is not visible to the source generator."),
-                (address2Location, "The member 'Location.Address2' has been annotated with the JsonIncludeAttribute but is not visible to the source generator."),
-                (countryLocation, "The member 'Location.Country' has been annotated with the JsonIncludeAttribute but is not visible to the source generator."),
-                (internalFieldLocation, "The member 'Location.internalField' has been annotated with the JsonIncludeAttribute but is not visible to the source generator."),
-                (privateFieldLocation, "The member 'Location.privateField' has been annotated with the JsonIncludeAttribute but is not visible to the source generator."),
+                new(DiagnosticSeverity.Warning, idLocation, "The member 'Location.Id' has been annotated with the JsonIncludeAttribute but is not visible to the source generator."),
+                new(DiagnosticSeverity.Warning, address2Location, "The member 'Location.Address2' has been annotated with the JsonIncludeAttribute but is not visible to the source generator."),
+                new(DiagnosticSeverity.Warning, countryLocation, "The member 'Location.Country' has been annotated with the JsonIncludeAttribute but is not visible to the source generator."),
+                new(DiagnosticSeverity.Warning, internalFieldLocation, "The member 'Location.internalField' has been annotated with the JsonIncludeAttribute but is not visible to the source generator."),
+                new(DiagnosticSeverity.Warning, privateFieldLocation, "The member 'Location.privateField' has been annotated with the JsonIncludeAttribute but is not visible to the source generator."),
             };
 
-            CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Info, result.Diagnostics, Array.Empty<(Location, string)>());
-            CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Warning, result.Diagnostics, expectedWarningDiagnostics, sort: false);
-            CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Error, result.Diagnostics, Array.Empty<(Location, string)>());
+            CompilationHelper.AssertEqualDiagnosticMessages(expectedDiagnostics, result.Diagnostics);
         }
 
         [Fact]
@@ -369,14 +325,30 @@ namespace System.Text.Json.SourceGeneration.UnitTests
 
             Location myBaseClassLocation = compilation.GetSymbolsWithName("MyBaseClass").First().Locations[0];
 
-            (Location, string)[] expectedWarningDiagnostics = new (Location, string)[]
+            var expectedDiagnostics = new DiagnosticData[]
+            {
+                new(DiagnosticSeverity.Warning, myBaseClassLocation, "Type 'HelloWorld.MyBaseClass' is annotated with 'JsonDerivedTypeAttribute' which is not supported in 'JsonSourceGenerationMode.Serialization'."),
+            };
+
+            CompilationHelper.AssertEqualDiagnosticMessages(expectedDiagnostics, result.Diagnostics);
+        }
+
+        [Fact]
+        public void JsonStringEnumConverterWarns()
+        {
+            Compilation compilation = CompilationHelper.CreateTypesAnnotatedWithJsonStringEnumConverter();
+            JsonSourceGeneratorResult result = CompilationHelper.RunJsonSourceGenerator(compilation);
+
+            Location enum2PropLocation = compilation.GetSymbolsWithName("Enum2Prop").First().Locations[0];
+            Location enum1TypeLocation = compilation.GetSymbolsWithName("Enum1").First().Locations[0];
+
+            var expectedDiagnostics = new DiagnosticData[]
             {
-                (myBaseClassLocation, "Type 'HelloWorld.MyBaseClass' is annotated with 'JsonDerivedTypeAttribute' which is not supported in 'JsonSourceGenerationMode.Serialization'."),
+                new(DiagnosticSeverity.Warning, enum1TypeLocation, "The member 'HelloWorld.Enum1' has been annotated with 'JsonStringEnumConverter' which is not supported in native AOT. Consider using the generic 'JsonStringEnumConverter<TEnum>' instead."),
+                new(DiagnosticSeverity.Warning, enum2PropLocation, "The member 'HelloWorld.MyClass.Enum2Prop' has been annotated with 'JsonStringEnumConverter' which is not supported in native AOT. Consider using the generic 'JsonStringEnumConverter<TEnum>' instead."),
             };
 
-            CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Info, result.Diagnostics, Array.Empty<(Location, string)>());
-            CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Warning, result.Diagnostics, expectedWarningDiagnostics, sort: false);
-            CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Error, result.Diagnostics, Array.Empty<(Location, string)>());
+            CompilationHelper.AssertEqualDiagnosticMessages(expectedDiagnostics, result.Diagnostics);
         }
     }
 }
index 749673c..e511ca4 100644 (file)
@@ -230,9 +230,7 @@ namespace System.Text.Json.SourceGeneration.UnitTests
                 Assert.Empty(result.AllGeneratedTypes);
             }
 
-            CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Info, result.Diagnostics, Array.Empty<(Location, string)>());
-            CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Warning, result.Diagnostics, Array.Empty<(Location, string)>());
-            CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Error, result.Diagnostics, Array.Empty<(Location, string)>());
+            Assert.Empty(result.Diagnostics);
         }
 
         [Theory]
@@ -262,10 +260,7 @@ namespace System.Text.Json.SourceGeneration.UnitTests
             JsonSourceGeneratorResult result = CompilationHelper.RunJsonSourceGenerator(compilation);
 
             Assert.Empty(result.AllGeneratedTypes);
-
-            CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Info, result.Diagnostics, Array.Empty<(Location, string)>());
-            CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Warning, result.Diagnostics, Array.Empty<(Location, string)>());
-            CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Error, result.Diagnostics, Array.Empty<(Location, string)>());
+            Assert.Empty(result.Diagnostics);
         }
 
         [Fact]
@@ -602,9 +597,7 @@ namespace System.Text.Json.SourceGeneration.UnitTests
             ImmutableArray<Diagnostic> generatorDiags = result.NewCompilation.GetDiagnostics();
 
             // No diagnostics expected.
-            CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Info, generatorDiags, Array.Empty<(Location, string)>());
-            CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Warning, generatorDiags, Array.Empty<(Location, string)>());
-            CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Error, generatorDiags, Array.Empty<(Location, string)>());
+            Assert.Empty(result.Diagnostics);
         }
 
         [Fact]
@@ -632,9 +625,7 @@ namespace System.Text.Json.SourceGeneration.UnitTests
             ImmutableArray<Diagnostic> generatorDiags = result.NewCompilation.GetDiagnostics();
 
             // No diagnostics expected.
-            CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Info, generatorDiags, Array.Empty<(Location, string)>());
-            CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Warning, generatorDiags, Array.Empty<(Location, string)>());
-            CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Error, generatorDiags, Array.Empty<(Location, string)>());
+            Assert.Empty(result.Diagnostics);
         }
 
         [Fact]
@@ -695,9 +686,7 @@ namespace System.Text.Json.SourceGeneration.UnitTests
             ImmutableArray<Diagnostic> generatorDiags = result.NewCompilation.GetDiagnostics();
 
             // No diagnostics expected.
-            CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Info, generatorDiags, Array.Empty<(Location, string)>());
-            CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Warning, generatorDiags, Array.Empty<(Location, string)>());
-            CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Error, generatorDiags, Array.Empty<(Location, string)>());
+            Assert.Empty(result.Diagnostics);
         }
 
         [Fact]
index 062a7e9..adceb7d 100644 (file)
@@ -2,6 +2,7 @@
 // The .NET Foundation licenses this file to you under the MIT license.
 
 using System.Collections.Generic;
+using System.Reflection;
 using Xunit;
 
 namespace System.Text.Json.Serialization.Tests
@@ -113,44 +114,60 @@ namespace System.Text.Json.Serialization.Tests
             public ICollection<InvalidTypeConverterEnum> MyEnumValues { get; set; }
         }
 
+        private class InvalidEnumTypeConverterClass
+        {
+            [JsonConverter(typeof(JsonStringEnumConverter<BindingFlags>))]
+            public InvalidTypeConverterEnum MyEnumValues { get; set; }
+        }
+
         private enum InvalidTypeConverterEnum
         {
             Value1,
             Value2,
         }
 
-        [Fact]
-        public static void AttributeOnPropertyFail()
+        [Theory]
+        [InlineData(typeof(InvalidTypeConverterClass),
+            "System.Text.Json.Serialization.Tests.CustomConverterTests+InvalidTypeConverterClass.MyEnumValues",
+            "System.Collections.Generic.ICollection`1[System.Text.Json.Serialization.Tests.CustomConverterTests+InvalidTypeConverterEnum]")]
+        [InlineData(typeof(InvalidEnumTypeConverterClass),
+            "System.Text.Json.Serialization.Tests.CustomConverterTests+InvalidEnumTypeConverterClass.MyEnumValues",
+            "System.Text.Json.Serialization.Tests.CustomConverterTests+InvalidTypeConverterEnum")]
+        public static void AttributeOnPropertyFail(Type type, string propertyName, string propertyTypeName)
         {
+            object value = Activator.CreateInstance(type);
             InvalidOperationException ex;
 
-            ex = Assert.Throws<InvalidOperationException>(() => JsonSerializer.Serialize(new InvalidTypeConverterClass()));
+            ex = Assert.Throws<InvalidOperationException>(() => JsonSerializer.Serialize(value, type));
             // Message should be in the form "The converter specified on 'System.Text.Json.Serialization.Tests.CustomConverterTests+InvalidTypeConverterClass.MyEnumValues' is not compatible with the type 'System.Collections.Generic.ICollection`1[System.Text.Json.Serialization.Tests.CustomConverterTests+InvalidTypeConverterEnum]'."
-            Assert.Contains("'System.Text.Json.Serialization.Tests.CustomConverterTests+InvalidTypeConverterClass.MyEnumValues'", ex.Message);
+            Assert.Contains($"'{propertyName}'", ex.Message);
 
-            ex = Assert.Throws<InvalidOperationException>(() => JsonSerializer.Deserialize<InvalidTypeConverterClass>("{}"));
-            Assert.Contains("'System.Text.Json.Serialization.Tests.CustomConverterTests+InvalidTypeConverterClass.MyEnumValues'", ex.Message);
-            Assert.Contains("'System.Collections.Generic.ICollection`1[System.Text.Json.Serialization.Tests.CustomConverterTests+InvalidTypeConverterEnum]'", ex.Message);
+            ex = Assert.Throws<InvalidOperationException>(() => JsonSerializer.Deserialize("{}", type));
+            Assert.Contains($"'{propertyName}'", ex.Message);
+            Assert.Contains($"'{propertyTypeName}'", ex.Message);
         }
 
         [JsonConverter(typeof(JsonStringEnumConverter))]
         private class InvalidTypeConverterClassWithAttribute { }
 
-        [Fact]
-        public static void AttributeOnClassFail()
-        {
-            const string expectedSubStr = "'System.Text.Json.Serialization.Tests.CustomConverterTests+InvalidTypeConverterClassWithAttribute'";
+        [JsonConverter(typeof(JsonStringEnumConverter<BindingFlags>))]
+        private enum InvalidTypeConverterEnumWithAttribute { }
 
+        [Theory]
+        [InlineData(typeof(InvalidTypeConverterClassWithAttribute), "System.Text.Json.Serialization.Tests.CustomConverterTests+InvalidTypeConverterClassWithAttribute")]
+        [InlineData(typeof(InvalidTypeConverterEnumWithAttribute), "System.Text.Json.Serialization.Tests.CustomConverterTests+InvalidTypeConverterEnumWithAttribute")]
+        public static void AttributeOnClassFail(Type type, string expectedSubStr)
+        {
             InvalidOperationException ex;
+            object value = Activator.CreateInstance(type);
 
-            ex = Assert.Throws<InvalidOperationException>(() => JsonSerializer.Serialize(new InvalidTypeConverterClassWithAttribute()));
-            // Message should be in the form "The converter specified on 'System.Text.Json.Serialization.Tests.CustomConverterTests+InvalidTypeConverterClassWithAttribute' is not compatible with the type 'System.Text.Json.Serialization.Tests.CustomConverterTests+InvalidTypeConverterClassWithAttribute'."
+            ex = Assert.Throws<InvalidOperationException>(() => JsonSerializer.Serialize(value, type));
 
             int pos = ex.Message.IndexOf(expectedSubStr);
             Assert.True(pos > 0);
             Assert.Contains(expectedSubStr, ex.Message.Substring(pos + expectedSubStr.Length)); // The same string is repeated again.
 
-            ex = Assert.Throws<InvalidOperationException>(() => JsonSerializer.Deserialize<InvalidTypeConverterClassWithAttribute>("{}"));
+            ex = Assert.Throws<InvalidOperationException>(() => JsonSerializer.Deserialize("{}", type));
             pos = ex.Message.IndexOf(expectedSubStr);
             Assert.True(pos > 0);
             Assert.Contains(expectedSubStr, ex.Message.Substring(pos + expectedSubStr.Length));
index ddf0e5f..2c7516f 100644 (file)
@@ -13,11 +13,49 @@ namespace System.Text.Json.Serialization.Tests
 {
     public class EnumConverterTests
     {
-        [Fact]
-        public void ConvertDayOfWeek()
+        [Theory]
+        [InlineData(typeof(JsonStringEnumConverter), typeof(DayOfWeek))]
+        [InlineData(typeof(JsonStringEnumConverter), typeof(MyCustomEnum))]
+        [InlineData(typeof(JsonStringEnumConverter<DayOfWeek>), typeof(DayOfWeek))]
+        [InlineData(typeof(JsonStringEnumConverter<MyCustomEnum>), typeof(MyCustomEnum))]
+        public static void JsonStringEnumConverter_SupportedType_WorksAsExpected(Type converterType, Type supportedType)
+        {
+            var options = new JsonSerializerOptions();
+            var factory = (JsonConverterFactory)Activator.CreateInstance(converterType);
+
+            Assert.True(factory.CanConvert(supportedType));
+
+            JsonConverter converter = factory.CreateConverter(supportedType, options);
+            // TODO use https://github.com/dotnet/runtime/issues/63898 once implemented
+            Type expectedConverterType = typeof(JsonConverter<>).MakeGenericType(supportedType);
+            Assert.IsAssignableFrom(expectedConverterType, converter);
+        }
+
+        [Theory]
+        [InlineData(typeof(JsonStringEnumConverter), typeof(int))]
+        [InlineData(typeof(JsonStringEnumConverter), typeof(string))]
+        [InlineData(typeof(JsonStringEnumConverter), typeof(JsonStringEnumConverter))]
+        [InlineData(typeof(JsonStringEnumConverter<DayOfWeek>), typeof(int))]
+        [InlineData(typeof(JsonStringEnumConverter<DayOfWeek>), typeof(string))]
+        [InlineData(typeof(JsonStringEnumConverter<DayOfWeek>), typeof(JsonStringEnumConverter<MyCustomEnum>))]
+        [InlineData(typeof(JsonStringEnumConverter<DayOfWeek>), typeof(MyCustomEnum))]
+        [InlineData(typeof(JsonStringEnumConverter<MyCustomEnum>), typeof(DayOfWeek))]
+        public static void JsonStringEnumConverter_InvalidType_ThrowsArgumentOutOfRangeException(Type converterType, Type unsupportedType)
+        {
+            var options = new JsonSerializerOptions();
+            var factory = (JsonConverterFactory)Activator.CreateInstance(converterType);
+
+            Assert.False(factory.CanConvert(unsupportedType));
+            ArgumentOutOfRangeException ex = Assert.Throws<ArgumentOutOfRangeException>(() => factory.CreateConverter(unsupportedType, options));
+            Assert.Contains(unsupportedType.FullName, ex.Message);
+        }
+
+        [Theory]
+        [InlineData(false)]
+        [InlineData(true)]
+        public void ConvertDayOfWeek(bool useGenericVariant)
         {
-            JsonSerializerOptions options = new JsonSerializerOptions();
-            options.Converters.Add(new JsonStringEnumConverter());
+            JsonSerializerOptions options = CreateStringEnumOptionsForType<DayOfWeek>(useGenericVariant);
 
             WhenClass when = JsonSerializer.Deserialize<WhenClass>(@"{""Day"":""Monday""}", options);
             Assert.Equal(DayOfWeek.Monday, when.Day);
@@ -36,8 +74,7 @@ namespace System.Text.Json.Serialization.Tests
             Assert.Equal(@"""Friday""", json);
 
             // Try a unique naming policy
-            options = new JsonSerializerOptions();
-            options.Converters.Add(new JsonStringEnumConverter(new ToLowerNamingPolicy()));
+            options = CreateStringEnumOptionsForType<DayOfWeek>(useGenericVariant, new ToLowerNamingPolicy());
 
             json = JsonSerializer.Serialize(DayOfWeek.Friday, options);
             Assert.Equal(@"""friday""", json);
@@ -47,8 +84,7 @@ namespace System.Text.Json.Serialization.Tests
             Assert.Equal(@"-1", json);
 
             // Not permitting integers should throw
-            options = new JsonSerializerOptions();
-            options.Converters.Add(new JsonStringEnumConverter(allowIntegerValues: false));
+            options = CreateStringEnumOptionsForType<DayOfWeek>(useGenericVariant, allowIntegerValues: false);
             Assert.Throws<JsonException>(() => JsonSerializer.Serialize((DayOfWeek)(-1), options));
         }
 
@@ -62,11 +98,12 @@ namespace System.Text.Json.Serialization.Tests
             public DayOfWeek Day { get; set; }
         }
 
-        [Fact]
-        public void ConvertFileAttributes()
+        [Theory]
+        [InlineData(false)]
+        [InlineData(true)]
+        public void ConvertFileAttributes(bool useGenericVariant)
         {
-            JsonSerializerOptions options = new JsonSerializerOptions();
-            options.Converters.Add(new JsonStringEnumConverter());
+            JsonSerializerOptions options = CreateStringEnumOptionsForType<FileAttributes>(useGenericVariant);
 
             FileState state = JsonSerializer.Deserialize<FileState>(@"{""Attributes"":""ReadOnly""}", options);
             Assert.Equal(FileAttributes.ReadOnly, state.Attributes);
@@ -95,8 +132,7 @@ namespace System.Text.Json.Serialization.Tests
             Assert.Equal(@"""Temporary, Offline""", json);
 
             // Try a unique casing
-            options = new JsonSerializerOptions();
-            options.Converters.Add(new JsonStringEnumConverter(new ToLowerNamingPolicy()));
+            options = CreateStringEnumOptionsForType<FileAttributes>(useGenericVariant, new ToLowerNamingPolicy());
 
             json = JsonSerializer.Serialize(FileAttributes.NoScrubData, options);
             Assert.Equal(@"""noscrubdata""", json);
@@ -108,13 +144,11 @@ namespace System.Text.Json.Serialization.Tests
             Assert.Equal(@"-1", json);
 
             // Not permitting integers should throw
-            options = new JsonSerializerOptions();
-            options.Converters.Add(new JsonStringEnumConverter(allowIntegerValues: false));
+            options = CreateStringEnumOptionsForType<FileAttributes>(useGenericVariant, allowIntegerValues: false);
             Assert.Throws<JsonException>(() => JsonSerializer.Serialize((FileAttributes)(-1), options));
 
             // Flag values honor naming policy correctly
-            options = new JsonSerializerOptions();
-            options.Converters.Add(new JsonStringEnumConverter(new SimpleSnakeCasePolicy()));
+            options = CreateStringEnumOptionsForType<FileAttributes>(useGenericVariant, new SimpleSnakeCasePolicy());
 
             json = JsonSerializer.Serialize(
                 FileAttributes.Directory | FileAttributes.Compressed | FileAttributes.IntegrityStream,
@@ -140,6 +174,11 @@ namespace System.Text.Json.Serialization.Tests
             public DayOfWeek WorkEnd { get; set; }
             [JsonConverter(typeof(LowerCaseEnumConverter))]
             public DayOfWeek WeekEnd { get; set; }
+
+            [JsonConverter(typeof(JsonStringEnumConverter<DayOfWeek>))]
+            public DayOfWeek WorkStart2 { get; set; }
+            [JsonConverter(typeof(LowerCaseEnumConverter<DayOfWeek>))]
+            public DayOfWeek WeekEnd2 { get; set; }
         }
 
         private class LowerCaseEnumConverter : JsonStringEnumConverter
@@ -149,17 +188,34 @@ namespace System.Text.Json.Serialization.Tests
             }
         }
 
+        private class LowerCaseEnumConverter<TEnum> : JsonStringEnumConverter<TEnum>
+            where TEnum : struct, Enum
+        {
+            public LowerCaseEnumConverter() : base(new ToLowerNamingPolicy())
+            {
+            }
+        }
+
         [Fact]
         public void ConvertEnumUsingAttributes()
         {
-            Week week = new Week { WorkStart = DayOfWeek.Monday, WorkEnd = DayOfWeek.Friday, WeekEnd = DayOfWeek.Saturday };
+            Week week = new Week {
+                WorkStart = DayOfWeek.Monday,
+                WorkEnd = DayOfWeek.Friday,
+                WeekEnd = DayOfWeek.Saturday,
+                WorkStart2 = DayOfWeek.Tuesday,
+                WeekEnd2 = DayOfWeek.Thursday,
+            };
+
             string json = JsonSerializer.Serialize(week);
-            Assert.Equal(@"{""WorkStart"":""Monday"",""WorkEnd"":5,""WeekEnd"":""saturday""}", json);
+            Assert.Equal("""{"WorkStart":"Monday","WorkEnd":5,"WeekEnd":"saturday","WorkStart2":"Tuesday","WeekEnd2":"thursday"}""", json);
 
             week = JsonSerializer.Deserialize<Week>(json);
             Assert.Equal(DayOfWeek.Monday, week.WorkStart);
             Assert.Equal(DayOfWeek.Friday, week.WorkEnd);
             Assert.Equal(DayOfWeek.Saturday, week.WeekEnd);
+            Assert.Equal(DayOfWeek.Tuesday, week.WorkStart2);
+            Assert.Equal(DayOfWeek.Thursday, week.WeekEnd2);
         }
 
         [Fact]
@@ -190,26 +246,34 @@ namespace System.Text.Json.Serialization.Tests
             Second = 2
         }
 
-        [Fact]
-        public void EnumWithConverterAttribute()
+        [JsonConverter(typeof(JsonStringEnumConverter<MyCustomEnum2>))]
+        private enum MyCustomEnum2
         {
-            string json = JsonSerializer.Serialize(MyCustomEnum.Second);
-            Assert.Equal(@"""Second""", json);
+            First = 1,
+            Second = 2
+        }
 
-            MyCustomEnum obj = JsonSerializer.Deserialize<MyCustomEnum>("\"Second\"");
-            Assert.Equal(MyCustomEnum.Second, obj);
+        [Theory]
+        [InlineData(typeof(MyCustomEnum), MyCustomEnum.Second, "\"Second\"", "2")]
+        [InlineData(typeof(MyCustomEnum2), MyCustomEnum2.Second, "\"Second\"", "2")]
+        public void EnumWithConverterAttribute(Type enumType, object value, string expectedJson, string alternativeJson)
+        {
+            string json = JsonSerializer.Serialize(value, enumType);
+            Assert.Equal(expectedJson, json);
+
+            object? result = JsonSerializer.Deserialize(json, enumType);
+            Assert.Equal(value, result);
 
-            obj = JsonSerializer.Deserialize<MyCustomEnum>("2");
-            Assert.Equal(MyCustomEnum.Second, obj);
+            result = JsonSerializer.Deserialize(alternativeJson, enumType);
+            Assert.Equal(value, result);
         }
 
-        [Fact]
-        public static void EnumWithNoValues()
+        [Theory]
+        [InlineData(false)]
+        [InlineData(true)]
+        public static void EnumWithNoValues(bool useGenericVariant)
         {
-            var options = new JsonSerializerOptions
-            {
-                Converters = { new JsonStringEnumConverter() }
-            };
+            JsonSerializerOptions options = CreateStringEnumOptionsForType<EmptyEnum>(useGenericVariant);
 
             Assert.Equal("-1", JsonSerializer.Serialize((EmptyEnum)(-1), options));
             Assert.Equal("1", JsonSerializer.Serialize((EmptyEnum)(1), options));
@@ -217,13 +281,12 @@ namespace System.Text.Json.Serialization.Tests
 
         public enum EmptyEnum { };
 
-        [Fact]
-        public static void MoreThan64EnumValuesToSerialize()
+        [Theory]
+        [InlineData(false)]
+        [InlineData(true)]
+        public static void MoreThan64EnumValuesToSerialize(bool useGenericVariant)
         {
-            var options = new JsonSerializerOptions
-            {
-                Converters = { new JsonStringEnumConverter() }
-            };
+            JsonSerializerOptions options = CreateStringEnumOptionsForType<MyEnum>(useGenericVariant);
 
             for (int i = 0; i < 128; i++)
             {
@@ -234,13 +297,12 @@ namespace System.Text.Json.Serialization.Tests
             }
         }
 
-        [Fact]
-        public static void MoreThan64EnumValuesToSerializeWithNamingPolicy()
+        [Theory]
+        [InlineData(false)]
+        [InlineData(true)]
+        public static void MoreThan64EnumValuesToSerializeWithNamingPolicy(bool useGenericVariant)
         {
-            var options = new JsonSerializerOptions
-            {
-                Converters = { new JsonStringEnumConverter(new ToLowerNamingPolicy()) }
-            };
+            JsonSerializerOptions options = CreateStringEnumOptionsForType<MyEnum>(useGenericVariant, new ToLowerNamingPolicy());
 
             for (int i = 0; i < 128; i++)
             {
@@ -556,16 +618,12 @@ namespace System.Text.Json.Serialization.Tests
             G = 1 << 6,
         }
 
-        [Fact]
-        public static void Honor_EnumNamingPolicy_On_Deserialization()
+        [Theory]
+        [InlineData(false)]
+        [InlineData(true)]
+        public static void Honor_EnumNamingPolicy_On_Deserialization(bool useGenericVariant)
         {
-            JsonSerializerOptions options = new()
-            {
-                Converters =
-                {
-                    new JsonStringEnumConverter(namingPolicy: new SimpleSnakeCasePolicy() )
-                }
-            };
+            JsonSerializerOptions options = CreateStringEnumOptionsForType<BindingFlags>(useGenericVariant, new SimpleSnakeCasePolicy());
 
             BindingFlags bindingFlags = JsonSerializer.Deserialize<BindingFlags>(@"""non_public""", options);
             Assert.Equal(BindingFlags.NonPublic, bindingFlags);
@@ -585,18 +643,14 @@ namespace System.Text.Json.Serialization.Tests
             Assert.Null(JsonSerializer.Deserialize<BindingFlags?>("null", options));
         }
 
-        [Fact]
-        public static void EnumDictionaryKeyDeserialization()
+        [Theory]
+        [InlineData(false)]
+        [InlineData(true)]
+        public static void EnumDictionaryKeyDeserialization(bool useGenericVariant)
         {
             JsonNamingPolicy snakeCasePolicy = new SimpleSnakeCasePolicy();
-            JsonSerializerOptions options = new()
-            {
-                Converters =
-                {
-                    new JsonStringEnumConverter(namingPolicy: snakeCasePolicy)
-                },
-                DictionaryKeyPolicy = snakeCasePolicy
-            };
+            JsonSerializerOptions options = CreateStringEnumOptionsForType<BindingFlags>(useGenericVariant);
+            options.DictionaryKeyPolicy = snakeCasePolicy;
 
             // Baseline.
             var dict = JsonSerializer.Deserialize<Dictionary<BindingFlags, int>>(@"{""NonPublic, Public"": 1}", options);
@@ -635,5 +689,18 @@ namespace System.Text.Json.Serialization.Tests
         {
             public override string ConvertName(string name) => name + "0";
         }
+
+        private static JsonSerializerOptions CreateStringEnumOptionsForType<TEnum>(bool useGenericVariant, JsonNamingPolicy? namingPolicy = null, bool allowIntegerValues = true) where TEnum : struct, Enum
+        {
+            return new JsonSerializerOptions
+            {
+                Converters =
+                {
+                    useGenericVariant
+                    ? new JsonStringEnumConverter<TEnum>(namingPolicy, allowIntegerValues)
+                    : new JsonStringEnumConverter(namingPolicy, allowIntegerValues)
+                }
+            };
+        }
     }
 }
index 1d49796..e3cd7e6 100644 (file)
     <Left>net7.0/System.Runtime.Loader.dll</Left>
     <Right>net8.0/System.Runtime.Loader.dll</Right>
   </Suppression>
+  <Suppression>
+    <DiagnosticId>CP0015</DiagnosticId>
+    <Target>T:System.Text.Json.Serialization.JsonStringEnumConverter:[T:System.Diagnostics.CodeAnalysis.RequiresDynamicCodeAttribute]</Target>
+    <Left>net7.0/System.Text.Json.dll</Left>
+    <Right>net8.0/System.Text.Json.dll</Right>
+  </Suppression>
 </Suppressions>
\ No newline at end of file