Skip to content

Commit 1e1bafa

Browse files
author
Bart Koelman
committed
Unified error messages about incompatible types
1 parent d5d5c8d commit 1e1bafa

26 files changed

+81
-129
lines changed

src/JsonApiDotNetCore/Errors/InvalidRequestBodyException.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ public sealed class InvalidRequestBodyException : JsonApiException
1414
public string RequestBody { get; }
1515

1616
public InvalidRequestBodyException(string requestBody, string genericMessage, string specificMessage, string sourcePointer,
17-
Exception innerException = null)
18-
: base(new ErrorObject(HttpStatusCode.UnprocessableEntity)
17+
HttpStatusCode? alternativeStatusCode = null, Exception innerException = null)
18+
: base(new ErrorObject(alternativeStatusCode ?? HttpStatusCode.UnprocessableEntity)
1919
{
2020
Title = genericMessage != null ? $"Failed to deserialize request body: {genericMessage}" : "Failed to deserialize request body.",
2121
Detail = specificMessage,

src/JsonApiDotNetCore/Errors/ResourceTypeMismatchException.cs

-25
This file was deleted.

src/JsonApiDotNetCore/Serialization/JsonApiReader.cs

+3-2
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,8 @@ private object GetModel(string requestBody)
7171
}
7272
catch (ModelConversionException exception)
7373
{
74-
throw new InvalidRequestBodyException(requestBody, exception.GenericMessage, exception.SpecificMessage, exception.SourcePointer, exception);
74+
throw new InvalidRequestBodyException(requestBody, exception.GenericMessage, exception.SpecificMessage, exception.SourcePointer,
75+
exception.StatusCode, exception);
7576
}
7677
catch (DeserializationException exception)
7778
{
@@ -107,7 +108,7 @@ private Document DeserializeDocument(string requestBody, JsonSerializerOptions s
107108
// JsonException.Path looks great for setting error.source.pointer, but unfortunately it is wrong in most cases.
108109
// This is due to the use of custom converters, which are unable to interact with internal position tracking.
109110
// https://github.com/dotnet/runtime/issues/50205#issuecomment-808401245
110-
throw new InvalidRequestBodyException(requestBody, null, exception.Message, null, exception);
111+
throw new InvalidRequestBodyException(requestBody, null, exception.Message, null, null, exception);
111112
}
112113
}
113114
}

src/JsonApiDotNetCore/Serialization/ModelConversionException.cs

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Net;
23
using JsonApiDotNetCore.Serialization.RequestAdapters;
34

45
namespace JsonApiDotNetCore.Serialization
@@ -10,15 +11,17 @@ internal sealed class ModelConversionException : Exception
1011
{
1112
public string GenericMessage { get; }
1213
public string SpecificMessage { get; }
14+
public HttpStatusCode? StatusCode { get; }
1315
public string SourcePointer { get; }
1416

15-
public ModelConversionException(RequestAdapterPosition position, string genericMessage, string specificMessage)
17+
public ModelConversionException(RequestAdapterPosition position, string genericMessage, string specificMessage, HttpStatusCode? statusCode = null)
1618
: base(genericMessage)
1719
{
1820
ArgumentGuard.NotNull(position, nameof(position));
1921

2022
GenericMessage = genericMessage;
2123
SpecificMessage = specificMessage;
24+
StatusCode = statusCode;
2225
SourcePointer = position.ToSourcePointer();
2326
}
2427
}

src/JsonApiDotNetCore/Serialization/RequestAdapters/RelationshipDataAdapter.cs

+1-2
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,7 @@ public object Convert(SingleOrManyData<ResourceIdentifierObject> data, Relations
8080
{
8181
ResourceContext = rightResourceContext,
8282
IdConstraint = JsonElementConstraint.Required,
83-
RelationshipName = relationship.PublicName,
84-
UseLegacyError = state.Request.Kind != EndpointKind.Relationship
83+
RelationshipName = relationship.PublicName
8584
};
8685

8786
return relationship is HasOneAttribute

src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceIdentityAdapter.cs

+13-34
Original file line numberDiff line numberDiff line change
@@ -112,43 +112,10 @@ private ResourceContext ConvertType(IResourceIdentity identity, ResourceIdentity
112112
AssertHasType(identity, state);
113113

114114
using IDisposable _ = state.Position.PushElement("type");
115-
116115
ResourceContext resourceContext = _resourceGraph.TryGetResourceContext(identity.Type);
117116

118117
AssertIsKnownResourceType(resourceContext, identity.Type, state);
119-
120-
if (requirements?.ResourceContext != null && !requirements.ResourceContext.ResourceType.IsAssignableFrom(resourceContext.ResourceType))
121-
{
122-
if (requirements.UseLegacyError)
123-
{
124-
throw new DeserializationException(state.Position, "Relationship contains incompatible resource type.",
125-
$"Relationship '{requirements.RelationshipName}' contains incompatible resource type '{resourceContext.PublicName}'.");
126-
}
127-
128-
if (state.Request.Kind == EndpointKind.AtomicOperations)
129-
{
130-
throw new DeserializationException(state.Position, "Resource type mismatch between 'ref.type' and 'data.type' element.",
131-
$"Expected resource of type '{requirements.ResourceContext.PublicName}' in 'data.type', instead of '{resourceContext.PublicName}'.");
132-
}
133-
134-
string title = requirements.RelationshipName != null ? "Resource type is incompatible with relationship type." :
135-
state.Request.Kind == EndpointKind.AtomicOperations ? "Resource type is incompatible with type in ref." :
136-
"Resource type is incompatible with endpoint URL.";
137-
138-
string detail = requirements.RelationshipName != null
139-
? $"Type '{resourceContext.PublicName}' is incompatible with type '{requirements.ResourceContext.PublicName}' of relationship '{requirements.RelationshipName}'."
140-
: $"Type '{resourceContext.PublicName}' is incompatible with type '{requirements.ResourceContext.PublicName}'.";
141-
142-
throw new JsonApiException(new ErrorObject(HttpStatusCode.Conflict)
143-
{
144-
Title = title,
145-
Detail = detail,
146-
Source = new ErrorSource
147-
{
148-
Pointer = state.Position.ToSourcePointer()
149-
}
150-
});
151-
}
118+
AssertIsCompatibleResourceType(resourceContext, requirements.ResourceContext, requirements.RelationshipName, state);
152119

153120
return resourceContext;
154121
}
@@ -169,6 +136,18 @@ private static void AssertIsKnownResourceType(ResourceContext resourceContext, s
169136
}
170137
}
171138

139+
private static void AssertIsCompatibleResourceType(ResourceContext actual, ResourceContext expected, string relationshipName, RequestAdapterState state)
140+
{
141+
if (expected != null && !expected.ResourceType.IsAssignableFrom(actual.ResourceType))
142+
{
143+
string message = relationshipName != null
144+
? $"Type '{actual.PublicName}' is incompatible with type '{expected.PublicName}' of relationship '{relationshipName}'."
145+
: $"Type '{actual.PublicName}' is incompatible with type '{expected.PublicName}'.";
146+
147+
throw new ModelConversionException(state.Position, "Incompatible resource type found.", message, HttpStatusCode.Conflict);
148+
}
149+
}
150+
172151
private static void AssertHasNoLid(IResourceIdentity identity, RequestAdapterState state)
173152
{
174153
if (identity.Lid != null)

src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceIdentityRequirements.cs

-5
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,5 @@ public sealed class ResourceIdentityRequirements
3434
/// When not null, indicates the name of the relationship to use in error messages.
3535
/// </summary>
3636
public string RelationshipName { get; init; }
37-
38-
/// <summary>
39-
/// This temporary property will be removed in a future commit.
40-
/// </summary>
41-
public bool UseLegacyError { get; init; }
4237
}
4338
}

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

+4-4
Original file line numberDiff line numberDiff line change
@@ -445,14 +445,14 @@ public async Task Cannot_create_on_relationship_type_mismatch()
445445
(HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync<Document>(route, requestBody);
446446

447447
// Assert
448-
httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity);
448+
httpResponse.Should().HaveStatusCode(HttpStatusCode.Conflict);
449449

450450
responseDocument.Errors.Should().HaveCount(1);
451451

452452
ErrorObject error = responseDocument.Errors[0];
453-
error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity);
454-
error.Title.Should().Be("Failed to deserialize request body: Relationship contains incompatible resource type.");
455-
error.Detail.Should().Be("Relationship 'performers' contains incompatible resource type 'playlists'.");
453+
error.StatusCode.Should().Be(HttpStatusCode.Conflict);
454+
error.Title.Should().Be("Failed to deserialize request body: Incompatible resource type found.");
455+
error.Detail.Should().Be("Type 'playlists' is incompatible with type 'performers' of relationship 'performers'.");
456456
error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/performers/data[0]/type");
457457
}
458458

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

+4-4
Original file line numberDiff line numberDiff line change
@@ -480,14 +480,14 @@ public async Task Cannot_create_on_relationship_type_mismatch()
480480
(HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync<Document>(route, requestBody);
481481

482482
// Assert
483-
httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity);
483+
httpResponse.Should().HaveStatusCode(HttpStatusCode.Conflict);
484484

485485
responseDocument.Errors.Should().HaveCount(1);
486486

487487
ErrorObject error = responseDocument.Errors[0];
488-
error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity);
489-
error.Title.Should().Be("Failed to deserialize request body: Relationship contains incompatible resource type.");
490-
error.Detail.Should().Be("Relationship 'lyric' contains incompatible resource type 'playlists'.");
488+
error.StatusCode.Should().Be(HttpStatusCode.Conflict);
489+
error.Title.Should().Be("Failed to deserialize request body: Incompatible resource type found.");
490+
error.Detail.Should().Be("Type 'playlists' is incompatible with type 'lyrics' of relationship 'lyric'.");
491491
error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/lyric/data/type");
492492
}
493493

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

+4-4
Original file line numberDiff line numberDiff line change
@@ -894,14 +894,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext =>
894894
(HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync<Document>(route, requestBody);
895895

896896
// Assert
897-
httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity);
897+
httpResponse.Should().HaveStatusCode(HttpStatusCode.Conflict);
898898

899899
responseDocument.Errors.Should().HaveCount(1);
900900

901901
ErrorObject error = responseDocument.Errors[0];
902-
error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity);
903-
error.Title.Should().Be("Failed to deserialize request body: Relationship contains incompatible resource type.");
904-
error.Detail.Should().Be("Relationship 'performers' contains incompatible resource type 'playlists'.");
902+
error.StatusCode.Should().Be(HttpStatusCode.Conflict);
903+
error.Title.Should().Be("Failed to deserialize request body: Incompatible resource type found.");
904+
error.Detail.Should().Be("Type 'playlists' is incompatible with type 'performers' of relationship 'performers'.");
905905
error.Source.Pointer.Should().Be("/atomic:operations[0]/data[0]/type");
906906
}
907907

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

+4-4
Original file line numberDiff line numberDiff line change
@@ -857,14 +857,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext =>
857857
(HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync<Document>(route, requestBody);
858858

859859
// Assert
860-
httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity);
860+
httpResponse.Should().HaveStatusCode(HttpStatusCode.Conflict);
861861

862862
responseDocument.Errors.Should().HaveCount(1);
863863

864864
ErrorObject error = responseDocument.Errors[0];
865-
error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity);
866-
error.Title.Should().Be("Failed to deserialize request body: Relationship contains incompatible resource type.");
867-
error.Detail.Should().Be("Relationship 'performers' contains incompatible resource type 'playlists'.");
865+
error.StatusCode.Should().Be(HttpStatusCode.Conflict);
866+
error.Title.Should().Be("Failed to deserialize request body: Incompatible resource type found.");
867+
error.Detail.Should().Be("Type 'playlists' is incompatible with type 'performers' of relationship 'performers'.");
868868
error.Source.Pointer.Should().Be("/atomic:operations[0]/data[0]/type");
869869
}
870870

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

+4-4
Original file line numberDiff line numberDiff line change
@@ -1003,14 +1003,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext =>
10031003
(HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync<Document>(route, requestBody);
10041004

10051005
// Assert
1006-
httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity);
1006+
httpResponse.Should().HaveStatusCode(HttpStatusCode.Conflict);
10071007

10081008
responseDocument.Errors.Should().HaveCount(1);
10091009

10101010
ErrorObject error = responseDocument.Errors[0];
1011-
error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity);
1012-
error.Title.Should().Be("Failed to deserialize request body: Relationship contains incompatible resource type.");
1013-
error.Detail.Should().Be("Relationship 'performers' contains incompatible resource type 'playlists'.");
1011+
error.StatusCode.Should().Be(HttpStatusCode.Conflict);
1012+
error.Title.Should().Be("Failed to deserialize request body: Incompatible resource type found.");
1013+
error.Detail.Should().Be("Type 'playlists' is incompatible with type 'performers' of relationship 'performers'.");
10141014
error.Source.Pointer.Should().Be("/atomic:operations[0]/data[0]/type");
10151015
}
10161016
}

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

+4-4
Original file line numberDiff line numberDiff line change
@@ -1213,14 +1213,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext =>
12131213
(HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync<Document>(route, requestBody);
12141214

12151215
// Assert
1216-
httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity);
1216+
httpResponse.Should().HaveStatusCode(HttpStatusCode.Conflict);
12171217

12181218
responseDocument.Errors.Should().HaveCount(1);
12191219

12201220
ErrorObject error = responseDocument.Errors[0];
1221-
error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity);
1222-
error.Title.Should().Be("Failed to deserialize request body: Relationship contains incompatible resource type.");
1223-
error.Detail.Should().Be("Relationship 'lyric' contains incompatible resource type 'playlists'.");
1221+
error.StatusCode.Should().Be(HttpStatusCode.Conflict);
1222+
error.Title.Should().Be("Failed to deserialize request body: Incompatible resource type found.");
1223+
error.Detail.Should().Be("Type 'playlists' is incompatible with type 'lyrics' of relationship 'lyric'.");
12241224
error.Source.Pointer.Should().Be("/atomic:operations[0]/data/type");
12251225
}
12261226
}

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

+4-4
Original file line numberDiff line numberDiff line change
@@ -670,14 +670,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext =>
670670
(HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync<Document>(route, requestBody);
671671

672672
// Assert
673-
httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity);
673+
httpResponse.Should().HaveStatusCode(HttpStatusCode.Conflict);
674674

675675
responseDocument.Errors.Should().HaveCount(1);
676676

677677
ErrorObject error = responseDocument.Errors[0];
678-
error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity);
679-
error.Title.Should().Be("Failed to deserialize request body: Relationship contains incompatible resource type.");
680-
error.Detail.Should().Be("Relationship 'performers' contains incompatible resource type 'playlists'.");
678+
error.StatusCode.Should().Be(HttpStatusCode.Conflict);
679+
error.Title.Should().Be("Failed to deserialize request body: Incompatible resource type found.");
680+
error.Detail.Should().Be("Type 'playlists' is incompatible with type 'performers' of relationship 'performers'.");
681681
error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/performers/data[0]/type");
682682
}
683683
}

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

+4-4
Original file line numberDiff line numberDiff line change
@@ -1085,14 +1085,14 @@ public async Task Cannot_update_on_resource_type_mismatch_between_ref_and_data()
10851085
(HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync<Document>(route, requestBody);
10861086

10871087
// Assert
1088-
httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity);
1088+
httpResponse.Should().HaveStatusCode(HttpStatusCode.Conflict);
10891089

10901090
responseDocument.Errors.Should().HaveCount(1);
10911091

10921092
ErrorObject error = responseDocument.Errors[0];
1093-
error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity);
1094-
error.Title.Should().Be("Failed to deserialize request body: Resource type mismatch between 'ref.type' and 'data.type' element.");
1095-
error.Detail.Should().Be("Expected resource of type 'performers' in 'data.type', instead of 'playlists'.");
1093+
error.StatusCode.Should().Be(HttpStatusCode.Conflict);
1094+
error.Title.Should().Be("Failed to deserialize request body: Incompatible resource type found.");
1095+
error.Detail.Should().Be("Type 'playlists' is incompatible with type 'performers'.");
10961096
error.Source.Pointer.Should().Be("/atomic:operations[0]/data/type");
10971097
}
10981098

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

+4-4
Original file line numberDiff line numberDiff line change
@@ -916,14 +916,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext =>
916916
(HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync<Document>(route, requestBody);
917917

918918
// Assert
919-
httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity);
919+
httpResponse.Should().HaveStatusCode(HttpStatusCode.Conflict);
920920

921921
responseDocument.Errors.Should().HaveCount(1);
922922

923923
ErrorObject error = responseDocument.Errors[0];
924-
error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity);
925-
error.Title.Should().Be("Failed to deserialize request body: Relationship contains incompatible resource type.");
926-
error.Detail.Should().Be("Relationship 'lyric' contains incompatible resource type 'playlists'.");
924+
error.StatusCode.Should().Be(HttpStatusCode.Conflict);
925+
error.Title.Should().Be("Failed to deserialize request body: Incompatible resource type found.");
926+
error.Detail.Should().Be("Type 'playlists' is incompatible with type 'lyrics' of relationship 'lyric'.");
927927
error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/lyric/data/type");
928928
}
929929
}

0 commit comments

Comments
 (0)