Skip to content
This repository has been archived by the owner on Jan 23, 2023. It is now read-only.
/ corefx Public archive

Added snake case naming policy to the JSON serializer #41354

Closed
wants to merge 5 commits into from
Closed

Added snake case naming policy to the JSON serializer #41354

wants to merge 5 commits into from

Conversation

YohDeadfall
Copy link
Contributor

Fixes #39564. The code is written some time ago for our ADO.NET driver for PostgreSQL where snake case is the common and default naming style.

public static class SnakeCaseUnitTests
{
[Fact]
public static void ToSnakeCaseTest()
Copy link
Member

@ahsonkhan ahsonkhan Sep 26, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Based on discussion from the API review, we should add tests for non-ascii text and for text with surrogate pairs (including invalid surrogate pairs) and also validate (at least locally - but I would be ok with checking it in) that the output matches Newtonsoft.Json.

I was working on some testing for CamelCase, so you could try and leverage that as a starting point. Note that for non-ascii characters, we wouldn't match Newtonsoft.Json exactly due to the default escaping policy in S.T.Json (feel free to either override that or loosen the assert condition for the tests).

[Theory]
[InlineData("lowerCase")]
[InlineData("UpperCase")]
[InlineData("UPperCase")]
[InlineData("lower Case")]
[InlineData("loweR Case")]
[InlineData("Upper Case")]
[InlineData("UppeR Case")]
[InlineData("lower case")]
[InlineData("loweR case")]
[InlineData("Upper case")]
[InlineData("UppeR case")]
[InlineData("lower cASe")]
[InlineData("loweR cASe")]
[InlineData("Upper cASe")]
[InlineData("UppeR cASe")]
[InlineData("UPper Case")]
[InlineData("UPpeR Case")]
[InlineData("UPper case")]
[InlineData("UPpeR case")]
[InlineData("UPper cASe")]
[InlineData("UPpeR cASe")]

[InlineData("i")]
[InlineData("I")]
[InlineData("i ")]
[InlineData("I ")]

[InlineData("ii")]
[InlineData("II")]
[InlineData("iI")]
[InlineData("Ii")]
[InlineData("ii ")]
[InlineData("II ")]
[InlineData("iI ")]
[InlineData("Ii ")]

[InlineData("iii")]
[InlineData("III")]
[InlineData("iiI")]
[InlineData("iIi")]
[InlineData("Iii")]
[InlineData("iII")]
[InlineData("IIi")]
[InlineData("IiI")]
[InlineData("iii ")]
[InlineData("III ")]
[InlineData("iiI ")]
[InlineData("iIi ")]
[InlineData("Iii ")]
[InlineData("iII ")]
[InlineData("IIi ")]
[InlineData("IiI ")]

[InlineData("iPhone")]
[InlineData("IPhone")]
[InlineData("IPHone")]
[InlineData("IPH one")]
[InlineData("IPH One")]
[InlineData("IPHONe")]
[InlineData("IPHON e")]
[InlineData("IPHON E")]
[InlineData("IPHONE")]
public static void TestCasing(string baseString)
{
    var testObject = new TestClassWithDictionary
    {
        Dictionary = new Dictionary<string, string>
        {
            { baseString, "hi" }
        }
    };

    string json = JsonSerializer.Serialize<TestClassWithDictionary>(testObject, new JsonSerializerOptions { DictionaryKeyPolicy = JsonNamingPolicy.CamelCase });
    // Assert JSON output is correct

    testObject = JsonSerializer.Deserialize<TestClassWithDictionary>(json);
    json = JsonSerializer.Serialize(testObject);
    // Assert JSON output is correct after round-tripping

    DefaultContractResolver contractResolver = new DefaultContractResolver
    {
        NamingStrategy = new CamelCaseNamingStrategy()
    };

    string expected = JsonConvert.SerializeObject(testObject, new JsonSerializerSettings { ContractResolver = contractResolver });
    // Assert and compare against Newtonsoft.Json

    char[] baseStringArray = baseString.ToCharArray();
    char[] testCharArray = new char[baseStringArray.Length + 2];
    for (int i = 0; i <= baseStringArray.Length; i++)
    {
        for (int j = 0; j < i; j++)
        {
            testCharArray[j] = baseStringArray[j];
        }
        testCharArray[i] = '\uD835';
        testCharArray[i + 1] = '\uDD80';
        //testCharArray[i] = '\uD801';
        //testCharArray[i + 1] = '\uDC27';
        for (int j = i; j < baseStringArray.Length; j++)
        {
            testCharArray[j + 2] = baseStringArray[j];
        }

        string testString = new string(testCharArray);

        testObject = new TestClassWithDictionary
        {
            Dictionary = new Dictionary<string, string>
            {
                { testString, "hi" }
            }
        };

        json = JsonSerializer.Serialize<TestClassWithDictionary>(testObject, new JsonSerializerOptions { DictionaryKeyPolicy = JsonNamingPolicy.CamelCase });
        // Assert JSON output is correct

        testObject = JsonSerializer.Deserialize<TestClassWithDictionary>(json);
        json = JsonSerializer.Serialize(testObject);
        // Assert JSON output is correct after round-tripping

        expected = JsonConvert.SerializeObject(testObject, new JsonSerializerSettings { ContractResolver = contractResolver });
        // Assert and compare against Newtonsoft.Json
    }

    baseStringArray = baseString.ToCharArray();
    testCharArray = new char[baseStringArray.Length + 1];
    for (int i = 0; i <= baseStringArray.Length; i++)
    {
        for (int j = 0; j < i; j++)
        {
            testCharArray[j] = baseStringArray[j];
        }
        testCharArray[i] = '\uD835';
        for (int j = i; j < baseStringArray.Length; j++)
        {
            testCharArray[j + 1] = baseStringArray[j];
        }

        string testString = new string(testCharArray);

        testObject = new TestClassWithDictionary
        {
            Dictionary = new Dictionary<string, string>
            {
                { testString, "hi" }
            }
        };

        json = JsonSerializer.Serialize<TestClassWithDictionary>(testObject, new JsonSerializerOptions { DictionaryKeyPolicy = JsonNamingPolicy.CamelCase });
        // Assert JSON output is correct

        testObject = JsonSerializer.Deserialize<TestClassWithDictionary>(json);
        json = JsonSerializer.Serialize(testObject);
        // Assert JSON output is correct after round-tripping

        expected = JsonConvert.SerializeObject(testObject, new JsonSerializerSettings { ContractResolver = contractResolver });
        // Assert and compare against Newtonsoft.Json
    }
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Newtonsoft.Json had some bad translations (Xml2Json to xml2_json) and the bug I mentioned. Should the behavior be the same or should it be improved a bit? Maybe open an issue in Newtonsoft.Json?

/cc @JamesNK

Copy link
Member

@ahsonkhan ahsonkhan Sep 27, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Newtonsoft.Json had some bad translations (Xml2Json to xml2_json) and the bug I mentioned.

It's fine for those cases for us to deviate, but for other test cases making sure our output matches would be good (or better yet, codify the expected output from Newtonsoft.Json into test cases and verify our output matches against those strings).

Should the behavior be the same or should it be improved a bit?

In cases where the Newtonsoft behavior is buggy/undesirable, we should certainly try to make it better here (possibly in both libraries).

Maybe open an issue in Newtonsoft.Json?

I'd leave the Newtonsoft.Json side of things to @JamesNK, but filing an issue wouldn't hurt :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Already tracked by JamesNK/Newtonsoft.Json#1956.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Based on discussion from the API review, we should add tests for non-ascii text and for text with surrogate pairs (including invalid surrogate pairs) and also validate (at least locally - but I would be ok with checking it in) that the output matches Newtonsoft.Json.

Can you add some more tests for non-ascii text?

@ahsonkhan ahsonkhan added this to the 5.0 milestone Sep 27, 2019
@YohDeadfall
Copy link
Contributor Author

Added two new tests to cover compatibility with Newtonsoft.Json. Everything works well except a some cases due to the bug in Newtonsoft.Json there is some difference in produced output.

Incompatible test cases System.Text.Json Newtonsoft.Json
"II " "ii" "i_i"
"III " "iii" "ii_i"
"IPH One" "iph_one" "ip_h_one"
"IPH one" "iph_one" "ip_h_one"
"IPHON E" "iphon_e" "ipho_n_e"
"IPHON e" "iphon_e" "ipho_n_e"
"iII " "i_ii" "i_i_i"

For me it looks like the current behavior is better, so makes sense to remove failing tests. Are you okay with this, @ahsonkhan?

@ahsonkhan
Copy link
Member

For me it looks like the current behavior is better, so makes sense to remove failing tests. Are you okay with this, @ahsonkhan?

I agree. @JamesNK - what do you think about those cases? I think having a single _ per space character is more appropriate than having multiple (and similar concerns when the last character is a space). I don't know if we should remain 100% compatible with the underscore casing output (even for these cases), and I'd like to hear what compelled you to have the showcased behavior in Json.NET.

@YohDeadfall
Copy link
Contributor Author

/azp run

@azure-pipelines
Copy link

Azure Pipelines successfully started running 4 pipeline(s).

@YohDeadfall
Copy link
Contributor Author

@ahsonkhan There are infrastructure related failures again...

@YohDeadfall
Copy link
Contributor Author

/azp run

@azure-pipelines
Copy link

Azure Pipelines successfully started running 4 pipeline(s).

@YohDeadfall
Copy link
Contributor Author

@ahsonkhan All checks passed.

if (string.IsNullOrEmpty(name))
return name;

StringBuilder builder = new StringBuilder(name.Length + Math.Min(2, name.Length / 5));

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would this benefit from ValueStringBuilder with a stackalloc buffer (alocated with a size cap)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, but in that case ValueStringBuilder should be referenced by the project since the struct isn't public yet (see #28379). If it's okay, I will do it (:

/cc @stephentoub @ahsonkhan

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since ValueStringBuilder is internal (within corelib), I wouldn't take a dependency on it here. I don't think it's worth it in this context, particularly because creating the converted name only happens once and then the result is stored.

Though I believe it is feasible to do, if we really wanted to. For example:

ValueStringBuilder sb = buffer != null ?

The main benefit would be on startup, but we might be able to write this method without using ValueStringBuilder and still be faster. I am heads down on some other work currently, and will review this change soon. Thanks for your patience, @YohDeadfall, appreciate it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it's worth it in this context, particularly because creating the converted name only happens once and then the result is stored

@ahsonkhan, are there circumstances where it might happen for every occurrence? e.g. when happens when serializing a dictionary?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will take a second look and verify, but from what I understand, calls to ConvertName should only ever happen once. @steveharter, @layomia - feel free to chime in about the behavior of serializing dictionary keys/values.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

from what I understand, calls to ConvertName should only ever happen once

That doesn't seem to be the case, e.g.


but maybe I'm just missing where that gets cached. I wouldn't expect it to be cached, though, considering arbitrary dictionaries might get serialized, and their keys could be all over the map.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point @stephentoub. I was looking at PropertyNamingPolicy only, but JsonNamingPolicy applies to DictionaryKeyPolicy as well.

Once the implementation of JsonSnakeCaseNamingPolicy is well tested, we should consider trying to optimize the built-in naming policies. This can be done in a follow-up PR (no point trying to optimize it without a larger test set that ensures correctness). Filed an issue: https://github.com/dotnet/corefx/issues/42581

BenchmarkDotNet=v0.11.5.1159-nightly, OS=Windows 10.0.18362
Intel Core i7-6700 CPU 3.40GHz (Skylake), 1 CPU, 8 logical and 4 physical cores
.NET Core SDK=3.1.100-preview1-014459
  [Host]     : .NET Core 3.0.0 (CoreCLR 4.700.19.46205, CoreFX 4.700.19.46214), X64 RyuJIT
  Job-HPZZRZ : .NET Core 3.0.0 (CoreCLR 4.700.19.46205, CoreFX 4.700.19.46214), X64 RyuJIT

PowerPlanMode=00000000-0000-0000-0000-000000000000  IterationTime=250.0000 ms  MaxIterationCount=10  
MinIterationCount=5  WarmupCount=1  
Method Mean Error StdDev Median Min Max Ratio RatioSD Gen 0 Gen 1 Gen 2 Allocated
Default 2.426 us 0.0473 us 0.0281 us 2.425 us 2.391 us 2.473 us 1.00 0.00 0.1567 - - 656 B
Camel 3.040 us 0.0895 us 0.0592 us 3.027 us 2.979 us 3.150 us 1.26 0.04 0.2491 - - 1048 B
Snake 4.293 us 0.0939 us 0.0621 us 4.280 us 4.195 us 4.397 us 1.77 0.01 0.5624 - - 2432 B
public class SerializeDictionaryWithNamingPolicy
{
    JsonSerializerOptions _camelOptions;
    JsonSerializerOptions _snakeOptions;

    Dictionary<string, string> _foo;

    [GlobalSetup]
    public void Setup()
    {
        _camelOptions = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, DictionaryKeyPolicy = JsonNamingPolicy.CamelCase };
        _snakeOptions = new JsonSerializerOptions { PropertyNamingPolicy = new JsonSnakeCaseNamingPolicy(), DictionaryKeyPolicy = new JsonSnakeCaseNamingPolicy() };

        _foo = new Dictionary<string, string>()
        {
            ["ID"] = "A",
            ["url"] = "A",
            ["URL"] = "A",
            ["THIS IS SPARTA"] = "A",
            ["IsCIA"] = "A",
            ["iPhone"] = "A",
            ["IPhone"] = "A",
            ["xml2json"] = "A",
            ["already_snake_case"] = "A",
            ["IsJSONProperty"] = "A",
            ["ABCDEFGHIJKLMNOP"] = "A",
            ["Hi!! This is text.Time to test."] = "A",
        };
    }

    [BenchmarkCategory(Categories.CoreFX, Categories.JSON)]
    [Benchmark(Baseline = true)]
    public string Default()
    {
        return JsonSerializer.Serialize(_foo);
    }

    [BenchmarkCategory(Categories.CoreFX, Categories.JSON)]
    [Benchmark]
    public string Camel()
    {
        return JsonSerializer.Serialize(_foo, _camelOptions);
    }

    [BenchmarkCategory(Categories.CoreFX, Categories.JSON)]
    [Benchmark]
    public string Snake()
    {
        return JsonSerializer.Serialize(_foo, _snakeOptions);
    }
}

@maryamariyan
Copy link
Member

maryamariyan commented Nov 6, 2019

Thank you for your contribution. As announced in dotnet/coreclr#27549 this repository will be moving to dotnet/runtime on November 13. If you would like to continue working on this PR after this date, the easiest way to move the change to dotnet/runtime is:

  1. In your corefx repository clone, create patch by running git format-patch origin
  2. In your runtime repository clone, apply the patch by running git apply --directory src/libraries <path to the patch created in step 1>

[InlineData("u_ppe_r_case", "UPpeR Case")]
[InlineData("u_ppe_r_c_a_se", "UPpeR cASe")]
//
[InlineData("ä", "ä")]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-ascii tests like these should be written using escaped/hex characters.

When including non-ASCII characters in the source code use Unicode escape sequences (\uXXXX) instead of literal characters. Literal non-ASCII characters occasionally get garbled by a tool or editor.

See:

[InlineData("{\r\n\"is\\r\\nAct\u6F22\u5B57ive\": false \"in\u6F22\u5B57valid\"\r\n}", 30, 30, 1, 28)]

So, for this case (same for the special "42" below):

Suggested change
[InlineData("ä", "ä")]
[InlineData("\u00E4", "\u00E4")]

}
break;

case UnicodeCategory.Surrogate:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider adding tests for invalid surrogate characters too (which is possible by doing a substring in between a surrogate pair of characters or crafting one using chars).
So:

  • a string containing a high-surrogate without a low-surrogate following it
  • a string containing a low-surrogate first before a high-surrogate

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Already have one of them. It's 42.

Copy link
Member

@ahsonkhan ahsonkhan Nov 14, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The special 42 contains valid surrogate pairs (https://www.fileformat.info/info/unicode/char/1d7dc/index.htm).

UTF-16 (hex) 0xD835 0xDFDC (d835dfdc)

I am talking about a string that is invalid (i.e. doesn't contain the correct pair of surrogate characters).

For example:

[InlineData("\"hello\"", new char[1] { (char)0xDC01 })] // low surrogate - invalid
[InlineData("\"hello\"", new char[1] { (char)0xD801 })] // high surrogate - missing pair

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missed word "invalid", sorry. Will do (:

@YohDeadfall
Copy link
Contributor Author

Agreed with you, but should we follow the rule that adds an underscore after numbers? If so Xml2Json will be translated as xml2_json instead of xml2json.

@khellang
Copy link
Member

should we follow the rule that adds an underscore after numbers?

I'd say either xml_2_json or xml2json, but not xml2_json.

@YohDeadfall
Copy link
Contributor Author

@ygoe shown an example which doesn't look well without underscores (En13757Index is translated as en13757index), so prefixing numbers sounds good to me.

@khellang
Copy link
Member

I think @ahsonkhan's observations from the two linked implementations in Python and Node makes sense:

Numbers are only treated as separate words if followed by capital letter

@YohDeadfall
Copy link
Contributor Author

The change-case package splits the passed string into words and then adds a separator between words. Therefore, it collapses multiple underscores into one and ignores leading/trailing spaces.

Here is a map of characters which should appear in a resulting string using the package:

Unicode category Should be in resulting string
Cc (other, control) No
Cf (other, format) No
Cn (other, not assigned) No
Co (other, private use) No
Cs (other, surrogate) No
Ll (letter, lowercase) Yes, see exceptions
Lm (letter, modifier) Yes, all
Lo (letter, other) Yes, see exceptions
Lt (letter, titlecase) Yes, all
Lu (letter, uppercase) Yes, see exceptions
Mc (mark, spacing combining) Yes, see exceptions
Me (mark, enclosing) No
Mn (mark, nonspacing) No
Nd (number, decimal digit) Yes, all
Nl (number, letter) Yes, all
No (number, other) Yes, see exceptions
Pc (punctuation, connector) No
Pd (punctuation, dash) No
Pe (punctuation, close) No
Pf (punctuation, final quote) No
Pi (punctuation, initial quote) No
Po (punctuation, other) No
Ps (punctuation, open) No
Sc (symbol, currency) No
Sk (symbol, modifier) No
Sm (symbol, math) No
So (symbol, other) No
Zl (separator, line) No
Zp (separator, paragraph) No
Zs (separator, space) No

Included into an output

Unicode category Characters
Mc (mark, spacing combining) U+1885, U+1886

Excluded from an output

Unicode category Characters
Ll (letter, lowercase) U+0560, U+0588, U+1C80 - U+A7AF, U+A7B9
Lo (letter, other) U+05EF, U+0860, U+0861 - U+08B6, U+08B7 - U+09FC, U+0C80, U+0D54, U+0D55 - U+1878, U+312E - U+9FD6, U+9FD7 - U+A8FE
Lu (letter, uppercase) U+1C90 - U+1CBD, U+1CBE - U+A7AE, U+A7B8
No (number, other) U+0D58 - U+0D5E, U+0D76 - U+0D78

Questions

Should surrogates be included?

Pros: Compatibility with the existing implementations. Lowers collision probability
Cons: Undetermined.

Should listed above characters be excluded or included?

Some of them ansuvara (randomly checked) so there is no reason to exclude this symbols since they behave like variations, but phonetic. To make the final decision each character should be checked.

Should all excluded letters be replaced by an underscore?

Pros: Lowers collision probability.
Cons: The result would look uglier in the case of many replacing characters.

Should leading/trailing replacing characters be ignored?

Pros: Compatibility with the existing implementations.
Cons: Lowers collision probability.

Should multiple underscores be collapsed?

Pros: Compatibility with the existing implementations only when letters get replaced. Underscores in the original string should be preserved.
Cons: Lowers collision probability.

Should an underscore be added after a number?

Pros: Compatibility with the existing implementations.
Cons: Some edge cases like xml2json will be properly handled.

@YohDeadfall
Copy link
Contributor Author

@ahsonkhan To make things simple I researched the npm package and added the table you can see above. Let's analyze it and answer the questions first so we won't spent time on adding test and changing the behavior multiple times. After that I will write a final and optimized version of the policy.

@khellang
Copy link
Member

khellang commented Nov 18, 2019

Question: How does all of this compare to the existing behavior of camelCase naming?

It would certainly be interesting to use the same word-splitting behavior for all naming conventions.
That would make it really easy to add new conventions, as it would only be a matter of changing casing and adding separators between words:

Name First Rest Separator
PascalCase Capitalize Capitalize None
camelCase LowerCase Capitalize None
snake_case LowerCase LowerCase _
SCREAMING_SNAKE_CASE UpperCase UpperCase _
camel_Snake_Case LowerCase Capitalize _
Pascal_Snake_Case Capitalize Capitalize _
kebab-case LowerCase LowerCase -
SCREAMING-KEBAB-CASE UpperCase UpperCase -

@YohDeadfall
Copy link
Contributor Author

This is how the npm works, but the camel case policy (from the new JSON serializer) just lowers the first characters and does nothing more due to performance reasons. So an user controls all characters except the first one if it's a letter. Otherwise, the user controls all of them.

The snake case policy isn't limited to the first, so it's a problem.

@khellang
Copy link
Member

khellang commented Nov 18, 2019

This is how the npm works

This is what camel-snake-kebab in Clojure does as well. It's very consistent.

the camel case policy (from the new JSON serializer) just lowers the first characters and does nothing more due to performance reasons.

How is this performance critical? Is the result of the transformation not cached?

Anyway, I guess the ship has sailed for camelCase as it would be a major breaking change to switch behavior now, but for this and (the inevitable ask for) future conventions, it would be nice if they used the same strategy. I guess this could be refactored out if it becomes relevant later.

@stephentoub
Copy link
Member

Is the result of the transformation not cached?

#41354 (comment)

@YohDeadfall
Copy link
Contributor Author

The serializer caches only property and class names, but not dictionary keys. Having a pool would help only if it supports LRU strategy.

@YohDeadfall
Copy link
Contributor Author

Anyway, I guess the ship has sailed for camelCase as it would be a major breaking change to switch behavior now

This is major problem, but as I know the serializer isn't very popular due to limitations, a lot of them. So if a breakage would be introduced, it won't affect to many people. Taking into account that any policy is mostly used for members and classes, the change won't hurt so much, but will give a positive impact.

@maryamariyan
Copy link
Member

Thank you for your contribution. As announced in #27549 the dotnet/runtime repository will be used going forward for changes to this code base. Closing this PR as no more changes will be accepted into master for this repository. If you’d like to continue working on this change please move it to dotnet/runtime.

actual.SomeIntProperty);
}

private class NamingPolictyTestClass
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typo NamingPolictyTestClass -> NamingPolicyTestClass

Comment on lines +126 to +128
Assert.Equal(
expected.SomeIntProperty,
actual.SomeIntProperty);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is short enough to be one line, no?

[Fact]
public static void SerializeDictionary_RoundTipping_MatchesOriginal()
{
var options = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.SnakeCase };
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be DictionaryKeyPolicy = JsonNamingPolicy.SnakeCase

var options = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.SnakeCase };

string expected = @"{""some_int_property"":42}";
string actual = JsonSerializer.Serialize(JsonSerializer.Deserialize<Dictionary<string, int>>(expected, options), options);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DictionaryKeyPolicy is only applied on serialization. The way to test this is to have a dictionary with keys that are some other casing like Pascal case, and make sure the serialized content is snake case.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area-System.Text.Json * NO MERGE * The PR is not ready for merge yet (see discussion for detailed reasons) post-consolidation PRs which will be hand ported to dotnet/runtime
Projects
None yet
Development

Successfully merging this pull request may close these issues.