Skip to content
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

JSON properties get camelCased on serialization #1719

Closed
nlaslett opened this issue Jan 24, 2022 · 6 comments
Closed

JSON properties get camelCased on serialization #1719

nlaslett opened this issue Jan 24, 2022 · 6 comments

Comments

@nlaslett
Copy link

When adding a Json body to a request using request.AddJsonBody(), it seems that properties are converted to camelCase on serialization. This is happening on both the 106.x and 107.x branches. On the 106.x branch, it happens with both SimpleJson and Newtonsoft.Json sterilizers. With Newtonsoft, [JsonProperty("Name")] attributes are correctly read, but the camelCasing takes place after that.

The rules seem to be: if the first character is uppercase, downcase that character and all subsequent uppercase character, and stop when you come to a uppercase character followed by a lowercase or non-word character. For example: XXXProfile --> xxxProfile

When using client.UseNewtonsoftJson(), this casing change happens even though calling the serializer directly ex: Newtonsoft.Json.JsonConvert.SerializeObject(obj) produces the correct results.

To Reproduce

public class MyClass
{
    [JsonProperty("TESTOne")]
    public string TestOne { get; set; } = "abc123";

    [JsonProperty("test_two")]
    public string TestTwo { get; set; } = "abc123";

    [JsonProperty("TEST_THREE")]
    public string TestThree { get; set; } = "abc123";

    [JsonProperty("myTestFour")]
    public string TestFour { get; set; } = "abc123";

    [JsonProperty("TestFive")]
    public string TestFive { get; set; } = "abc123";

    public string TESTSIX { get; set; } = "abc123";
}

With RestSharp 107.1.1 and client.UseNewtonsoftJson(), this is what is serialized and sent:

{
    "testOne": "abc123",
    "test_two": "abc123",
    "tesT_THREE": "abc123",
    "myTestFour": "abc123",
    "testFive": "abc123",
    "testsix": "abc123"
}

With RestSharp 107.1.1 and NOT explicitly calling UseNewtonsoftJson(), we get this:

{
    "testOne": "abc123",
    "testTwo": "abc123",
    "testThree": "abc123",
    "testFour": "abc123",
    "testFive": "abc123",
    "testsix": "abc123"
}

These are the results from calling Newtonsoft.Json.JsonConvert.SerializeObject(obj):

{
    "TESTOne": "abc123",
    "test_two": "abc123",
    "TEST_THREE": "abc123",
    "myTestFour": "abc123",
    "TestFive": "abc123",
    "TESTSIX": "abc123"
}

Testing is a little harder in 107.x since there is no longer a request.Body property with the post-serialization text. I am getting the serialization by capturing actual traffic with Fiddler.

Expected behavior
Objects are serialized exactly as defined in the [JsonProperty], or lacking that, the property name.

Desktop (please complete the following information):

  • OS: Windows 10 (20H2)
  • .NET 4.8 Framework
  • Versions 107.1.1 & 106.13.0
@nlaslett nlaslett added the bug label Jan 24, 2022
@alexeyzimarev
Copy link
Member

alexeyzimarev commented Jan 24, 2022

You need to override the serializer options like

client.UseSystemTextJson(new JsonSerialializerOptions(JsonSerializerDefaults.General));

It's mentioned in the docs, and for Newtonsoft.Json defaults are directly listed, so you should understand what to change if you need to.

RestSharp 107 doesn't have any wicked rules, it just calls the serializer. The fact that v107 uses System.Text.Json serializer by default is documented.

@alexeyzimarev alexeyzimarev removed the bug label Jan 24, 2022
@nlaslett
Copy link
Author

@alexeyzimarev Thanks for the quick reply. I'm a bit confused, on several points:

  1. Directly calling the Newtonsoft serializer with default options yields correct results. (see above)
  2. The Camel Casing settings are buried deep in the options (https://www.newtonsoft.com/json/help/html/NamingStrategyCamelCase.htm) and are not the default NamingStrategy. I am not changing that; I'm just doing very basic RestSharp calls. Something in RestSharp seems to be setting this. I have working code below that restores the DefaultNamingStrategy.
  3. Digging further, I realized that while this happens with both System.Text.Json (107.x) and Newtonsoft serializers, it does not happen with SimpleJson (106.x). Yes, I understand that System.Text.Json is the new default, which is why I tested it both ways.
  4. Are you saying that calling client.UseNewtonsoftJson() is not the correct way to use Newtonsoft.Json, even with the defaults?
  5. The JsonSerializerOptions class does not seem to exist in .NET 4.8

Have things always behaved this way? I've used RestSharp in just about all of my projects for many years and have never come across this, but maybe I just never had to deal with a case-sensitive API before? (I'm talking to Okta, FWIW.)

Here is some working code that restores what I would consider 'classic' RestSharp / Newtonsoft behavior:

client.UseNewtonsoftJson(new Newtonsoft.Json.JsonSerializerSettings
{
    ContractResolver = new DefaultContractResolver
    {
        NamingStrategy = new DefaultNamingStrategy()
    }
});

This was tested with RestSharp 107.1.1 and Newtonsoft.Json 13.0.1 running under .NET 4.8.

If nothing has changed on your end, maybe something changed in Newtonsoft and System.Text.Json? The DefaultContractResolver does not have a default NamingStrategy (nullable). What could be setting it to new CamelCaseNamingStrategy() as opposed to new DefaultNamingStrategy()?

@alexeyzimarev
Copy link
Member

alexeyzimarev commented Jan 25, 2022

Don't get me wrong, but I don't think you have read the docs following the link I provided.

The default serializer for RestSharp is using JsonSerializer from System.Text.Json. It is right there in the docs. It uses Web defaults because, well, RestSharp is for the web, and 80% of the APIs out there use camel case, some use snake case, but I almost never see pascal case. I provided a snippet on how to change it to General, which should give you the pascal case.

I posted a direct link to Newtonsoft.Json details for RestSharp.Serializers.NewtonsoftJson. It shows the RestSharp defaults:

JsonSerializerSettings DefaultSettings = new JsonSerializerSettings {
    ContractResolver     = new CamelCasePropertyNamesContractResolver(),
    DefaultValueHandling = DefaultValueHandling.Include,
    TypeNameHandling     = TypeNameHandling.None,
    NullValueHandling    = NullValueHandling.Ignore,
    Formatting           = Formatting.None,
    ConstructorHandling  = ConstructorHandling.AllowNonPublicDefaultConstructor
};

It also says:

If you need to use different settings, you can supply your instance of JsonSerializerSettings as a parameter for the extension method.

Explicitly setting the ContractResolver to default one will result in property names being serialized without changing the casing.

As you can see there, the RestSharp default is camel case. As NewtonsoftJson itself is properly documented, it should not be an issue to reconfigure it in a way you want.

Both System.Text.Json and Newtonsoft.Json serializer exist for almost two years with exactly these defaults, there was no change at all during the v107 release. The only thing that really changed is that SimpleJson was removed for good.

@nlaslett
Copy link
Author

Ah, I see the RestSharp default ContractResolver now. Sorry, I somehow overlooked that. And looking at the GIT history, it's been there for at least two years. My bad.

What's the thinking on forcing an explicit casing on outbound calls? I would think it would be better to use attributes like [JsonProperty("Name")] and trust the user to provide correct property names. Changing case mid-flight seems like black-box programming and can lead to unexpected errors when dealing with case-sensitive endpoints.

I'm not looking for any kind of standard casing, camel or otherwise. Okta uses camel on default properties but custom properties can be anything, and they have to match.

I can now initialize Newtonsoft with DefaultContractResolver and DefaultNamingStrategy. Thanks for the pointers. I'm just questing why RestSharp serialization defaults specify CamelCasePropertyNamesContractResolver instead.

I would think that calling Newtonsoft.Json.JsonConvert.SerializeObject(obj) would generate the same serialization, with the same casing and defaults, as the default RestSharp serialization, but it doesn't.

PS, thanks for all your hard work on this project! I don't mean to come across as unappreciative.

@alexeyzimarev
Copy link
Member

I would think that calling Newtonsoft.Json.JsonConvert.SerializeObject(obj) would generate the same serialization, with the same casing and defaults, as the default RestSharp serialization, but it doesn't.

Well, it's because Newtonsoft.Json is not the default serializer.

The decision to choose the camel case is, again, because most JSON APIs today use camel case as it is a de-facto standard for JavaScript. That's why the JsonSerializerDefaults.Web convention for System.Text.Json serializer is using camel case. Like I wrote, as RestSharp main operation space is web, is makes sense to have camel case as a default. In addition, this is the first time I get an issue about casing in serialization, so it would confirm my bias and support that decision. I am sorry if it caused you trouble, at the same time it's very easy to fix by adding a couple of lines of code when configuring the serializer.

Concerning JsonProperty attribute, not many people like to annotate their models when using the de-facto convention (camel case in this context), as it mostly serves as an "override". Once again, I was seeking a "good default", and I still think it is a good default, which is completely customizable.

@nlaslett
Copy link
Author

Got it. Thanks for your time and attention!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants