Skip to content

Commit c259150

Browse files
Bart Koelmanmaurei
Bart Koelman
andauthored
Fixed: Apply casing convention in error response resulting from invalid modal state (#723)
Co-authored-by: Maurits Moeys <maurei@users.noreply.github.com>
1 parent ab1e180 commit c259150

File tree

7 files changed

+22
-15
lines changed

7 files changed

+22
-15
lines changed

src/JsonApiDotNetCore/Builders/ResourceGraphBuilder.cs

+1-2
Original file line numberDiff line numberDiff line change
@@ -232,8 +232,7 @@ private string FormatResourceName(Type resourceType)
232232

233233
private string FormatPropertyName(PropertyInfo resourceProperty)
234234
{
235-
var contractResolver = (DefaultContractResolver)_options.SerializerSettings.ContractResolver;
236-
return contractResolver.NamingStrategy.GetPropertyName(resourceProperty.Name, false);
235+
return _options.SerializerContractResolver.NamingStrategy.GetPropertyName(resourceProperty.Name, false);
237236
}
238237
}
239238
}

src/JsonApiDotNetCore/Configuration/IJsonApiOptions.cs

+3
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using JsonApiDotNetCore.Models;
33
using JsonApiDotNetCore.Models.JsonApiDocuments;
44
using Newtonsoft.Json;
5+
using Newtonsoft.Json.Serialization;
56

67
namespace JsonApiDotNetCore.Configuration
78
{
@@ -61,6 +62,8 @@ public interface IJsonApiOptions : ILinksConfiguration
6162
/// </summary>
6263
JsonSerializerSettings SerializerSettings { get; }
6364

65+
internal DefaultContractResolver SerializerContractResolver => (DefaultContractResolver)SerializerSettings.ContractResolver;
66+
6467
/// <summary>
6568
/// Specifies the default query string capabilities that can be used on exposed json:api attributes.
6669
/// Defaults to <see cref="AttrCapabilities.All"/>.

src/JsonApiDotNetCore/Controllers/BaseJsonApiController.cs

+9-2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using JsonApiDotNetCore.Services;
77
using Microsoft.AspNetCore.Mvc;
88
using Microsoft.Extensions.Logging;
9+
using Newtonsoft.Json.Serialization;
910

1011
namespace JsonApiDotNetCore.Controllers
1112
{
@@ -114,7 +115,10 @@ public virtual async Task<IActionResult> PostAsync([FromBody] T entity)
114115
throw new ResourceIdInPostRequestNotAllowedException();
115116

116117
if (_jsonApiOptions.ValidateModelState && !ModelState.IsValid)
117-
throw new InvalidModelStateException(ModelState, typeof(T), _jsonApiOptions.IncludeExceptionStackTraceInErrors);
118+
{
119+
var namingStrategy = _jsonApiOptions.SerializerContractResolver.NamingStrategy;
120+
throw new InvalidModelStateException(ModelState, typeof(T), _jsonApiOptions.IncludeExceptionStackTraceInErrors, namingStrategy);
121+
}
118122

119123
entity = await _create.CreateAsync(entity);
120124

@@ -130,7 +134,10 @@ public virtual async Task<IActionResult> PatchAsync(TId id, [FromBody] T entity)
130134
throw new InvalidRequestBodyException(null, null, null);
131135

132136
if (_jsonApiOptions.ValidateModelState && !ModelState.IsValid)
133-
throw new InvalidModelStateException(ModelState, typeof(T), _jsonApiOptions.IncludeExceptionStackTraceInErrors);
137+
{
138+
var namingStrategy = _jsonApiOptions.SerializerContractResolver.NamingStrategy;
139+
throw new InvalidModelStateException(ModelState, typeof(T), _jsonApiOptions.IncludeExceptionStackTraceInErrors, namingStrategy);
140+
}
134141

135142
var updatedEntity = await _update.UpdateAsync(id, entity);
136143
return updatedEntity == null ? Ok(null) : Ok(updatedEntity);

src/JsonApiDotNetCore/Exceptions/InvalidModelStateException.cs

+5-5
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using JsonApiDotNetCore.Models;
77
using JsonApiDotNetCore.Models.JsonApiDocuments;
88
using Microsoft.AspNetCore.Mvc.ModelBinding;
9+
using Newtonsoft.Json.Serialization;
910

1011
namespace JsonApiDotNetCore.Exceptions
1112
{
@@ -17,13 +18,13 @@ public class InvalidModelStateException : Exception
1718
public IList<Error> Errors { get; }
1819

1920
public InvalidModelStateException(ModelStateDictionary modelState, Type resourceType,
20-
bool includeExceptionStackTraceInErrors)
21+
bool includeExceptionStackTraceInErrors, NamingStrategy namingStrategy)
2122
{
22-
Errors = FromModelState(modelState, resourceType, includeExceptionStackTraceInErrors);
23+
Errors = FromModelState(modelState, resourceType, includeExceptionStackTraceInErrors, namingStrategy);
2324
}
2425

2526
private static List<Error> FromModelState(ModelStateDictionary modelState, Type resourceType,
26-
bool includeExceptionStackTraceInErrors)
27+
bool includeExceptionStackTraceInErrors, NamingStrategy namingStrategy)
2728
{
2829
List<Error> errors = new List<Error>();
2930

@@ -32,9 +33,8 @@ private static List<Error> FromModelState(ModelStateDictionary modelState, Type
3233
var propertyName = pair.Key;
3334
PropertyInfo property = resourceType.GetProperty(propertyName);
3435

35-
// TODO: Need access to ResourceContext here, in order to determine attribute name when not explicitly set.
3636
string attributeName =
37-
property?.GetCustomAttribute<AttrAttribute>().PublicAttributeName ?? property?.Name;
37+
property.GetCustomAttribute<AttrAttribute>().PublicAttributeName ?? namingStrategy.GetPropertyName(property.Name, false);
3838

3939
foreach (var modelError in pair.Value.Errors)
4040
{

src/JsonApiDotNetCore/Graph/ResourceNameFormatter.cs

+1-2
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,7 @@ internal sealed class ResourceNameFormatter
1313

1414
public ResourceNameFormatter(IJsonApiOptions options)
1515
{
16-
var contractResolver = (DefaultContractResolver) options.SerializerSettings.ContractResolver;
17-
_namingStrategy = contractResolver.NamingStrategy;
16+
_namingStrategy = options.SerializerContractResolver.NamingStrategy;
1817
}
1918

2019
/// <summary>

src/JsonApiDotNetCore/Internal/DefaultRoutingConvention.cs

+1-2
Original file line numberDiff line numberDiff line change
@@ -107,8 +107,7 @@ private string TemplateFromResource(ControllerModel model)
107107
/// </summary>
108108
private string TemplateFromController(ControllerModel model)
109109
{
110-
var contractResolver = (DefaultContractResolver) _options.SerializerSettings.ContractResolver;
111-
string controllerName = contractResolver.NamingStrategy.GetPropertyName(model.ControllerName, false);
110+
string controllerName = _options.SerializerContractResolver.NamingStrategy.GetPropertyName(model.ControllerName, false);
112111

113112
var template = $"{_options.Namespace}/{controllerName}";
114113
if (_registeredTemplates.Add(template))

test/JsonApiDotNetCoreExampleTests/Acceptance/ModelStateValidationTests.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ public async Task When_posting_tag_with_invalid_name_it_must_fail()
5353
Assert.Equal(HttpStatusCode.UnprocessableEntity, errorDocument.Errors[0].StatusCode);
5454
Assert.Equal("Input validation failed.", errorDocument.Errors[0].Title);
5555
Assert.Equal("The field Name must match the regular expression '^\\W$'.", errorDocument.Errors[0].Detail);
56-
Assert.Equal("/data/attributes/Name", errorDocument.Errors[0].Source.Pointer);
56+
Assert.Equal("/data/attributes/name", errorDocument.Errors[0].Source.Pointer);
5757
}
5858

5959
[Fact]
@@ -127,7 +127,7 @@ public async Task When_patching_tag_with_invalid_name_it_must_fail()
127127
Assert.Equal(HttpStatusCode.UnprocessableEntity, errorDocument.Errors[0].StatusCode);
128128
Assert.Equal("Input validation failed.", errorDocument.Errors[0].Title);
129129
Assert.Equal("The field Name must match the regular expression '^\\W$'.", errorDocument.Errors[0].Detail);
130-
Assert.Equal("/data/attributes/Name", errorDocument.Errors[0].Source.Pointer);
130+
Assert.Equal("/data/attributes/name", errorDocument.Errors[0].Source.Pointer);
131131
}
132132

133133
[Fact]

0 commit comments

Comments
 (0)