Skip to content

Instantly share code, notes, and snippets.

@crallen
Created February 26, 2014 20:46
Show Gist options
  • Save crallen/9238178 to your computer and use it in GitHub Desktop.
Save crallen/9238178 to your computer and use it in GitHub Desktop.
Json.NET contract resolver that uses Ruby-style lowercase with underscore naming conventions.
using Newtonsoft.Json.Serialization;
namespace ConsoleApplication3
{
public class SnakeCaseContractResolver : DefaultContractResolver
{
protected override string ResolvePropertyName(string propertyName)
{
return GetSnakeCase(propertyName);
}
private string GetSnakeCase(string input)
{
if (string.IsNullOrEmpty(input))
return input;
var buffer = "";
for (var i = 0; i < input.Length; i++)
{
var isLast = (i == input.Length - 1);
var isSecondFromLast = (i == input.Length - 2);
var curr = input[i];
var next = !isLast ? input[i + 1] : '\0';
var afterNext = !isSecondFromLast && !isLast ? input[i + 2] : '\0';
buffer += char.ToLower(curr);
if (!char.IsDigit(curr) && char.IsUpper(next))
{
if (char.IsUpper(curr))
{
if (!isLast && !isSecondFromLast && !char.IsUpper(afterNext))
buffer += "_";
}
else
buffer += "_";
}
if (!char.IsDigit(curr) && char.IsDigit(next))
buffer += "_";
if (char.IsDigit(curr) && !char.IsDigit(next) && !isLast)
buffer += "_";
}
return buffer;
}
}
}
@crallen
Copy link
Author

crallen commented Feb 26, 2014

Serialization:

var settings = new JsonSerializerSettings
{
    ContractResolver = new SnakeCaseContractResolver()
};

var product = new Product
{
    Id = 1,
    ItemName = "test",
    ItemNumber = "test123",
    Price = 5.99m
};

var output = JsonConvert.SerializeObject(product, settings);

Deserialization:

var deserializedProduct = JsonConvert.DeserializeObject<Product>(output, settings);

@chrisisaacs
Copy link

Nice, cheers!

@NiclasLindqvist
Copy link

Nice! Thank you!

@floodcode
Copy link

Thanks!

@dirodrigues
Copy link

Hey guys,
This doesn't work if some of it is already PascalCase. anyone come across this issue?
i.e.

 {
    "some_key": 0,
    "SomeOtherKey": 0,
    "some_id": "1",
    "SomeOtherId": "2"
  }

only the first and the third one work

@crallen?

@Timovzl
Copy link

Timovzl commented Apr 11, 2016

// (Preceded by a lowercase character or digit) (a capital) => The character prefixed with an underscore
var result = Regex.Replace(input, "(?<=[a-z0-9])[A-Z]", m => "_" + m.Value);
result = result.ToLowerInvariant();
  • This works for both PascalCase and camelCase.
  • It creates no leading or trailing underscores.
  • It leaves in tact any sequences of non-word characters and underscores in the string, because they would seem intentional, e.g. __HiThere_Guys becomes __hi_there_guys.
  • Digit suffixes are (intentionally) considered part of the word, e.g. NewVersion3 becomes new_version3.
  • Digit prefixes follow the original casing, e.g. 3VersionsHere becomes 3_versions_here, but 3rdVersion becomes 3rd_version.

@jorgeyanesdiez
Copy link

I agree, I was using Regex.Replace(input, @"([a-z0-9])([A-Z])", "$1_$2").ToLowerInvariant();
I think the ?<= in the regex adds unnecessary complexity, am I wrong?

@Timovzl
Copy link

Timovzl commented Jun 9, 2016

Interesting idea. I'm not sure how the ?<= (lookbehind) is handled by the regex engine. It's that vs. replacing more data, which at least is guaranteed to be linear time. And yours looks simpler too, so agreed.

@Lechus
Copy link

Lechus commented Jun 28, 2017

Works! Thanks.

@snboisen
Copy link

snboisen commented Aug 8, 2017

Nowadays you don't need a custom contract resolver, just do:

var settings = new JsonSerializerSettings
{
    ContractResolver = new DefaultContractResolver
    {
        NamingStrategy = new SnakeCaseNamingStrategy()
    }
};

@Andrei-Fogoros
Copy link

Hi Chris,
Thank you for your code! :)

Can you please tell me what is the license for the above code?

Regards,
Andrei

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment