Short version
How to create API that can be shared between different parties that use different conventions for JSON properties in both places, request and response. In other words, if client #1 sends JSON request in snake case, it should receive JSON response in snake case. If client #2 sends camel case, they should receive camel case back in the response.
Long version with explanation and examples
1. snake_case { some_id: 1, some_name: "www" }
2. alllowercase { someid: 2, somename: "www" }
3. TitleCase { SomeId: 1, SomeName: "www" }
4. camelCase { someId: 1, someName: "www" }
Default model binder is case insensitive, and can handle options 2, 3, 4 naturally, but if input JSON contains "snake_case", default model binder will not extract it [FromUri] and will not map it to a model.
class SomeModel
{
int SomeId { get; set; }
string SomeName { get; set; }
}
A controller.
class SomeController()
{
[HttpGet]
public dynamic SomeGetAction([FromUri] SomeModel model) { ... }
[HttpPost]
public dynamic SomePostAction([FromBody] SomeModel model) { ... }
}
Approach #1: Attributes
In this case, we tell the controller to search for snake case properties in an incoming JSON. If the snake case attribute won't be found, it should try to extract properties without underscore.
class SomeModel
{
[JsonProperty(Name="some_id")]
int SomeId { get; set; }
[JsonProperty(Name="some_name")]
string SomeName { get; set; }
}
Approach #2: Get-Set wrappers
We can create both kinds of property in our model to cover all possible JSON variations.
class SomeModel
{
int some_id { get; set; }
string some_name { get; set; }
int SomeId { get { return some_id; } set { some_id = value; } }
string SomeName { get { return some_name; } set { some_name = value; } }
}
Approach #3: Custom model binder
In this case, create an intermediate layer that can parse request header and body and extract properties of any case.
class MyModelBinder<SomeModel>()
{
public bool BindModel(HttpActionContext actionCtx, ModelBindingContext modelCtx)
{
var queryGet = actionCtx
.Request
.GetQueryNameValuePairs()
.ToDictionary(o => o.Key, o => o.Value as object);
var queryPost = actionCtx
.Request
.Content
.ReadAsAsync<dynamic>
.Result
.ToObject<Dictionary<string, object>>);
modelContext.model.SomeId = // try to find property in the request
queryGet["some_id"] ??
queryGet["someid"] ??
queryGet["someId"] ??
queryGet["SomeId"];
return true;
}
}
class SomeController()
{
[HttpGet]
public dynamic SomeGetAction([MyModelBinder<typeof(SomeModel)>] SomeModel model) { ... }
[HttpPost]
public dynamic SomePostAction([MyModelBinder<typeof(SomeModel)>] SomeModel model) { ... }
}
Approach #4: Contract resolver
Approaches #1 and #2 seem too verbose and is not accepted by some team members. Approach #3 seems to cover all cases, but for some reason, it hides all model properties from Swagger, so we decided to take a look at a contract resolver that can be set up on the controller level and modify incoming and outgoing JSON before or after the action. We tried this approach https://stackoverflow.com/a/52766426/437393 and it works fine for the outgoing JSON in the response, but it doesn't transform incoming JSON in the request.
So, the alternate question is, how to make sure that the controller will receive JSON of a specific format using Contract Resolver, always convert JSON to snake case?