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 @@ +