Skip to content

Commit 79cfb3d

Browse files
committed
Improve error response from invalid ModelState when [ApiController] is used
1 parent 55d7cb0 commit 79cfb3d

File tree

3 files changed

+68
-3
lines changed

3 files changed

+68
-3
lines changed

src/JsonApiDotNetCore/Errors/UnsuccessfulActionResultException.cs

+19-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System.Net;
22
using JetBrains.Annotations;
33
using JsonApiDotNetCore.Serialization.Objects;
4+
using Microsoft.AspNetCore.Http;
45
using Microsoft.AspNetCore.Mvc;
56

67
namespace JsonApiDotNetCore.Errors;
@@ -20,20 +21,35 @@ public UnsuccessfulActionResultException(HttpStatusCode status)
2021
}
2122

2223
public UnsuccessfulActionResultException(ProblemDetails problemDetails)
23-
: base(ToError(problemDetails))
24+
: base(ToErrorObjects(problemDetails))
2425
{
2526
}
2627

27-
private static ErrorObject ToError(ProblemDetails problemDetails)
28+
private static IEnumerable<ErrorObject> ToErrorObjects(ProblemDetails problemDetails)
2829
{
2930
ArgumentGuard.NotNull(problemDetails);
3031

3132
HttpStatusCode status = problemDetails.Status != null ? (HttpStatusCode)problemDetails.Status.Value : HttpStatusCode.InternalServerError;
3233

34+
if (problemDetails is HttpValidationProblemDetails validationProblemDetails && validationProblemDetails.Errors.Any())
35+
{
36+
foreach (string errorMessage in validationProblemDetails.Errors.SelectMany(pair => pair.Value))
37+
{
38+
yield return ToErrorObject(status, validationProblemDetails, errorMessage);
39+
}
40+
}
41+
else
42+
{
43+
yield return ToErrorObject(status, problemDetails, problemDetails.Detail);
44+
}
45+
}
46+
47+
private static ErrorObject ToErrorObject(HttpStatusCode status, ProblemDetails problemDetails, string? detail)
48+
{
3349
var error = new ErrorObject(status)
3450
{
3551
Title = problemDetails.Title,
36-
Detail = problemDetails.Detail
52+
Detail = detail
3753
};
3854

3955
if (!string.IsNullOrWhiteSpace(problemDetails.Instance))

test/JsonApiDotNetCoreTests/IntegrationTests/CustomRoutes/ApiControllerAttributeTests.cs

+44
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,48 @@ public async Task ApiController_attribute_transforms_NotFound_action_result_with
3535
error.Links.ShouldNotBeNull();
3636
error.Links.About.Should().Be("https://tools.ietf.org/html/rfc7231#section-6.5.4");
3737
}
38+
39+
[Fact]
40+
public async Task ProblemDetails_from_invalid_ModelState_is_translated_into_error_response()
41+
{
42+
// Arrange
43+
var requestBody = new
44+
{
45+
data = new
46+
{
47+
type = "civilians",
48+
attributes = new
49+
{
50+
name = (string?)null,
51+
yearOfBirth = 1850
52+
}
53+
}
54+
};
55+
56+
const string route = "/world-civilians";
57+
58+
// Act
59+
(HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync<Document>(route, requestBody);
60+
61+
// Assert
62+
httpResponse.ShouldHaveStatusCode(HttpStatusCode.BadRequest);
63+
64+
responseDocument.Errors.ShouldHaveCount(2);
65+
66+
ErrorObject error1 = responseDocument.Errors[0];
67+
error1.StatusCode.Should().Be(HttpStatusCode.BadRequest);
68+
error1.Links.ShouldNotBeNull();
69+
error1.Links.About.Should().Be("https://tools.ietf.org/html/rfc7231#section-6.5.1");
70+
error1.Title.Should().Be("One or more validation errors occurred.");
71+
error1.Detail.Should().Be("The Name field is required.");
72+
error1.Source.Should().BeNull();
73+
74+
ErrorObject error2 = responseDocument.Errors[1];
75+
error2.StatusCode.Should().Be(HttpStatusCode.BadRequest);
76+
error2.Links.ShouldNotBeNull();
77+
error2.Links.About.Should().Be("https://tools.ietf.org/html/rfc7231#section-6.5.1");
78+
error2.Title.Should().Be("One or more validation errors occurred.");
79+
error2.Detail.Should().Be("The field YearOfBirth must be between 1900 and 2050.");
80+
error2.Source.Should().BeNull();
81+
}
3882
}

test/JsonApiDotNetCoreTests/IntegrationTests/CustomRoutes/Civilian.cs

+5
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System.ComponentModel.DataAnnotations;
12
using JetBrains.Annotations;
23
using JsonApiDotNetCore.Resources;
34
using JsonApiDotNetCore.Resources.Annotations;
@@ -10,4 +11,8 @@ public sealed class Civilian : Identifiable<int>
1011
{
1112
[Attr]
1213
public string Name { get; set; } = null!;
14+
15+
[Attr]
16+
[Range(1900, 2050)]
17+
public int YearOfBirth { get; set; }
1318
}

0 commit comments

Comments
 (0)