diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/EnumConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/EnumConverter.cs
index d6e1d6cbd3a301..51874b7abaec0b 100644
--- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/EnumConverter.cs
+++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/EnumConverter.cs
@@ -454,8 +454,17 @@ private bool IsDefinedValueOrCombinationOfValues(ulong key)
{
if (s_isFlagsEnum)
{
- ulong remainingBits = key;
+ // First try to match exact field (for performance)
+ foreach (EnumFieldInfo fieldInfo in _enumFieldInfo)
+ {
+ if (fieldInfo.Key == key)
+ {
+ return true;
+ }
+ }
+ // Try the standard bit-by-bit matching
+ ulong remainingBits = key;
foreach (EnumFieldInfo fieldInfo in _enumFieldInfo)
{
ulong fieldKey = fieldInfo.Key;
@@ -470,6 +479,27 @@ private bool IsDefinedValueOrCombinationOfValues(ulong key)
}
}
+ // If the above failed, try matching by analyzing field combinations
+ // This ensures backwards compatibility with .NET 8 behavior for flags enums with combination values
+ if (remainingBits != 0)
+ {
+ // Similar to the approach used by FormatEnumAsString, but we don't need to build the actual string.
+ // We only need to know if any field matches at all - if it does, we'll format it as a string.
+ // This mimics the .NET 8 behavior where any flags enum with at least one named field would
+ // be formatted as a string rather than a number.
+ ulong unmatchedBits = key;
+
+ foreach (EnumFieldInfo enumField in _enumFieldInfo)
+ {
+ ulong fieldKey = enumField.Key;
+ if (fieldKey == 0 ? key == 0 : (unmatchedBits & fieldKey) == fieldKey)
+ {
+ unmatchedBits &= ~fieldKey;
+ return true; // Found at least one field that matches part of the value
+ }
+ }
+ }
+
return false;
}
else
diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/EnumConverterTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/EnumConverterTests.cs
index aecc33a26760b1..0c8292f67154d5 100644
--- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/EnumConverterTests.cs
+++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/EnumConverterTests.cs
@@ -1007,6 +1007,218 @@ public enum EnumFlagsWithMemberAttributes
Value4 = 8,
Value5 = 16,
}
+
+ ///
+ /// Flag enum used to test serialization of flags where all bits have defined values.
+ ///
+ [Flags]
+ public enum MyFlagsEnum1
+ {
+ UNKNOWN = 0,
+ BIT0 = 1,
+ BIT1 = 2,
+ BIT2 = 4,
+ BIT3 = 8,
+ BITS01 = 3,
+ }
+
+ ///
+ /// Flag enum used to test serialization of flags where not all bits have defined values.
+ /// This is the regression case for the issue fixed in this PR.
+ ///
+ [Flags]
+ public enum MyFlagsEnum2
+ {
+ UNKNOWN = 0,
+ BIT0 = 1,
+ // direct option for bit 1 missing
+ BIT2 = 4,
+ BIT3 = 8,
+ BITS01 = 3,
+ }
+
+ ///
+ /// Complex flag enum with various combinations and missing definitions for certain combinations.
+ ///
+ [Flags]
+ public enum ComplexFlagsEnum
+ {
+ None = 0,
+ Flag1 = 1,
+ Flag2 = 2,
+ Flag4 = 4,
+ Flag8 = 8,
+ Flag16 = 16,
+ Flag32 = 32,
+ Combo1And2 = Flag1 | Flag2,
+ Combo4And8And16 = Flag4 | Flag8 | Flag16,
+ // No definition for Flag32 combinations
+ }
+
+ ///
+ /// Flag enum with negative values to test serialization behavior
+ ///
+ [Flags]
+ public enum FlagsWithNegativeValues
+ {
+ None = 0,
+ Negative = -2147483648, // int.MinValue
+ Max = 0x7FFFFFFF, // int.MaxValue
+ All = -1 // All bits set to 1 (0xFFFFFFFF)
+ }
+
+ ///
+ /// Flag enum with overlapping values to test serialization behavior
+ ///
+ [Flags]
+ public enum FlagsWithOverlappingValues
+ {
+ None = 0,
+ One = 1,
+ Two = 2,
+ Four = 4,
+ All = 7,
+ OneAndFour = 5,
+ // Both All and (One, Two, Four) represent value 7
+ }
+
+ ///
+ /// Test for the flags enum regression fix where flag enums with missing bit definitions
+ /// would be serialized as numbers instead of strings in .NET 9 but not in .NET 8.
+ ///
+ [Fact]
+ public static void JsonStringEnumConverter_SerializesFlagsEnumWithMissingBits()
+ {
+ JsonSerializerOptions options = new()
+ {
+ WriteIndented = false,
+ Converters = { new JsonStringEnumConverter() }
+ };
+
+ // Test that our standard enum with all bits defined serializes correctly
+ var e1 = MyFlagsEnum1.BITS01 | MyFlagsEnum1.BIT3;
+ string json1 = JsonSerializer.Serialize(e1, options);
+ Assert.Equal("\"BITS01, BIT3\"", json1);
+
+ // Verify the enum with missing bit definition produces the same result
+ // This is the key regression test
+ var e2 = MyFlagsEnum2.BITS01 | MyFlagsEnum2.BIT3;
+ string json2 = JsonSerializer.Serialize(e2, options);
+ Assert.Equal("\"BITS01, BIT3\"", json2);
+ }
+
+ ///
+ /// Test for complex flags enum serialization with the JsonStringEnumConverter.
+ /// Ensures combinations that include bits without direct field definitions are
+ /// still serialized as strings rather than falling back to numeric representation.
+ ///
+ [Fact]
+ public static void JsonStringEnumConverter_SerializesComplexFlagsCombinations()
+ {
+ JsonSerializerOptions options = new()
+ {
+ WriteIndented = false,
+ Converters = { new JsonStringEnumConverter() }
+ };
+
+ // Test a complex combination including a flag that doesn't have
+ // a direct field definition but can be represented by other fields
+ var value = ComplexFlagsEnum.Combo1And2 | ComplexFlagsEnum.Combo4And8And16 | ComplexFlagsEnum.Flag32;
+ string json = JsonSerializer.Serialize(value, options);
+
+ // Verify we get the exact expected JSON string
+ Assert.Equal("\"Combo1And2, Combo4And8And16, Flag32\"", json);
+ }
+
+ ///
+ /// Test flag enum serialization with JsonStringEnumConverter
+ /// for values that include flags with overlapping or missing definitions.
+ ///
+ [Fact]
+ public static void JsonStringEnumConverter_SerializesFlagEnumsWithOverlappingAndMissingDefinitions()
+ {
+ JsonSerializerOptions options = new()
+ {
+ WriteIndented = false,
+ Converters = { new JsonStringEnumConverter() }
+ };
+
+ // Test AttributeTargets which is a [Flags] enum in the BCL
+ Assert.Equal("\"Class, Delegate\"", JsonSerializer.Serialize(
+ AttributeTargets.Class | AttributeTargets.Delegate,
+ options));
+
+ // Test with more flags
+ Assert.Equal("\"Class, Delegate, Interface\"", JsonSerializer.Serialize(
+ AttributeTargets.Class | AttributeTargets.Delegate | AttributeTargets.Interface,
+ options));
+
+ // Test All value
+ Assert.Equal("\"All\"", JsonSerializer.Serialize(AttributeTargets.All, options));
+
+ // Test with overlapping values
+ Assert.Equal("\"All\"", JsonSerializer.Serialize(FlagsWithOverlappingValues.All, options));
+ Assert.Equal("\"One, Four\"", JsonSerializer.Serialize(FlagsWithOverlappingValues.OneAndFour, options));
+
+ // Test One + Two + Four (equals All)
+ Assert.Equal("\"All\"", JsonSerializer.Serialize(
+ FlagsWithOverlappingValues.One | FlagsWithOverlappingValues.Two | FlagsWithOverlappingValues.Four,
+ options));
+ }
+
+ ///
+ /// Test flag enum serialization for negative values and boundary conditions
+ ///
+ [Fact]
+ public static void JsonStringEnumConverter_SerializesFlagEnumsWithNegativeValues()
+ {
+ JsonSerializerOptions options = new()
+ {
+ WriteIndented = false,
+ Converters = { new JsonStringEnumConverter() }
+ };
+
+ // Test individual flags
+ Assert.Equal("\"Negative\"", JsonSerializer.Serialize(FlagsWithNegativeValues.Negative, options));
+ Assert.Equal("\"Max\"", JsonSerializer.Serialize(FlagsWithNegativeValues.Max, options));
+ Assert.Equal("\"All\"", JsonSerializer.Serialize(FlagsWithNegativeValues.All, options));
+
+ // Test combinations
+ Assert.Equal("\"All\"", JsonSerializer.Serialize(
+ FlagsWithNegativeValues.Negative | FlagsWithNegativeValues.Max,
+ options));
+ }
+
+ ///
+ /// Test for round-trip serialization and deserialization of flag enums
+ ///
+ [Fact]
+ public static void JsonStringEnumConverter_RoundTripsFlagEnums()
+ {
+ JsonSerializerOptions options = new()
+ {
+ WriteIndented = false,
+ Converters = { new JsonStringEnumConverter() }
+ };
+
+ // Round trip with enum that has all bits defined
+ var e1 = MyFlagsEnum1.BIT0 | MyFlagsEnum1.BIT2;
+ string json1 = JsonSerializer.Serialize(e1, options);
+ var e1Result = JsonSerializer.Deserialize(json1, options);
+ Assert.Equal(e1, e1Result);
+
+ // Round trip with enum that has missing bit definitions
+ var e2 = MyFlagsEnum2.BIT0 | MyFlagsEnum2.BIT2;
+ string json2 = JsonSerializer.Serialize(e2, options);
+ var e2Result = JsonSerializer.Deserialize(json2, options);
+ Assert.Equal(e2, e2Result);
+
+ // Round trip with complex combinations
+ var complex = ComplexFlagsEnum.Combo1And2 | ComplexFlagsEnum.Flag32;
+ string jsonComplex = JsonSerializer.Serialize(complex, options);
+ var complexResult = JsonSerializer.Deserialize(jsonComplex, options);
+ Assert.Equal(complex, complexResult);
+ }
[Theory]
[InlineData(EnumWithConflictingMemberAttributes.Value1)]
@@ -1257,5 +1469,89 @@ public enum EnumWithVaryingNamingPolicies
A,
b,
}
+
+ ///
+ /// Test for the flags enum regression fix where flag enums with missing bit definitions
+ /// would be serialized as numbers instead of strings in .NET 9 but not in .NET 8.
+ ///
+ [Fact]
+ public static void JsonStringEnumConverter_SerializesFlagsEnumWithMissingBits()
+ {
+ JsonSerializerOptions options = new()
+ {
+ WriteIndented = false,
+ Converters = { new JsonStringEnumConverter() }
+ };
+
+ // Test that our standard enum with all bits defined serializes correctly
+ var e1 = MyFlagsEnum1.BITS01 | MyFlagsEnum1.BIT3;
+ string json1 = JsonSerializer.Serialize(e1, options);
+ Assert.Equal("\"BITS01, BIT3\"", json1);
+
+ // Verify the enum with missing bit definition produces the same result
+ // This is the key regression test
+ var e2 = MyFlagsEnum2.BITS01 | MyFlagsEnum2.BIT3;
+ string json2 = JsonSerializer.Serialize(e2, options);
+ Assert.Equal("\"BITS01, BIT3\"", json2);
+ }
+
+ ///
+ /// Test for complex flags enum serialization with the JsonStringEnumConverter.
+ /// Ensures combinations that include bits without direct field definitions are
+ /// still serialized as strings rather than falling back to numeric representation.
+ ///
+ [Fact]
+ public static void JsonStringEnumConverter_SerializesComplexFlagsCombinations()
+ {
+ JsonSerializerOptions options = new()
+ {
+ WriteIndented = false,
+ Converters = { new JsonStringEnumConverter() }
+ };
+
+ // Test a complex combination including a flag that doesn't have
+ // a direct field definition but can be represented by other fields
+ var value = ComplexFlagsEnum.Combo1And2 | ComplexFlagsEnum.Combo4And8And16 | ComplexFlagsEnum.Flag32;
+ string json = JsonSerializer.Serialize(value, options);
+
+ // Verify we get the exact expected JSON string
+ Assert.Equal("\"Combo1And2, Combo4And8And16, Flag32\"", json);
+ }
+
+ ///
+ /// Test flag enum serialization with JsonStringEnumConverter
+ /// for values that include flags with overlapping or missing definitions.
+ ///
+ [Fact]
+ public static void JsonStringEnumConverter_SerializesFlagEnumsWithOverlappingAndMissingDefinitions()
+ {
+ JsonSerializerOptions options = new()
+ {
+ WriteIndented = false,
+ Converters = { new JsonStringEnumConverter() }
+ };
+
+ // Test AttributeTargets which is a [Flags] enum in the BCL
+ Assert.Equal("\"Class, Delegate\"", JsonSerializer.Serialize(
+ AttributeTargets.Class | AttributeTargets.Delegate,
+ options));
+
+ // Test with more flags
+ Assert.Equal("\"Class, Delegate, Interface\"", JsonSerializer.Serialize(
+ AttributeTargets.Class | AttributeTargets.Delegate | AttributeTargets.Interface,
+ options));
+
+ // Test All value
+ Assert.Equal("\"All\"", JsonSerializer.Serialize(AttributeTargets.All, options));
+
+ // Test with overlapping values
+ Assert.Equal("\"All\"", JsonSerializer.Serialize(FlagsWithOverlappingValues.All, options));
+ Assert.Equal("\"One, Four\"", JsonSerializer.Serialize(FlagsWithOverlappingValues.OneAndFour, options));
+
+ // Test One + Two + Four (equals All)
+ Assert.Equal("\"All\"", JsonSerializer.Serialize(
+ FlagsWithOverlappingValues.One | FlagsWithOverlappingValues.Two | FlagsWithOverlappingValues.Four,
+ options));
+ }
}
}