2

I have an issue with the default serialization CamelCasing behavior of .Net Core and was hoping to see if someone else faced the same issue and what work around they used.

Property Names like FOO12 or FOO1 are incorrectly serialized to something like

foO12 or foO1

When infact they should probably be done as foo12 or foo1.

I have used a workaround of adding the following Attribute but was hoping somebody might have a better answer to this issue:

[JsonProperty(PropertyName = "foo12")]

TechLover
  • 108
  • 8
  • If you don't like Json.NET's implementation of camel casing, you can always define your own replacement. [How to apply a general rule for remapping all property names when serializing with Json.NET?](https://stackoverflow.com/q/46476903/3744182) is one place to start. – dbc Sep 17 '18 at 19:41

1 Answers1

5

Json.NET's CamelCasePropertyNamesContractResolver uses a CamelCaseNamingStrategy to convert the property names to camelcase. Internally it uses StringUtils.ToCamelCase which doesn't convert a character to lowercase in case it is followed by a number, see link.

CamelCaseNamingStrategy

public class CamelCaseNamingStrategy : NamingStrategy
{
    // ...

    protected override string ResolvePropertyName(string name)
    {
        return StringUtils.ToCamelCase(name);
    }
}

StringUtils

Notice the 2nd if statement, where there's no check for a number.

internal static class StringUtils
{
    public static string ToCamelCase(string s)
    {
        if (!string.IsNullOrEmpty(s) && char.IsUpper(s[0]))
        {
            char[] array = s.ToCharArray();
            for (int i = 0; i < array.Length && (i != 1 || char.IsUpper(array[i])); i++)
            {
                bool flag = i + 1 < array.Length;
                if ((i > 0 & flag) && !char.IsUpper(array[i + 1])) // << Missing check for a number.
                {
                    break;
                }
                char c = char.ToLower(array[i], CultureInfo.InvariantCulture);
                array[i] = c;
            }
            return new string(array);
        }
        return s;
    }
}

You can implement a custom NamingStrategy to implement this missing check as shown below.

class CustomCamelCaseNamingStrategy : CamelCaseNamingStrategy
{
    protected override String ResolvePropertyName(String propertyName)
    {
        return this.toCamelCase(propertyName);
    }

    private string toCamelCase(string s)
    {
        if (!string.IsNullOrEmpty(s) && char.IsUpper(s[0]))
        {
            char[] array = s.ToCharArray();
            for (int i = 0; i < array.Length && (i != 1 || char.IsUpper(array[i])); i++)
            {
                bool flag = i + 1 < array.Length;
                if ((i > 0 & flag) && !char.IsUpper(array[i + 1]) && !char.IsNumber(array[i + 1]))
                {
                    break;
                }
                char c = char.ToLower(array[i], CultureInfo.InvariantCulture);
                array[i] = c;
            }
            return new string(array);
        }
        return s;
    }
}

In ConfigureServices you assign this custom NamingStrategy to the CamelCasePropertyNamesContractResolver.
There's no need to implement a full custom ContractResolver.
(When using the default CamelCaseNamingStrategy, the CamelCasePropertyNamesContractResolver sets the properties ProcessDictionaryKeys and OverrideSpecifiedNames to True, so we keep this behaviour.)

services
    .AddMvc()
    .AddJsonOptions(options => 
        options.SerializerSettings.ContractResolver = 
            new CamelCasePropertyNamesContractResolver() { 
                NamingStrategy = new CustomCamelCaseNamingStrategy() { 
                ProcessDictionaryKeys = true,
                OverrideSpecifiedNames = true 
        }});
pfx
  • 20,323
  • 43
  • 37
  • 57
  • Oh, don't change the `NamingStrategy` on `CamelCasePropertyNamesContractResolver`, it shares contract information globally across all instances. See [Json.Net: Html Helper Method not regenerating](https://stackoverflow.com/a/30743234/3744182) for details. Instead just replace the `NamingStrategy` on `DefaultContractResolver`. – dbc Sep 17 '18 at 21:36
  • @dbc Might not be an issue if the whole app requires this kind of serialization. I do understand it for the case you point to, as some properties are being excluded from serialization which is not desired globally in that question. – pfx Sep 17 '18 at 21:46
  • You can get burned if some other part of the app had previously serialized some data using an unmodified `CamelCasePropertyNamesContractResolver`. In that case, the `CustomCamelCaseNamingStrategy` set later will apparently get ignored. – dbc Sep 17 '18 at 21:51
  • @dbc OK, thanks, I wasn't aware of that. I'll leave it to OP to decide whether to go for a full custom `ContractResolver` or not. The clue to the serialization remains in the custom `NamingStrategy`. But thanks, I learned something new! – pfx Sep 17 '18 at 22:02
  • You don't need to subclass `DefaultContractResolver`. You can just set a custom naming strategy on a regular old `DefaultContractResolver`, e.g. as is shown in [`JsonSerializerSettingsProvider`](https://github.com/aspnet/Mvc/blob/master/src/Microsoft.AspNetCore.Mvc.Formatters.Json/JsonSerializerSettingsProvider.cs). Actually, after naming strategies were split out from the contract resolver in Json.NET 9.0.1, `CamelCasePropertyNamesContractResolver` is mostly redundant. – dbc Sep 17 '18 at 22:06
  • Awesome description and right on point..Thank you !! I will have to discuss this with the Team to decide which way to go..but now we have options ! :) – TechLover Sep 20 '18 at 20:40