Skip to content

Commit 4d05ee7

Browse files
author
Bart Koelman
committed
Updated error message for unknown attribute/relationship
1 parent 3737de1 commit 4d05ee7

14 files changed

+79
-76
lines changed
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
using System;
22
using System.Net;
3-
using System.Text;
43
using JetBrains.Annotations;
54
using JsonApiDotNetCore.Serialization.Objects;
65

@@ -14,11 +13,12 @@ public sealed class InvalidRequestBodyException : JsonApiException
1413
{
1514
public string RequestBody { get; }
1615

17-
public InvalidRequestBodyException(string reason, string details, string requestBody, string sourcePointer, Exception innerException = null)
16+
public InvalidRequestBodyException(string requestBody, string genericMessage, string specificMessage, string sourcePointer,
17+
Exception innerException = null)
1818
: base(new ErrorObject(HttpStatusCode.UnprocessableEntity)
1919
{
20-
Title = reason != null ? $"Failed to deserialize request body: {reason}" : "Failed to deserialize request body.",
21-
Detail = FormatErrorDetail(details, innerException),
20+
Title = genericMessage != null ? $"Failed to deserialize request body: {genericMessage}" : "Failed to deserialize request body.",
21+
Detail = specificMessage,
2222
Source = sourcePointer == null
2323
? null
2424
: new ErrorSource
@@ -29,13 +29,5 @@ public InvalidRequestBodyException(string reason, string details, string request
2929
{
3030
RequestBody = requestBody;
3131
}
32-
33-
private static string FormatErrorDetail(string details, Exception innerException)
34-
{
35-
var builder = new StringBuilder();
36-
builder.Append(details ?? innerException?.Message);
37-
38-
return builder.Length > 0 ? builder.ToString() : null;
39-
}
4032
}
4133
}

src/JsonApiDotNetCore/Serialization/JsonApiReader.cs

+6-2
Original file line numberDiff line numberDiff line change
@@ -69,9 +69,13 @@ private object GetModel(string requestBody)
6969
{
7070
return _documentAdapter.Convert(document);
7171
}
72+
catch (ModelConversionException exception)
73+
{
74+
throw new InvalidRequestBodyException(requestBody, exception.GenericMessage, exception.SpecificMessage, exception.SourcePointer, exception);
75+
}
7276
catch (DeserializationException exception)
7377
{
74-
throw new InvalidRequestBodyException(exception.GenericMessage, exception.SpecificMessage, requestBody, exception.SourcePointer);
78+
throw new InvalidRequestBodyException(requestBody, exception.GenericMessage, exception.SpecificMessage, exception.SourcePointer);
7579
}
7680
}
7781

@@ -103,7 +107,7 @@ private Document DeserializeDocument(string requestBody, JsonSerializerOptions s
103107
// JsonException.Path looks great for setting error.source.pointer, but unfortunately it is wrong in most cases.
104108
// This is due to the use of custom converters, which are unable to interact with internal position tracking.
105109
// https://github.com/dotnet/runtime/issues/50205#issuecomment-808401245
106-
throw new InvalidRequestBodyException(null, exception.Message, requestBody, null, exception);
110+
throw new InvalidRequestBodyException(requestBody, null, exception.Message, null, exception);
107111
}
108112
}
109113
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
using System;
2+
using JsonApiDotNetCore.Serialization.RequestAdapters;
3+
4+
namespace JsonApiDotNetCore.Serialization
5+
{
6+
/// <summary>
7+
/// The error that is thrown when unable to convert a deserialized request body to an ASP.NET model.
8+
/// </summary>
9+
internal sealed class ModelConversionException : Exception
10+
{
11+
public string GenericMessage { get; }
12+
public string SpecificMessage { get; }
13+
public string SourcePointer { get; }
14+
15+
public ModelConversionException(RequestAdapterPosition position, string genericMessage, string specificMessage)
16+
: base(genericMessage)
17+
{
18+
ArgumentGuard.NotNull(position, nameof(position));
19+
20+
GenericMessage = genericMessage;
21+
SpecificMessage = specificMessage;
22+
SourcePointer = position.ToSourcePointer();
23+
}
24+
}
25+
}

src/JsonApiDotNetCore/Serialization/RequestAdapters/AtomicReferenceAdapter.cs

+1-10
Original file line numberDiff line numberDiff line change
@@ -39,21 +39,12 @@ private RelationshipAttribute ConvertRelationship(string relationshipName, Resou
3939

4040
RelationshipAttribute relationship = resourceContext.TryGetRelationshipByPublicName(relationshipName);
4141

42-
AssertIsKnownRelationship(relationship, relationshipName, state);
42+
AssertIsKnownRelationship(relationship, relationshipName, resourceContext, state);
4343
AssertToManyInAddOrRemoveRelationship(relationship, state);
4444

4545
return relationship;
4646
}
4747

48-
private static void AssertIsKnownRelationship(RelationshipAttribute relationship, string relationshipName, RequestAdapterState state)
49-
{
50-
if (relationship == null)
51-
{
52-
throw new DeserializationException(state.Position, "Request body includes unknown relationship.",
53-
$"Relationship '{relationshipName}' does not exist.");
54-
}
55-
}
56-
5748
private static void AssertToManyInAddOrRemoveRelationship(RelationshipAttribute relationship, RequestAdapterState state)
5849
{
5950
bool requireToManyRelationship = state.Request.WriteOperation == WriteOperationKind.AddToRelationship ||

src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceIdentityAdapter.cs

+11
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using JsonApiDotNetCore.Errors;
55
using JsonApiDotNetCore.Middleware;
66
using JsonApiDotNetCore.Resources;
7+
using JsonApiDotNetCore.Resources.Annotations;
78
using JsonApiDotNetCore.Serialization.Objects;
89

910
namespace JsonApiDotNetCore.Serialization.RequestAdapters
@@ -212,5 +213,15 @@ private string ConvertLid(IResourceIdentity identity, RequestAdapterState state)
212213

213214
return identity.Lid;
214215
}
216+
217+
protected static void AssertIsKnownRelationship(RelationshipAttribute relationship, string relationshipName, ResourceContext resourceContext,
218+
RequestAdapterState state)
219+
{
220+
if (relationship == null)
221+
{
222+
throw new ModelConversionException(state.Position, "Unknown relationship found.",
223+
$"Relationship '{relationshipName}' does not exist on resource type '{resourceContext.PublicName}'.");
224+
}
225+
}
215226
}
216227
}

src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceObjectAdapter.cs

+8-28
Original file line numberDiff line numberDiff line change
@@ -60,11 +60,12 @@ private void ConvertAttribute(IIdentifiable resource, string attributeName, obje
6060

6161
AttrAttribute attr = resourceContext.TryGetAttributeByPublicName(attributeName);
6262

63-
if (!AssertIsKnownAttribute(attr, attributeName, state))
63+
if (attr == null && _options.AllowUnknownFieldsInRequestBody)
6464
{
6565
return;
6666
}
6767

68+
AssertIsKnownAttribute(attr, attributeName, resourceContext, state);
6869
AssertNoInvalidAttribute(attributeValue, state);
6970
AssertNoBlockedCreate(attr, state);
7071
AssertNoBlockedChange(attr, state);
@@ -75,19 +76,13 @@ private void ConvertAttribute(IIdentifiable resource, string attributeName, obje
7576
}
7677

7778
[AssertionMethod]
78-
private bool AssertIsKnownAttribute(AttrAttribute attr, string attributeName, RequestAdapterState state)
79+
private static void AssertIsKnownAttribute(AttrAttribute attr, string attributeName, ResourceContext resourceContext, RequestAdapterState state)
7980
{
8081
if (attr == null)
8182
{
82-
if (_options.AllowUnknownFieldsInRequestBody)
83-
{
84-
return false;
85-
}
86-
87-
throw new DeserializationException(state.Position, "Request body includes unknown attribute.", $"Attribute '{attributeName}' does not exist.");
83+
throw new ModelConversionException(state.Position, "Unknown attribute found.",
84+
$"Attribute '{attributeName}' does not exist on resource type '{resourceContext.PublicName}'.");
8885
}
89-
90-
return true;
9186
}
9287

9388
private static void AssertNoInvalidAttribute(object attributeValue, RequestAdapterState state)
@@ -150,32 +145,17 @@ private void ConvertRelationship(string relationshipName, SingleOrManyData<Resou
150145

151146
RelationshipAttribute relationship = resourceContext.TryGetRelationshipByPublicName(relationshipName);
152147

153-
if (!AssertIsKnownRelationship(relationship, relationshipName, state))
148+
if (relationship == null && _options.AllowUnknownFieldsInRequestBody)
154149
{
155150
return;
156151
}
157152

153+
AssertIsKnownRelationship(relationship, relationshipName, resourceContext, state);
154+
158155
object rightValue = _relationshipDataAdapter.Convert(relationshipData, relationship, true, state);
159156

160157
relationship.SetValue(resource, rightValue);
161158
state.WritableTargetedFields.Relationships.Add(relationship);
162159
}
163-
164-
[AssertionMethod]
165-
private bool AssertIsKnownRelationship(RelationshipAttribute relationship, string relationshipName, RequestAdapterState state)
166-
{
167-
if (relationship == null)
168-
{
169-
if (_options.AllowUnknownFieldsInRequestBody)
170-
{
171-
return false;
172-
}
173-
174-
throw new DeserializationException(state.Position, "Request body includes unknown relationship.",
175-
$"Relationship '{relationshipName}' does not exist.");
176-
}
177-
178-
return true;
179-
}
180160
}
181161
}

test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceTests.cs

+4-4
Original file line numberDiff line numberDiff line change
@@ -254,8 +254,8 @@ public async Task Cannot_create_resource_with_unknown_attribute()
254254

255255
ErrorObject error = responseDocument.Errors[0];
256256
error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity);
257-
error.Title.Should().Be("Failed to deserialize request body: Request body includes unknown attribute.");
258-
error.Detail.Should().Be("Attribute 'doesNotExist' does not exist.");
257+
error.Title.Should().Be("Failed to deserialize request body: Unknown attribute found.");
258+
error.Detail.Should().Be("Attribute 'doesNotExist' does not exist on resource type 'playlists'.");
259259
error.Source.Pointer.Should().Be("/atomic:operations[0]/data/attributes/doesNotExist");
260260

261261
responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty();
@@ -356,8 +356,8 @@ public async Task Cannot_create_resource_with_unknown_relationship()
356356

357357
ErrorObject error = responseDocument.Errors[0];
358358
error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity);
359-
error.Title.Should().Be("Failed to deserialize request body: Request body includes unknown relationship.");
360-
error.Detail.Should().Be("Relationship 'doesNotExist' does not exist.");
359+
error.Title.Should().Be("Failed to deserialize request body: Unknown relationship found.");
360+
error.Detail.Should().Be("Relationship 'doesNotExist' does not exist on resource type 'lyrics'.");
361361
error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/doesNotExist");
362362

363363
responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty();

test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicAddToToManyRelationshipTests.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -545,8 +545,8 @@ public async Task Cannot_add_for_unknown_relationship_in_ref()
545545

546546
ErrorObject error = responseDocument.Errors[0];
547547
error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity);
548-
error.Title.Should().Be("Failed to deserialize request body: Request body includes unknown relationship.");
549-
error.Detail.Should().Be($"Relationship '{Unknown.Relationship}' does not exist.");
548+
error.Title.Should().Be("Failed to deserialize request body: Unknown relationship found.");
549+
error.Detail.Should().Be($"Relationship '{Unknown.Relationship}' does not exist on resource type 'performers'.");
550550
error.Source.Pointer.Should().Be("/atomic:operations[0]/ref/relationship");
551551
}
552552

test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicRemoveFromToManyRelationshipTests.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -508,8 +508,8 @@ public async Task Cannot_remove_for_unknown_relationship_in_ref()
508508

509509
ErrorObject error = responseDocument.Errors[0];
510510
error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity);
511-
error.Title.Should().Be("Failed to deserialize request body: Request body includes unknown relationship.");
512-
error.Detail.Should().Be($"Relationship '{Unknown.Relationship}' does not exist.");
511+
error.Title.Should().Be("Failed to deserialize request body: Unknown relationship found.");
512+
error.Detail.Should().Be($"Relationship '{Unknown.Relationship}' does not exist on resource type 'performers'.");
513513
error.Source.Pointer.Should().Be("/atomic:operations[0]/ref/relationship");
514514
}
515515

test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicReplaceToManyRelationshipTests.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -600,8 +600,8 @@ public async Task Cannot_replace_for_unknown_relationship_in_ref()
600600

601601
ErrorObject error = responseDocument.Errors[0];
602602
error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity);
603-
error.Title.Should().Be("Failed to deserialize request body: Request body includes unknown relationship.");
604-
error.Detail.Should().Be($"Relationship '{Unknown.Relationship}' does not exist.");
603+
error.Title.Should().Be("Failed to deserialize request body: Unknown relationship found.");
604+
error.Detail.Should().Be($"Relationship '{Unknown.Relationship}' does not exist on resource type 'performers'.");
605605
error.Source.Pointer.Should().Be("/atomic:operations[0]/ref/relationship");
606606
}
607607

test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicUpdateToOneRelationshipTests.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -839,8 +839,8 @@ public async Task Cannot_create_for_unknown_relationship_in_ref()
839839

840840
ErrorObject error = responseDocument.Errors[0];
841841
error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity);
842-
error.Title.Should().Be("Failed to deserialize request body: Request body includes unknown relationship.");
843-
error.Detail.Should().Be($"Relationship '{Unknown.Relationship}' does not exist.");
842+
error.Title.Should().Be("Failed to deserialize request body: Unknown relationship found.");
843+
error.Detail.Should().Be($"Relationship '{Unknown.Relationship}' does not exist on resource type 'performers'.");
844844
error.Source.Pointer.Should().Be("/atomic:operations[0]/ref/relationship");
845845
}
846846

test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateResourceTests.cs

+4-4
Original file line numberDiff line numberDiff line change
@@ -207,8 +207,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext =>
207207

208208
ErrorObject error = responseDocument.Errors[0];
209209
error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity);
210-
error.Title.Should().Be("Failed to deserialize request body: Request body includes unknown attribute.");
211-
error.Detail.Should().Be("Attribute 'doesNotExist' does not exist.");
210+
error.Title.Should().Be("Failed to deserialize request body: Unknown attribute found.");
211+
error.Detail.Should().Be("Attribute 'doesNotExist' does not exist on resource type 'musicTracks'.");
212212
error.Source.Pointer.Should().Be("/atomic:operations[0]/data/attributes/doesNotExist");
213213

214214
responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty();
@@ -320,8 +320,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext =>
320320

321321
ErrorObject error = responseDocument.Errors[0];
322322
error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity);
323-
error.Title.Should().Be("Failed to deserialize request body: Request body includes unknown relationship.");
324-
error.Detail.Should().Be("Relationship 'doesNotExist' does not exist.");
323+
error.Title.Should().Be("Failed to deserialize request body: Unknown relationship found.");
324+
error.Detail.Should().Be("Relationship 'doesNotExist' does not exist on resource type 'musicTracks'.");
325325
error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/doesNotExist");
326326

327327
responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty();

test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs

+4-4
Original file line numberDiff line numberDiff line change
@@ -284,8 +284,8 @@ public async Task Cannot_create_resource_with_unknown_attribute()
284284

285285
ErrorObject error = responseDocument.Errors[0];
286286
error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity);
287-
error.Title.Should().Be("Failed to deserialize request body: Request body includes unknown attribute.");
288-
error.Detail.Should().Be("Attribute 'doesNotExist' does not exist.");
287+
error.Title.Should().Be("Failed to deserialize request body: Unknown attribute found.");
288+
error.Detail.Should().Be("Attribute 'doesNotExist' does not exist on resource type 'workItems'.");
289289
error.Source.Pointer.Should().Be("/data/attributes/doesNotExist");
290290

291291
responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty();
@@ -371,8 +371,8 @@ public async Task Cannot_create_resource_with_unknown_relationship()
371371

372372
ErrorObject error = responseDocument.Errors[0];
373373
error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity);
374-
error.Title.Should().Be("Failed to deserialize request body: Request body includes unknown relationship.");
375-
error.Detail.Should().Be("Relationship 'doesNotExist' does not exist.");
374+
error.Title.Should().Be("Failed to deserialize request body: Unknown relationship found.");
375+
error.Detail.Should().Be("Relationship 'doesNotExist' does not exist on resource type 'workItems'.");
376376
error.Source.Pointer.Should().Be("/data/relationships/doesNotExist");
377377

378378
responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty();

test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs

+4-4
Original file line numberDiff line numberDiff line change
@@ -125,8 +125,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext =>
125125

126126
ErrorObject error = responseDocument.Errors[0];
127127
error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity);
128-
error.Title.Should().Be("Failed to deserialize request body: Request body includes unknown attribute.");
129-
error.Detail.Should().Be("Attribute 'doesNotExist' does not exist.");
128+
error.Title.Should().Be("Failed to deserialize request body: Unknown attribute found.");
129+
error.Detail.Should().Be("Attribute 'doesNotExist' does not exist on resource type 'userAccounts'.");
130130
error.Source.Pointer.Should().Be("/data/attributes/doesNotExist");
131131

132132
responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty();
@@ -225,8 +225,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext =>
225225

226226
ErrorObject error = responseDocument.Errors[0];
227227
error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity);
228-
error.Title.Should().Be("Failed to deserialize request body: Request body includes unknown relationship.");
229-
error.Detail.Should().Be("Relationship 'doesNotExist' does not exist.");
228+
error.Title.Should().Be("Failed to deserialize request body: Unknown relationship found.");
229+
error.Detail.Should().Be("Relationship 'doesNotExist' does not exist on resource type 'userAccounts'.");
230230
error.Source.Pointer.Should().Be("/data/relationships/doesNotExist");
231231

232232
responseDocument.Meta["requestBody"].ToString().Should().NotBeNullOrEmpty();

0 commit comments

Comments
 (0)