Creando una JsonNamingPolicy para hacer compatible System.Text.Json con SnakeCase y KebabCase
Nota: El código fuente de las políticas que en esta entrada se describen, han sido actualizadas después de publicar la entrada y el mismo día de la publicación para mejorar su rendimiento. La mejora ha sido considerable. Encontrarás el código actualizado en este artículo y en GitHub (enlace al repositorio de código al final de la entrada).
Introducción
Dentro del ecosistema de los diferentes lenguajes de programación, tenemos la posibilidad de encontrar diferentes convenciones de codificación.
Sobre ello escribí una entrada el año pasado por si quieres leer algo al respecto (La importancia de las convenciones de codificación: PascalCase, camelCase, snake_case y kebab-case).
SnakeCase por ejemplo es utilizado por las APIs de Facebook y Twitter, o en AWS.
En lenguajes de programación como Ruby, se utiliza snake_case, motivo por el cual Twitter lo tiene así implementado en su API.
En el caso de PHP, se utiliza igualmente snake_case.
Google y Microsoft por su parte suelen utilizar camelCase usado así en Java y .NET.
Igualmente pasa con Pascal en el uso de camelCase.
Es decir, no existe ninguna estandarización clara respecto a este uso, por lo que si no se cubre en la librería que utilizamos (por ejemplo en System.Text.Json en el caso de .NET Core 3.x), somos nosotros los programadores los que debemos cubrir esta necesidad.
Lo mismo sucede para KebabCase.
El siguiente código es para SnakeCase, pero es idéntico a KebabCase, con la salvedad de que en luar del carácter _ tenemos el carácter -.
No obstante, el código de ambas políticas las encontrarás en mi repositorio de código en GitHub que indico al final del artículo.
System.Text.Json y .NET Core 3.x
Cuando trabajamos con System.Text.Json, tenemos que tener en consideración que en un principio sólo hay soporte para camelCase y PascalCase, si bien se está haciendo esfuerzos por agregar soporte para SnakeCase (actualmente en fase de desarrollo y resolución).
Hasta que llegue ese soporte y de momento, podemos crear nuestra propia JsonNamingPolicy.
Eso es justamente lo que voy a hacer.
Creando la JsonNamingPolicy
Lo primero que debemos hacer es crear una JsonNamingPolicy, por lo que tendremos que crear una clase que la implemente.
Después sobreescribiremos el método ConvertName dentro del cual llevaremos a cabo la conversión del nombre de una propiedad a su correspondiente nombre en SnakeCase.
El código de nuestra clase quedará de la siguiente forma:
public class JsonSnakeCaseNamingPolicy : JsonNamingPolicy { private readonly string _separator = "_"; public override string ConvertName(string name) { if (String.IsNullOrEmpty(name) || String.IsNullOrWhiteSpace(name)) return String.Empty; ReadOnlySpan spanName = name.Trim(); var stringBuilder = new StringBuilder(); var addCharacter = true; var isPreviousSpace = false; var isPreviousSeparator = false; var isCurrentSpace = false; var isNextLower = false; var isNextUpper = false; var isNextSpace = false; for (int position = 0; position < spanName.Length; position++) { if (position != 0) { isCurrentSpace = spanName[position] == 32; isPreviousSpace = spanName[position - 1] == 32; isPreviousSeparator = spanName[position - 1] == 95; if (position + 1 != spanName.Length) { isNextLower = spanName[position + 1] > 96 && spanName[position + 1] < 123; isNextUpper = spanName[position + 1] > 64 && spanName[position + 1] < 91; isNextSpace = spanName[position + 1] == 32; } if ((isCurrentSpace) && ((isPreviousSpace) || (isPreviousSeparator) || (isNextUpper) || (isNextSpace))) addCharacter = false; else { var isCurrentUpper = spanName[position] > 64 && spanName[position] < 91; var isPreviousLower = spanName[position - 1] > 96 && spanName[position - 1] < 123; var isPreviousNumber = spanName[position - 1] > 47 && spanName[position - 1] < 58; if ((isCurrentUpper) && ((isPreviousLower) || (isPreviousNumber) || (isNextLower) || (isNextSpace) || (isNextLower && !isPreviousSpace))) stringBuilder.Append(_separator); else { if ((isCurrentSpace && !isPreviousSpace && !isNextSpace)) { stringBuilder.Append(_separator); addCharacter = false; } } } } if (addCharacter) stringBuilder.Append(spanName[position]); else addCharacter = true; } return stringBuilder.ToString().ToLower(); } }
Utilizando nuestra clase
Para utilizar nuestra clase, bastará con utilizarla a la hora de serializar nuestro objeto en JSON por ejemplo.
Ejemplo de uso:
var options = new JsonSerializerOptions() { PropertyNamingPolicy = new JsonSnakeCaseNamingPolicy() }; var person = new Person() { FirstName = "Jorge", Birthday = DateTime.UtcNow, MyJobCity = "Madrid" }; var json = JsonSerializer.Serialize(person, options);
Si ejecutamos nuestro ejemplo, deberíamos obtener un resultado similar al siguiente:
{"first_name":"Jorge","birthday":"2020-01-03T20:00:59.6991482Z","my_job_city":"Madrid"}
Espero que este ejemplo facilite tareas de migración y uso de System.Text.Json hasta que esté lista nativamente la conversión que se supone agregarán en la librería en breve.
Podrás acceder al código de JsonSnakeCaseNamingPolicy y JsonKebabCaseNamingPolicy en mi repositorio de GitHub aquí.
Happy Coding!