35

I managed to get it working using the following code:

.AddNewtonsoftJson(options => {
    options.SerializerSettings.ContractResolver = new DefaultContractResolver
    {
        NamingStrategy = new SnakeCaseNamingStrategy()
    };
});

However this makes MVC use Newtonsoft rather than System.Text.JSON which is faster, async and built in.

Looking at the naming policy options in System.Text.JSON I could only find CamelCase. Is there any native support for snake case? What is a better way of achieving snake case JSON naming style?

dbc
  • 104,963
  • 20
  • 228
  • 340
numberjak
  • 1,065
  • 4
  • 13
  • 28

7 Answers7

39

Just slight modification in pfx code to remove the dependency on Newtonsoft Json.Net.

String extension method to convert the given string to SnakeCase.

public static class StringUtils
{
    public static string ToSnakeCase(this string str)
    {
        return string.Concat(str.Select((x, i) => i > 0 && char.IsUpper(x) ? "_" + x.ToString() : x.ToString())).ToLower();
    }
}

Then in our SnakeCaseNamingPolicy we can do

public class SnakeCaseNamingPolicy : JsonNamingPolicy
{
    public static SnakeCaseNamingPolicy Instance { get; } = new SnakeCaseNamingPolicy();

    public override string ConvertName(string name)
    {
        // Conversion to other naming convention goes here. Like SnakeCase, KebabCase etc.
        return name.ToSnakeCase();
    }
}

The last step is to register our naming policy in Startup.cs

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers()            
        .AddJsonOptions(
            options => { 
                options.JsonSerializerOptions.PropertyNamingPolicy = 
                    SnakeCaseNamingPolicy.Instance;
            });
}

Using the model:

public class WeatherForecast
{
    public DateTime Date { get; set; }

    public int TemperatureCelcius { get; set; }

    public int TemperatureFahrenheit { get; set; }

    public string Summary { get; set; }
}

Json output:

{
  "date": "2019-10-28T08:26:14.878444+05:00",
  "temperature_celcius": 4,
  "temperature_fahrenheit": 0,
  "summary": "Scorching"
}
Noctis
  • 11,507
  • 3
  • 43
  • 82
Muhammad Hannan
  • 2,389
  • 19
  • 28
  • 7
    Actually it's not wrong. C# property naming convention is [PascalCase](https://stackoverflow.com/questions/41768733/camel-case-and-pascal-case-mistake). So, CPU is considered three different words. There two solutions you can use is either use `[JsonPropertyName("CPU_power")]` or change the property name to `CpuPower`. – Muhammad Hannan Oct 27 '19 at 13:44
  • What is the reason for using the PropertyNamingPolicy = SnakeCaseNamingPolicy.Instance instead of PropertyNamingPolicy = new SnakeCaseNamingPolicy() ? I tried the 'new' variant and as far as I can see, this works perfect. – Niels Lucas Apr 05 '23 at 15:02
18

At the moment there's no builtin support for snake case,
but .NET Core 3.0 allows to set up a custom naming policy by inheriting from JsonNamingPolicy.

You need to implement the ConvertName method with the snake case conversion.
(Newtonsoft Json.NET has an internal StringUtils class which shows how to handle this.)


The POC implementation below, re-uses Json.NET's SnakeCaseNamingStrategy only for the snake case conversion (, whereas the whole application uses System.Text.Json).

It is better to avoid having a dependency on Newtonsoft Json.Net for only the snake case conversion, but in this rather LAZY example below I don't want to rethink/reinvent a snake case conversion method.
The main point of this answer is how to hook a custom policy (and not the snake case conversion itself.)
(There are many libraries and code samples that show how to do so.)

public class SnakeCaseNamingPolicy : JsonNamingPolicy
{
    private readonly SnakeCaseNamingStrategy _newtonsoftSnakeCaseNamingStrategy
        = new SnakeCaseNamingStrategy();

    public static SnakeCaseNamingPolicy Instance { get; } = new SnakeCaseNamingPolicy();

    public override string ConvertName(string name)
    { 
        /* A conversion to snake case implementation goes here. */

        return _newtonsoftSnakeCaseNamingStrategy.GetPropertyName(name, false);
    }
}

In Startup.cs you apply this custom SnakeCaseNamingPolicy.

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers()            
        .AddJsonOptions(
            options => { 
                options.JsonSerializerOptions.PropertyNamingPolicy = 
                    SnakeCaseNamingPolicy.Instance;
            });
}

An instance of the class below

public class WeatherForecast
{
    public DateTime Date { get; set; }

    public int TemperatureCelcius { get; set; }

    public int TemperatureFahrenheit { get; set; }

    [JsonPropertyName("Description")]
    public string Summary { get; set; }
}

will have a Json representation as:

{ "date" : "2019-10-28T01:00:56.6498885+01:00",
  "temperature_celcius" : 48,
  "temperature_fahrenheit" : 118,
  "Description" : "Cool"
}

Note that the property Summary has been given the name Description,
which matches its System.Text.Json.Serialization.JsonPropertyNameAttribute.

pfx
  • 20,323
  • 43
  • 37
  • 57
  • 2
    FYI there's a [GitHub issue](https://github.com/dotnet/runtime/issues/782) opened to track the implementation of a `JsonSnakeCaseNamingPolicy` directly in `System.Text.Json`. As of July 23, 2021, the implementation is scheduled for .NET7. – Métoule Sep 30 '21 at 07:53
4

There is a GitHub Repository with SnakeCase and KebabCase support for System.Text.Json, also a nuget package is available.

nuget

PM> Install-Package JorgeSerrano.Json.JsonSnakeCaseNamingPolicy

SnakeCase NamingPolicy

var person = new Person { FirstName = "Jorge", Birthday = DateTime.UtcNow, MyJobCity = "Madrid" };

var options = new JsonSerializerOptions { PropertyNamingPolicy = new JsonSnakeCaseNamingPolicy() };
var json = JsonSerializer.Serialize(person, options);

KebabCase NamingPolicy

var person = new Person { FirstName = "Jorge", Birthday = DateTime.UtcNow, MyJobCity = "Madrid" };

var options = new JsonSerializerOptions { PropertyNamingPolicy = new JsonKebabCaseNamingPolicy() };
var json = JsonSerializer.Serialize(person, options);
live2
  • 3,771
  • 2
  • 37
  • 46
3

I share a full implementation of @pfx 's solution here. The custom naming policy (copied from NewtonSoft):

using System.Text;
using System.Text.Json;

namespace Utils
{
    public class SnakeCaseNamingPolicy : JsonNamingPolicy
    {
        public override string ConvertName(string name) => JsonUtils.ToSnakeCase(name);
    }

    public class JsonUtils
    {

        private enum SeparatedCaseState
        {
            Start,
            Lower,
            Upper,
            NewWord
        }

        public static string ToSnakeCase(string s) => ToSeparatedCase(s, '_');
    
        private static string ToSeparatedCase(string s, char separator)
        {
            if (string.IsNullOrEmpty(s))
            {
                return s;
            }

            StringBuilder sb = new StringBuilder();
            SeparatedCaseState state = SeparatedCaseState.Start;

            for (int i = 0; i < s.Length; i++)
            {
                if (s[i] == ' ')
                {
                    if (state != SeparatedCaseState.Start)
                    {
                        state = SeparatedCaseState.NewWord;
                    }
                }
                else if (char.IsUpper(s[i]))
                {
                    switch (state)
                    {
                        case SeparatedCaseState.Upper:
                            bool hasNext = (i + 1 < s.Length);
                            if (i > 0 && hasNext)
                            {
                                char nextChar = s[i + 1];
                                if (!char.IsUpper(nextChar) && nextChar != separator)
                                {
                                    sb.Append(separator);
                                }
                            }
                            break;
                        case SeparatedCaseState.Lower:
                        case SeparatedCaseState.NewWord:
                            sb.Append(separator);
                            break;
                    }

                    char c;
                    c = char.ToLowerInvariant(s[i]);
                    sb.Append(c);

                    state = SeparatedCaseState.Upper;
                }
                else if (s[i] == separator)
                {
                    sb.Append(separator);
                    state = SeparatedCaseState.Start;
                }
                else
                {
                    if (state == SeparatedCaseState.NewWord)
                    {
                        sb.Append(separator);
                    }

                    sb.Append(s[i]);
                    state = SeparatedCaseState.Lower;
                }
            }

            return sb.ToString();
        }
    }
}

The following model is used in the simple example:

    public class TestSerializer
    {
        public DateTime TimeStamp { get; set; }
        public int CPUPower { get; set; } 
    }

And the example usage:

var data = new TestSerializer();
data.TimeStamp = DateTime.Now;
data.CPUPower = 10;

var serializeOptions = new JsonSerializerOptions 
{
    PropertyNamingPolicy = new SnakeCaseNamingPolicy()
};
var json_string = JsonSerializer.Serialize(data, serializeOptions);
Console.WriteLine(json_string);

gives {"time_stamp":"2020-08-06T00:30:35.3815583-04:00","cpu_power":10}

James Hirschorn
  • 7,032
  • 5
  • 45
  • 53
1

It is a bit late, but this solution will also solve cases like ABCItem or MyCPU. This is just the concept, you can refine it to make it more versatile

using System.Collections.Generic;

namespace Extensions
{
    public static class StringExtension
    {
        public static string ToSnakeCase(this string str)
        {
            // collect the final result
            var snakeCase = new List<char>();

            // check and add chars (using for loop for performance)
            for (int i = 0; i < str.Length; i++)
            {
                if (i > 0 && char.IsUpper(str[i]) && !char.IsUpper(str[i + 1]))
                {
                    snakeCase.Add('_');
                }
                snakeCase.Add(str[i]);
            }

            // build the new string
            return new string(snakeCase.ToArray()).ToLower();
        }
    }
}

var snakeCase = "CPUMeter".ToSnakeCase();
var snakeCase = "PascalCase".ToSnakeCase();
var snakeCase = "camelCase".ToSnakeCase();
Gravity API
  • 680
  • 8
  • 16
0

There's no need for a seperate kebab- and snake- case naming policy. Also, if you absolutely want to minimize the number of unnecceesary character-conversions and lookups, the conversion method can be somewhat optimized

using System.Collections.Generic;
using System.Linq;
using System.Text.Json;

public class SeperatorNamingPolicy : JsonNamingPolicy
{
    public SeperatorNamingPolicy(char seperator = '_')
    {
        Seperator = seperator;
    }
    public char Seperator { get; }

    public override string ConvertName(string name)
    {
        IEnumerable<char> ToSeperated()
        {
            var e = name.GetEnumerator();
            if (!e.MoveNext()) yield break;
            yield return char.ToLower(e.Current);
            while (e.MoveNext())
            {
                if (char.IsUpper(e.Current))
                {
                    yield return Seperator;
                    yield return char.ToLower(e.Current);
                }
                else
                {
                    yield return e.Current;
                }
            }
        }

        return new string(ToSeperated().ToArray());
    }
}

However, if you just want a snake case naming policy without adding additional dependencies to your code, a dedicated snakecasenamingpolicy suffices:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;

public class SnakeCaseNamingPolicy : JsonNamingPolicy
{
    public override string ConvertName(string name)
    {
        static IEnumerable<char> ToSnakeCase(CharEnumerator e)
        {
            if (!e.MoveNext()) yield break;
            yield return char.ToLower(e.Current);
            while (e.MoveNext())
            {
                if (char.IsUpper(e.Current))
                {
                    yield return '_';
                    yield return char.ToLower(e.Current);
                }
                else
                {
                    yield return e.Current;
                }
            }
        }

        return new string(ToSnakeCase(name.GetEnumerator()).ToArray());
    }
}

You can of course use this by adding the json options in your startup.cs:

public void ConfigureServices(IServiceCollection services)
{
    // . . .
    services
        .AddControllers()
        .AddJsonOptions(o => o.JsonSerializerOptions.PropertyNamingPolicy = new SnakeCaseNamingPolicy());
    // . . .
}
realbart
  • 3,497
  • 1
  • 25
  • 37
0

As of .Net 8 (preview 2) there will be native support for:

See also: JsonNamingPolicy properties

Tom B.
  • 2,892
  • 3
  • 13
  • 34