aspnet / Mvc

[Archived] ASP.NET Core MVC is a model view controller framework for building dynamic web sites with clean separation of concerns, including the merged MVC, Web API, and Web Pages w/ Razor. Project moved to https://github.com/aspnet/AspNetCore
Apache License 2.0
5.62k stars 2.14k forks source link

[Discussion] MVC now serializes JSON with camel case names by default #4842

Closed dougbu closed 7 years ago

dougbu commented 8 years ago

In previous milestones, MVC's JSON serialization used Json.NET's default naming convention. This maintained C# property names in the JSON.

In 1.0.0, MVC uses camel case names by default. This matches most JSON naming conventions.

Potential compatibility breaks

Applications which depend on the exact bytes sent over the wire or that include code such as

dynamic d = JObject.Parse(body);

may need to be adjusted.

To restore previous naming strategy

If you have case-sensitive clients that cannot be easily updated, change your Startup from

    services.AddMvc();

to

    services
        .AddMvc()
        .AddJsonOptions(options => options.SerializerSettings.ContractResolver = new DefaultContractResolver());
Example
Before
public class Person
{
    public int Id { get; set; }

    public string FullName { get; set; }
}

Would serialize to

{"Id":9000,"FullName":"John Smith"}
After

The same model will serialize to

{"id":9000,"fullName":"John Smith"}

Note the initial lowercase letters.

rynowak commented 8 years ago

@NeelBhatt - I read through your blog, thanks!


camelCase is the default - services.AddMvc() will give you camelCase

If you want the old PascalCase behavior then do

 services
        .AddMvc()
        .AddJsonOptions(options => options.SerializerSettings.ContractResolver = new DefaultContractResolver());

Your blog post seems to give the impression that it's the other way around

NeelBhatt commented 8 years ago

@rynowak Thanks for looking into it and for the feedback.

I have updated my blog accordingly.

bwatts commented 8 years ago

I just encountered an interesting scenario with camel-cased names and wanted to ensure we account for it.

The default serialization for a Dictionary<,> is to a JSON object:

{ "foo": "...", "bar": "..." }

However, if we serialize an upper-cased key Foo, it will become lower-cased as a property, then deserialize back with the lower-cased name, creating a lossy conversion.

We worked around this by creating a DictionaryConverter that treats dictionaries as List<KeyValuePair<K, V>>:

[
    { "key": "Foo", "value": "..." },
    { "key": "bar", "value": "..." }
]

We wanted to simply tell JSON.NET to use JsonArrayContract for dictionaries, but had to account for existing data that uses the object-not-array format.

MaximRouiller commented 8 years ago

@bwatts did you open an issue on https://github.com/JamesNK/Newtonsoft.Json?

If it's a bug, @JamesNK should be informed. I've looked quickly at the repository and it doesn't look like this issue has been logged before.

bwatts commented 8 years ago

@MaximRouiller I wasn't sure if it is a bug or just an unfortunate feature collision. I'll open an issue over there anyhow to be sure.

khellang commented 8 years ago

@bwatts @MaximRouiller Camel-case dictionary keys is something that needs to be opted into. If you want to tweak the settings, you can set the JsonOptions:

services.AddMvc().AddJsonOptions(x =>
{
    x.SerializerSettings.ContractResolver = new DefaultContractResolver
    {
        NamingStrategy = new CamelCaseNamingStrategy
        {
            ProcessDictionaryKeys = false // this is the default.
        }
    };
});

As you can see, the MVC defaults are the same as Newtonsoft.Json defaults, which should keep dictionary keys the same.

MaximRouiller commented 8 years ago

Thanks @khellang for the clarification! Didn't know about that specific feature.

bwatts commented 8 years ago

@khellang I found NamingStrategy and ProcessDictionaryKeys while investigating, but this fiddle seems to contradict that default. Thoughts?

khellang commented 8 years ago

@bwatts Uuh. CamelCasePropertyNamesContractResolver isn't the same as DefaultContractResolver :wink:

This fiddle shows that it works :smile:

kijanawoodard commented 8 years ago

I'm using this: http://stackoverflow.com/a/24226442/214073. Not sure if it's still required, but works.

bwatts commented 8 years ago

@khellang isn't the point of the MVC default-to-camel-casing to make the CamelCasePropertyNamesContractResolver the default?

khellang commented 8 years ago

@bwatts No, because as you've discovered, CamelCasePropertyNamesContractResolver also makes dictionary keys camel-cased. That's whay @JamesNK introduced the NamingStrategy class.

MVC uses the DefaultContractResolver with NamingStrategy set to CamelCaseNamingStrategy, which has ProcessDictionaryKeys set to false by default.

bwatts commented 8 years ago

@khellang got it - so it seems I shouldn't be using CamelCasePropertyNamesContractResolver at all then. I didn't realize.

gr3ysky commented 7 years ago

This change is really ridicilious. You need to tell mvc to use default serializer to get the default behavior. This is why microsoft is not liked by many.

kijanawoodard commented 7 years ago

@gr3ysky The problem is "the default for what scenario"?

It's all a matter of choices. At least now, we know what the choices are more or less when they happen and we have a way to tailor things to our liking.

gr3ysky commented 7 years ago

The default is the f.cking default. No need to argue on what is default. It shouldnt change the property names. It is just like start menu issue. I have been developing mvc over six years. It was pascal case till now. And just check the code to get old behaviour. And thats silly to say use the default behavior. Right?

fhelwanger commented 7 years ago

@gr3ysky Did you read https://github.com/aspnet/Mvc/issues/4842#issuecomment-228834315?

kijanawoodard commented 7 years ago

Further, you're arguing defaults should never change? This is a major version. First thing I do on every project is change the "default" so I get "proper serialization" for javascript.

What's "right" is a judgement call, not an objective fact.

MaximRouiller commented 7 years ago

The new "Default" for serialization in JSON is now lower case.

The model binders for ASP.NET Core will interpret lower-case/upper-case without any issues so this is a non-issue for "current behavior" of the API.

So let's take a look at what scenarios you would need to change it.

  1. New Project
  2. Porting to .NET Core

Here are the solutions:

  1. Either the invoking client adapts to the API it invokes and parse the data properly or you adapt to the client and do a one line change of code to suit the invoker. Either way, minor change.
  2. You are porting an API from .NET Framework to .NET Core. So between the whole HttpApplication going away, the application lifecycle being modular, project.json, WebAPI/MVC being merged, Kestrel as a running host, EF Core != EF6, lack of Azure integrations, dependency injections as a first class citizen, no oData support yet, missing libraries that haven't converted to Core... and what is a deal breaker is ... default serialization for JSON.

It's a major rewrite of a framework. You can't expect to copy/paste your code and expect everything to be feature equal. I have more complaints about EF Core right now than the default serialization for JSON which can be reverted in one line of code. As mentioned before, there's only Microsoft that defaults the JSON serialization to Pascal Case. Now we are "back to normal" in the web sphere.

If you want to go back to the good old days when you are porting an application, here's how:

services.AddMvc().AddJsonOptions(options =>
            {
                options.SerializerSettings.ContractResolver = new DefaultContractResolver();
            });
Eilon commented 7 years ago

We are closing this issue because no further action is planned for this issue. If you still have any issues or questions, please log a new issue with any additional details that you have.

Mike-E-angelo commented 7 years ago

If I could, I wanted to thank everyone for the respectful and thoughtful discussion on this memorable thread, especially when it turns out I had no idea what I was talking about. 😛 (or to be more accurate, did not completely understand the issue at hand and misunderstood its actual implications).

I'd also like to send a special shout out to @bwatts for his "no good deed" comment above as well. I didn't really understand it at the time, but there have been been numerous instances since then in other discussions throughout GitHub where I have been brought to bear the full appreciation of that statement, LOL. If it helps at all, rest assured that I have gotten my due on the flip side. 😆

Anyways, thought I would share. And no, I can't help it. 👍

maxisam commented 6 years ago

It is interesting that if an object is not strong type like object or dynamic, it will not be converted to camelcase. But it can be fixed by

     .AddJsonOptions(opts =>
      {
            opts.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
      });
khellang commented 6 years ago

Or by using the DefaultContractResolver and setting ProcessDictionaryKeys = true (which I think is recommended):

.AddJsonFormatters(json =>
{
    json.ContractResolver = new DefaultContractResolver
    {
        NamingStrategy = new CamelCaseNamingStrategy
        {
            ProcessDictionaryKeys = true
        }
    };
});
QuAzI commented 5 years ago

Wow! I found this ugly bug! And it is a really bug! This behavior broke the data exchange between C# and JS! 1) All properties in JS must be fixed after migration from ASP.NET 2) Doesn't have backward compatibility (to restore broken case) and can't be returned AS IS to the ASP.NET Controller

public class TestModel
{
  public int Id { get; set;}
  public Dictionary<string, string> Settings { get; set; }
}

Simple method to use model

<script>
    var model = @Html.Raw(Json.Serialize(Model)); // case is broken there when ASP.NET Core used

    // some methods to bind inputs with model

    $('form').submit(function (e) {
        e.preventDefault();
        $.ajax({
            url: this.action,
            data: JSON.stringify(model),
            type: "POST",
            beforeSend: function (xhr) {
                xhr.setRequestHeader("Content-type", "application/json");
            },
            async: false
        });
    });
</script>

Simple method which must save returned model

[HttpPost]
public ActionResult YmlConfiguration([FromBody]TestModel config)
{
    // config = null and NullReferenceException fired there because case broken!
}

It is improbable to change names at serialization! Why?

MaximRouiller commented 5 years ago

@QuAzI If you want to restore the older behavior of keeping the casing, see the comments above. They show you many ways to resolve that issue.