Zalando’s RESTful API Guidelines describes in detail how they design RESTful APIs and even offer a configurable OpenAPI Linter called zally
.
In this blog post, I want to show you how to configure ASP.NET Core 5.0 Web APIs to conform to three specific rules regarding path and JSON body layout. For that propose I modified the “weather forecast” starter template to demonstrate the rules which can be found on my GitHub profile
.
- Rule #118
: Property names must be ASCII snake_case (and never camelCase)
- Rule #129
: use lowercase separate words with hyphens for path segments
- Rule #130
: use
snake_case
(never camelCase
) for query parameters
Rule #118
: Property names must be ASCII snake_case (and never camelCase)
First let us look how the default JSON body output looks like. Have a look at WeatherForecast class has the properties TemperatureCelsius and TemperatureFahrenheit.
public class WeatherForecast
{
public DateTime Date { get; set; }
public int TemperatureCelsius { get; set; }
public int TemperatureFahrenheit => 32 + (int) (TemperatureCelsius / 0.5556);
public string Summary { get; set; }
}
Without changing the setting of the Framework, we get the snakeCase formatting „temperatureCelsius“ and „temperatureFahrenheit“ as JSON property names.
[
{
"date": "2021-07-07T08:31:49.9734021+02:00",
"temperatureCelsius": 39,
"temperatureFahrenheit": 102,
"summary": "Warm"
},
{
"date": "2021-07-09T08:31:49.9734114+02:00",
"temperatureCelsius": 29,
"temperatureFahrenheit": 84,
"summary": "Scorching"
}
]
By default, APS.NET Core 5.0 uses System.Text.Json which currently only supports camelCase
to serialize and deserialize objects into JSON. There is an open issue
on the .NET runtime repository to add snake_case
support.
Solution #1 use [JsonPropertyName]
The simple solution is to use [JsonPropertyName]
Annotation from System.Text.Json.Serialization and define all property names by hand.
public class WeatherForecast
{
public DateTime Date { get; set; }
[JsonPropertyName("temperature_celsius")]
public int TemperatureCelsius { get; set; }
[JsonPropertyName("temperature_fahrenheit")]
public int TemperatureFahrenheit => 32 + (int)(TemperatureCelsius / 0.5556);
public string Summary { get; set; }
}
Which produces the desired output.
[
{
"date": "2021-07-07T09:19:30.9345519+02:00",
"temperature_celsius": 43,
"temperature_fahrenheit": 109,
"summary": "Sweltering"
},
{
"date": "2021-07-08T09:19:30.9345608+02:00",
"temperature_celsius": 5,
"temperature_fahrenheit": 40,
"summary": "Hot"
}
]
Solution #2 use Newtonsoft.Json
Another solution is to use Newtonsoft.Json
for serialization and configure it to use the SnakeCaseNamingStrategy
. Here for you need to add a dependency on Microsoft.AspNetCore.Mvc.NewtonsoftJson
and Swashbuckle.AspNetCore.Newtonsoft
. Then configure ASP.NET Core and Swashbuckle to use Newtonsoft.Json
instead of System.Text.Json
.
<itemgroup>
<packagereference include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" version="5.0.7"></packagereference>
<packagereference include="Swashbuckle.AspNetCore" version="6.1.4"></packagereference>
<packagereference include="Swashbuckle.AspNetCore.Newtonsoft" version="6.1.4"></packagereference>
</itemgroup>
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers()
.AddNewtonsoftJson(options =>
{
options.SerializerSettings.ContractResolver = new DefaultContractResolver
{
NamingStrategy = new SnakeCaseNamingStrategy()
};
options.SerializerSettings.Converters.Add(new StringEnumConverter());
});
services.AddSwaggerGenNewtonsoftSupport();
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo {Title = "ZalandoRestfulApiGuidelinesWebApi", Version = "v1"});
});
}
Rule #129
: use lowercase separate words with hyphens for path segments
By default, the Framework uses the Route Attribute [Route(„[controller]“)]
to generate the path. For example, the WeatherForecastController
will be reachable unter the /WeatherForecast
path. But that does not comply to the rule. /weather-forecast
would be compliant.
[ApiController]
[Route("[controller]")]
[Consumes("application/json")]
[Produces("application/json")]
public class WeatherForecastController : ControllerBase
{
...
}
Solution #1 explicit define the [Route]
Simply use a constant string in the [Route]
attribute. see line 2: [Route(„weather-forecast“)]
[ApiController]
[Route("weather-forecast")]
[Consumes("application/json")]
[Produces("application/json")]
public class WeatherForecastController : ControllerBase
{
...
}
Use of an RouteTokenTransformerConvention
to conform to this rule.
// Source https://docs.microsoft.com/en-us/aspnet/core/mvc/controllers/routing?view=aspnetcore-5.0#use-a-parameter-transformer-to-customize-token-replacement
public class SlugifyParameterTransformer : IOutboundParameterTransformer
{
public string TransformOutbound(object value)
{
if (value == null) { return null; }
return Regex.Replace(value.ToString(),
"([a-z])([A-Z])",
"$1-$2",
RegexOptions.CultureInvariant,
TimeSpan.FromMilliseconds(100)).ToLowerInvariant();
}
}
//Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers(options =>
{
options.Conventions.Add(new RouteTokenTransformerConvention(new SlugifyParameterTransformer()));
});
}
Rule #130
: use snake_case
(never camelCase
) for query parameters
Use of [FromQuery]
Attribute to define the correct formatting.
[HttpGet]
public IEnumerable<WeatherForecast> Search(
[FromQuery(Name = "q")] string query,
[FromQuery(Name = "created_before")] string createdBefore)
{
...
}
Links