diff --git a/src/libraries/System.Text.Json/Common/JsonNamingPolicy.cs b/src/libraries/System.Text.Json/Common/JsonNamingPolicy.cs
index ee0a86c2321bc..0ff4784ed8600 100644
--- a/src/libraries/System.Text.Json/Common/JsonNamingPolicy.cs
+++ b/src/libraries/System.Text.Json/Common/JsonNamingPolicy.cs
@@ -23,6 +23,11 @@ protected JsonNamingPolicy() { }
///
public static JsonNamingPolicy CamelCase { get; } = new JsonCamelCaseNamingPolicy();
+ ///
+ /// Returns the naming policy for snake-casing.
+ ///
+ public static JsonNamingPolicy SnakeCase { get; } = new JsonSnakeCaseNamingPolicy();
+
///
/// When overridden in a derived class, converts the specified name according to the policy.
///
diff --git a/src/libraries/System.Text.Json/Common/JsonSnakeCaseNamingPolicy.cs b/src/libraries/System.Text.Json/Common/JsonSnakeCaseNamingPolicy.cs
new file mode 100644
index 0000000000000..a1512a0a894d3
--- /dev/null
+++ b/src/libraries/System.Text.Json/Common/JsonSnakeCaseNamingPolicy.cs
@@ -0,0 +1,118 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace System.Text.Json
+{
+ internal sealed class JsonSnakeCaseNamingPolicy : JsonNamingPolicy
+ {
+ public override string ConvertName(string name)
+ {
+ if (string.IsNullOrEmpty(name))
+ {
+ return name;
+ }
+
+ if (name.Length == 1)
+ {
+ return name.ToLowerInvariant();
+ }
+
+ var result = new ValueStringBuilder(2 * name.Length);
+
+ bool wroteUnderscorePreviously = false;
+ for (int x = 0; x < name.Length; x++)
+ {
+ var current = name[x];
+
+ if (x > 0 && x < name.Length - 1 && char.IsLetter(current))
+ {
+ // text somewhere in the middle of the string
+ var previous = name[x - 1];
+ var next = name[x + 1];
+
+ if (char.IsLetter(previous) && char.IsLetter(next))
+ {
+ // in the middle of a bit of text
+ var previousUpper = char.IsUpper(previous);
+ var currentUpper = char.IsUpper(current);
+ var nextUpper = char.IsUpper(next);
+
+ switch ((previousUpper, currentUpper, nextUpper))
+ {
+ case (false, false, false): // aaa
+ case ( true, true, true): // AAA
+ case ( true, false, false): // Aaa
+ {
+ // same word
+ result.Append(char.ToLowerInvariant(current));
+ wroteUnderscorePreviously = false;
+ break;
+ }
+
+ case (false, false, true): // aaA
+ case ( true, false, true): // AaA
+ {
+ // end of word
+ result.Append(char.ToLowerInvariant(current));
+ result.Append('_');
+ wroteUnderscorePreviously = true;
+ break;
+ }
+
+ case (false, true, true): // aAA
+ case ( true, true, false): // AAa
+ case (false, true, false): // aAa
+ {
+ // beginning of word
+ if (!wroteUnderscorePreviously)
+ {
+ result.Append('_');
+ }
+ result.Append(char.ToLowerInvariant(current));
+ wroteUnderscorePreviously = false;
+ break;
+ }
+ }
+ }
+ else
+ {
+ // beginning or end of text
+ result.Append(char.ToLowerInvariant(current));
+ wroteUnderscorePreviously = false;
+ }
+ }
+ else if (char.IsLetter(current))
+ {
+ // text at the beginning or the end of the string
+ result.Append(char.ToLowerInvariant(current));
+ wroteUnderscorePreviously = false;
+ }
+ else if (char.IsNumber(current))
+ {
+ // a number at any point in the string
+ if (x > 0 && !wroteUnderscorePreviously)
+ {
+ result.Append('_');
+ }
+
+ result.Append(current);
+ wroteUnderscorePreviously = false;
+
+ if (x < name.Length - 1)
+ {
+ result.Append('_');
+ wroteUnderscorePreviously = true;
+ }
+ }
+ else if (!wroteUnderscorePreviously)
+ {
+ // any punctuation at any point in the string
+ result.Append('_');
+ wroteUnderscorePreviously = true;
+ }
+ }
+
+ return result.ToString();
+ }
+ }
+}
diff --git a/src/libraries/System.Text.Json/gen/System.Text.Json.SourceGeneration.csproj b/src/libraries/System.Text.Json/gen/System.Text.Json.SourceGeneration.csproj
index ec46f798ab378..2015aeea56f53 100644
--- a/src/libraries/System.Text.Json/gen/System.Text.Json.SourceGeneration.csproj
+++ b/src/libraries/System.Text.Json/gen/System.Text.Json.SourceGeneration.csproj
@@ -1,5 +1,6 @@
+ true
netstandard2.0
false
enable
@@ -22,6 +23,7 @@
+
@@ -29,6 +31,7 @@
+
diff --git a/src/libraries/System.Text.Json/ref/System.Text.Json.cs b/src/libraries/System.Text.Json/ref/System.Text.Json.cs
index f22feb4a517a7..eaf58865c7ced 100644
--- a/src/libraries/System.Text.Json/ref/System.Text.Json.cs
+++ b/src/libraries/System.Text.Json/ref/System.Text.Json.cs
@@ -156,6 +156,7 @@ public abstract partial class JsonNamingPolicy
{
protected JsonNamingPolicy() { }
public static System.Text.Json.JsonNamingPolicy CamelCase { get { throw null; } }
+ public static System.Text.Json.JsonNamingPolicy SnakeCase { get { throw null; } }
public abstract string ConvertName(string name);
}
public readonly partial struct JsonProperty
diff --git a/src/libraries/System.Text.Json/src/System.Text.Json.csproj b/src/libraries/System.Text.Json/src/System.Text.Json.csproj
index 142d9f8c4fdcb..402179436d34e 100644
--- a/src/libraries/System.Text.Json/src/System.Text.Json.csproj
+++ b/src/libraries/System.Text.Json/src/System.Text.Json.csproj
@@ -19,6 +19,7 @@
+
@@ -26,6 +27,7 @@
+
diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/SnakeCaseUnitTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/SnakeCaseUnitTests.cs
new file mode 100644
index 0000000000000..9e3f2145dd5aa
--- /dev/null
+++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/SnakeCaseUnitTests.cs
@@ -0,0 +1,52 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Xunit;
+
+namespace System.Text.Json.Serialization.Tests
+{
+ public static class SnakeCaseUnitTests
+ {
+ [Fact]
+ public static void ToSnakeCaseTest()
+ {
+ // These test cases were copied from Json.NET.
+ Assert.Equal("url_value", ConvertToSnakeCase("URLValue"));
+ Assert.Equal("url", ConvertToSnakeCase("URL"));
+ Assert.Equal("id", ConvertToSnakeCase("ID"));
+ Assert.Equal("i", ConvertToSnakeCase("I"));
+ Assert.Equal("", ConvertToSnakeCase(""));
+ Assert.Null(ConvertToSnakeCase(null));
+ Assert.Equal("person", ConvertToSnakeCase("Person"));
+ Assert.Equal("i_phone", ConvertToSnakeCase("iPhone"));
+ Assert.Equal("i_phone", ConvertToSnakeCase("IPhone"));
+ Assert.Equal("i_phone", ConvertToSnakeCase("I Phone"));
+ Assert.Equal("i_phone", ConvertToSnakeCase("I Phone"));
+ Assert.Equal("_i_phone", ConvertToSnakeCase(" IPhone"));
+ Assert.Equal("_i_phone_", ConvertToSnakeCase(" IPhone "));
+ Assert.Equal("is_cia", ConvertToSnakeCase("IsCIA"));
+ Assert.Equal("vm_q", ConvertToSnakeCase("VmQ"));
+ Assert.Equal("xml_2_json", ConvertToSnakeCase("Xml2Json"));
+ Assert.Equal("xml_2_net_6_0_json", ConvertToSnakeCase("Xml2Net60Json"));
+ Assert.Equal("sn_ak_ec_as_e", ConvertToSnakeCase("SnAkEcAsE"));
+ Assert.Equal("sn_a_k_ec_as_e", ConvertToSnakeCase("SnA__kEcAsE"));
+ Assert.Equal("sn_a_k_ec_as_e", ConvertToSnakeCase("SnA__ kEcAsE"));
+ Assert.Equal("already_snake_case_", ConvertToSnakeCase("already_snake_case_ "));
+ Assert.Equal("is_json_property", ConvertToSnakeCase("IsJSONProperty"));
+ Assert.Equal("shouting_case", ConvertToSnakeCase("SHOUTING_CASE"));
+ Assert.Equal("hi_this_is_text_time_to_test_", ConvertToSnakeCase("Hi!! This is text. Time to test."));
+ Assert.Equal("building", ConvertToSnakeCase("BUILDING"));
+ Assert.Equal("building_property", ConvertToSnakeCase("BUILDING Property"));
+ Assert.Equal("building_property", ConvertToSnakeCase("Building Property"));
+ Assert.Equal("building_property", ConvertToSnakeCase("BUILDING PROPERTY"));
+ }
+
+ // Use a helper method since the method is not public.
+ private static string ConvertToSnakeCase(string name)
+ {
+ JsonNamingPolicy policy = JsonNamingPolicy.SnakeCase;
+ string value = policy.ConvertName(name);
+ return value;
+ }
+ }
+}
diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/System.Text.Json.Tests.csproj b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/System.Text.Json.Tests.csproj
index ab9c1fbd7a8b7..dcb3aace3bc61 100644
--- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/System.Text.Json.Tests.csproj
+++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/System.Text.Json.Tests.csproj
@@ -153,6 +153,7 @@
+