-
Notifications
You must be signed in to change notification settings - Fork 4.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add JSON snake_case naming policy #54128
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thinking about it more, this condition will likely accidentally trigger if two or more numbers are hit in sequence. In this scenario, what would a user expect the name to be converted to? I know There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For now I've just left this as |
||
{ | ||
result.Append('_'); | ||
wroteUnderscorePreviously = true; | ||
} | ||
} | ||
else if (!wroteUnderscorePreviously) | ||
{ | ||
// any punctuation at any point in the string | ||
result.Append('_'); | ||
wroteUnderscorePreviously = true; | ||
} | ||
} | ||
|
||
return result.ToString(); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure 2 * length is a reasonable heuristic here, but I wasn't sure how much bigger the "average" property name would be, so I felt just doubling was safer.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ABC would turn into a_b_c so the absolute maximum would be (2 * name.Length) - 1
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ABC
should turn intoabc
since the entire string is all the same case; it should be almost the case asID
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
so AbCdEf would be ab_cd_ef which gives us name.Length + (int)Math.Ceiling(name.Length/(double)2) - 1
Edit: Actually it is more complicated than that. There are also single char numbers. I would still say that
(2 * name.Length) - 1
is the absolute maximum in all cases.