Skip to content

Commit c9a4f5a

Browse files
author
Bart Koelman
committed
Unified error messages about the absense or presense of 'id' and 'lid'
1 parent 213313e commit c9a4f5a

25 files changed

+156
-148
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
using System.Net;
2+
using JetBrains.Annotations;
3+
using JsonApiDotNetCore.Serialization.Objects;
4+
5+
namespace JsonApiDotNetCore.Errors
6+
{
7+
/// <summary>
8+
/// The error that is thrown when creating a resource with a client-generated ID.
9+
/// </summary>
10+
[PublicAPI]
11+
public sealed class ForbiddenClientGeneratedIdException : JsonApiException
12+
{
13+
public ForbiddenClientGeneratedIdException(string sourcePointer)
14+
: base(new ErrorObject(HttpStatusCode.Forbidden)
15+
{
16+
Title = "The use of client-generated IDs is disabled.",
17+
Source = new ErrorSource
18+
{
19+
Pointer = sourcePointer
20+
}
21+
})
22+
{
23+
}
24+
}
25+
}

src/JsonApiDotNetCore/Errors/ResourceIdInCreateResourceNotAllowedException.cs

-27
This file was deleted.

src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceIdentityAdapter.cs

+49-39
Original file line numberDiff line numberDiff line change
@@ -35,36 +35,20 @@ protected ResourceIdentityAdapter(IResourceGraph resourceGraph, IResourceFactory
3535

3636
ResourceContext resourceContext = ConvertType(identity, requirements, state);
3737

38-
bool hasNone = identity.Id == null && identity.Lid == null;
39-
bool hasBoth = identity.Id != null && identity.Lid != null;
40-
41-
if (requirements.IdConstraint == JsonElementConstraint.Required ? hasNone || hasBoth : hasBoth)
38+
if (state.Request.Kind != EndpointKind.AtomicOperations)
4239
{
43-
string parent = identity is AtomicReference
44-
? "'ref' element"
45-
:
46-
requirements.RelationshipName != null &&
47-
state.Request.WriteOperation is WriteOperationKind.CreateResource or WriteOperationKind.UpdateResource
48-
?
49-
$"'{requirements.RelationshipName}' relationship"
50-
: "'data' element";
51-
52-
throw new DeserializationException(state.Position,
53-
state.Request.Kind == EndpointKind.AtomicOperations
54-
? "Request body must include 'id' or 'lid' element."
55-
: "Request body must include 'id' element.",
56-
state.Request.Kind == EndpointKind.AtomicOperations
57-
? $"Expected 'id' or 'lid' element in {parent}."
58-
: $"Expected 'id' element in {parent}.");
40+
AssertHasNoLid(identity, state);
5941
}
6042

61-
if (requirements.IdConstraint == JsonElementConstraint.Forbidden && identity.Id != null)
43+
AssertNoIdWithLid(identity, state);
44+
45+
if (requirements.IdConstraint == JsonElementConstraint.Required)
6246
{
63-
using (state.Position.PushElement("id"))
64-
{
65-
throw new ResourceIdInCreateResourceNotAllowedException(state.Request.Kind == EndpointKind.AtomicOperations,
66-
state.Position.ToSourcePointer());
67-
}
47+
AssertHasIdOrLid(identity, state);
48+
}
49+
else if (requirements.IdConstraint == JsonElementConstraint.Forbidden)
50+
{
51+
AssertHasNoId(identity, state);
6852
}
6953

7054
if (requirements.IdValue != null && identity.Id != null && identity.Id != requirements.IdValue)
@@ -118,7 +102,7 @@ state.Request.WriteOperation is WriteOperationKind.CreateResource or WriteOperat
118102

119103
IIdentifiable resource = _resourceFactory.CreateInstance(resourceContext.ResourceType);
120104
AssignStringId(identity, resource, state);
121-
resource.LocalId = ConvertLid(identity, state);
105+
resource.LocalId = identity.Lid;
122106

123107
return (resource, resourceContext);
124108
}
@@ -189,6 +173,44 @@ private static void AssertIsKnownResourceType(ResourceContext resourceContext, s
189173
}
190174
}
191175

176+
private static void AssertHasNoLid(IResourceIdentity identity, RequestAdapterState state)
177+
{
178+
if (identity.Lid != null)
179+
{
180+
using IDisposable _ = state.Position.PushElement("lid");
181+
throw new ModelConversionException(state.Position, "The 'lid' element is not supported at this endpoint.", null);
182+
}
183+
}
184+
185+
private static void AssertNoIdWithLid(IResourceIdentity identity, RequestAdapterState state)
186+
{
187+
if (identity.Id != null && identity.Lid != null)
188+
{
189+
throw new ModelConversionException(state.Position, "The 'id' and 'lid' element are mutually exclusive.", null);
190+
}
191+
}
192+
193+
private static void AssertHasIdOrLid(IResourceIdentity identity, RequestAdapterState state)
194+
{
195+
if (identity.Id == null && identity.Lid == null)
196+
{
197+
string message = state.Request.Kind == EndpointKind.AtomicOperations
198+
? "The 'id' or 'lid' element is required."
199+
: "The 'id' element is required.";
200+
201+
throw new ModelConversionException(state.Position, message, null);
202+
}
203+
}
204+
205+
private static void AssertHasNoId(IResourceIdentity identity, RequestAdapterState state)
206+
{
207+
if (identity.Id != null)
208+
{
209+
using IDisposable _ = state.Position.PushElement("id");
210+
throw new ForbiddenClientGeneratedIdException(state.Position.ToSourcePointer());
211+
}
212+
}
213+
192214
private void AssignStringId(IResourceIdentity identity, IIdentifiable resource, RequestAdapterState state)
193215
{
194216
if (identity.Id != null)
@@ -206,18 +228,6 @@ private void AssignStringId(IResourceIdentity identity, IIdentifiable resource,
206228
}
207229
}
208230

209-
private string ConvertLid(IResourceIdentity identity, RequestAdapterState state)
210-
{
211-
using IDisposable _ = state.Position.PushElement("lid");
212-
213-
if (state.Request.Kind != EndpointKind.AtomicOperations && identity.Lid != null)
214-
{
215-
throw new DeserializationException(state.Position, null, "Local IDs cannot be used at this endpoint.");
216-
}
217-
218-
return identity.Lid;
219-
}
220-
221231
protected static void AssertIsKnownRelationship(RelationshipAttribute relationship, string relationshipName, ResourceContext resourceContext,
222232
RequestAdapterState state)
223233
{

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -459,7 +459,7 @@ public async Task Cannot_create_resource_with_client_generated_ID()
459459

460460
ErrorObject error = responseDocument.Errors[0];
461461
error.StatusCode.Should().Be(HttpStatusCode.Forbidden);
462-
error.Title.Should().Be("Specifying the resource ID in operations that create a resource is not allowed.");
462+
error.Title.Should().Be("The use of client-generated IDs is disabled.");
463463
error.Detail.Should().BeNull();
464464
error.Source.Pointer.Should().Be("/atomic:operations[0]/data/id");
465465
}

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -263,8 +263,8 @@ public async Task Cannot_create_resource_for_ID_and_local_ID()
263263

264264
ErrorObject error = responseDocument.Errors[0];
265265
error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity);
266-
error.Title.Should().Be("Failed to deserialize request body: Request body must include 'id' or 'lid' element.");
267-
error.Detail.Should().Be("Expected 'id' or 'lid' element in 'data' element.");
266+
error.Title.Should().Be("Failed to deserialize request body: The 'id' and 'lid' element are mutually exclusive.");
267+
error.Detail.Should().BeNull();
268268
error.Source.Pointer.Should().Be("/atomic:operations[0]/data");
269269
}
270270
}

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -331,8 +331,8 @@ public async Task Cannot_create_for_missing_relationship_ID()
331331

332332
ErrorObject error = responseDocument.Errors[0];
333333
error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity);
334-
error.Title.Should().Be("Failed to deserialize request body: Request body must include 'id' or 'lid' element.");
335-
error.Detail.Should().Be("Expected 'id' or 'lid' element in 'performers' relationship.");
334+
error.Title.Should().Be("Failed to deserialize request body: The 'id' or 'lid' element is required.");
335+
error.Detail.Should().BeNull();
336336
error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/performers/data[0]");
337337
}
338338

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -390,8 +390,8 @@ public async Task Cannot_create_for_missing_relationship_ID()
390390

391391
ErrorObject error = responseDocument.Errors[0];
392392
error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity);
393-
error.Title.Should().Be("Failed to deserialize request body: Request body must include 'id' or 'lid' element.");
394-
error.Detail.Should().Be("Expected 'id' or 'lid' element in 'lyric' relationship.");
393+
error.Title.Should().Be("Failed to deserialize request body: The 'id' or 'lid' element is required.");
394+
error.Detail.Should().BeNull();
395395
error.Source.Pointer.Should().Be("/atomic:operations[0]/data/relationships/lyric/data");
396396
}
397397

test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Deleting/AtomicDeleteResourceTests.cs

+4-4
Original file line numberDiff line numberDiff line change
@@ -497,8 +497,8 @@ public async Task Cannot_delete_resource_for_missing_ID()
497497

498498
ErrorObject error = responseDocument.Errors[0];
499499
error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity);
500-
error.Title.Should().Be("Failed to deserialize request body: Request body must include 'id' or 'lid' element.");
501-
error.Detail.Should().Be("Expected 'id' or 'lid' element in 'ref' element.");
500+
error.Title.Should().Be("Failed to deserialize request body: The 'id' or 'lid' element is required.");
501+
error.Detail.Should().BeNull();
502502
error.Source.Pointer.Should().Be("/atomic:operations[0]/ref");
503503
}
504504

@@ -613,8 +613,8 @@ public async Task Cannot_delete_resource_for_ID_and_local_ID()
613613

614614
ErrorObject error = responseDocument.Errors[0];
615615
error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity);
616-
error.Title.Should().Be("Failed to deserialize request body: Request body must include 'id' or 'lid' element.");
617-
error.Detail.Should().Be("Expected 'id' or 'lid' element in 'ref' element.");
616+
error.Title.Should().Be("Failed to deserialize request body: The 'id' and 'lid' element are mutually exclusive.");
617+
error.Detail.Should().BeNull();
618618
error.Source.Pointer.Should().Be("/atomic:operations[0]/ref");
619619
}
620620
}

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

+8-8
Original file line numberDiff line numberDiff line change
@@ -375,8 +375,8 @@ public async Task Cannot_add_for_missing_ID_in_ref()
375375

376376
ErrorObject error = responseDocument.Errors[0];
377377
error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity);
378-
error.Title.Should().Be("Failed to deserialize request body: Request body must include 'id' or 'lid' element.");
379-
error.Detail.Should().Be("Expected 'id' or 'lid' element in 'ref' element.");
378+
error.Title.Should().Be("Failed to deserialize request body: The 'id' or 'lid' element is required.");
379+
error.Detail.Should().BeNull();
380380
error.Source.Pointer.Should().Be("/atomic:operations[0]/ref");
381381
}
382382

@@ -470,8 +470,8 @@ public async Task Cannot_add_for_ID_and_local_ID_in_ref()
470470

471471
ErrorObject error = responseDocument.Errors[0];
472472
error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity);
473-
error.Title.Should().Be("Failed to deserialize request body: Request body must include 'id' or 'lid' element.");
474-
error.Detail.Should().Be("Expected 'id' or 'lid' element in 'ref' element.");
473+
error.Title.Should().Be("Failed to deserialize request body: The 'id' and 'lid' element are mutually exclusive.");
474+
error.Detail.Should().BeNull();
475475
error.Source.Pointer.Should().Be("/atomic:operations[0]/ref");
476476
}
477477

@@ -728,8 +728,8 @@ public async Task Cannot_add_for_missing_ID_in_data()
728728

729729
ErrorObject error = responseDocument.Errors[0];
730730
error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity);
731-
error.Title.Should().Be("Failed to deserialize request body: Request body must include 'id' or 'lid' element.");
732-
error.Detail.Should().Be("Expected 'id' or 'lid' element in 'data' element.");
731+
error.Title.Should().Be("Failed to deserialize request body: The 'id' or 'lid' element is required.");
732+
error.Detail.Should().BeNull();
733733
error.Source.Pointer.Should().Be("/atomic:operations[0]/data[0]");
734734
}
735735

@@ -775,8 +775,8 @@ public async Task Cannot_add_for_ID_and_local_ID_in_data()
775775

776776
ErrorObject error = responseDocument.Errors[0];
777777
error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity);
778-
error.Title.Should().Be("Failed to deserialize request body: Request body must include 'id' or 'lid' element.");
779-
error.Detail.Should().Be("Expected 'id' or 'lid' element in 'data' element.");
778+
error.Title.Should().Be("Failed to deserialize request body: The 'id' and 'lid' element are mutually exclusive.");
779+
error.Detail.Should().BeNull();
780780
error.Source.Pointer.Should().Be("/atomic:operations[0]/data[0]");
781781
}
782782

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

+8-8
Original file line numberDiff line numberDiff line change
@@ -375,8 +375,8 @@ public async Task Cannot_remove_for_missing_ID_in_ref()
375375

376376
ErrorObject error = responseDocument.Errors[0];
377377
error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity);
378-
error.Title.Should().Be("Failed to deserialize request body: Request body must include 'id' or 'lid' element.");
379-
error.Detail.Should().Be("Expected 'id' or 'lid' element in 'ref' element.");
378+
error.Title.Should().Be("Failed to deserialize request body: The 'id' or 'lid' element is required.");
379+
error.Detail.Should().BeNull();
380380
error.Source.Pointer.Should().Be("/atomic:operations[0]/ref");
381381
}
382382

@@ -470,8 +470,8 @@ public async Task Cannot_remove_for_ID_and_local_ID_in_ref()
470470

471471
ErrorObject error = responseDocument.Errors[0];
472472
error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity);
473-
error.Title.Should().Be("Failed to deserialize request body: Request body must include 'id' or 'lid' element.");
474-
error.Detail.Should().Be("Expected 'id' or 'lid' element in 'ref' element.");
473+
error.Title.Should().Be("Failed to deserialize request body: The 'id' and 'lid' element are mutually exclusive.");
474+
error.Detail.Should().BeNull();
475475
error.Source.Pointer.Should().Be("/atomic:operations[0]/ref");
476476
}
477477

@@ -691,8 +691,8 @@ public async Task Cannot_remove_for_missing_ID_in_data()
691691

692692
ErrorObject error = responseDocument.Errors[0];
693693
error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity);
694-
error.Title.Should().Be("Failed to deserialize request body: Request body must include 'id' or 'lid' element.");
695-
error.Detail.Should().Be("Expected 'id' or 'lid' element in 'data' element.");
694+
error.Title.Should().Be("Failed to deserialize request body: The 'id' or 'lid' element is required.");
695+
error.Detail.Should().BeNull();
696696
error.Source.Pointer.Should().Be("/atomic:operations[0]/data[0]");
697697
}
698698

@@ -738,8 +738,8 @@ public async Task Cannot_remove_for_ID_and_local_ID_in_data()
738738

739739
ErrorObject error = responseDocument.Errors[0];
740740
error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity);
741-
error.Title.Should().Be("Failed to deserialize request body: Request body must include 'id' or 'lid' element.");
742-
error.Detail.Should().Be("Expected 'id' or 'lid' element in 'data' element.");
741+
error.Title.Should().Be("Failed to deserialize request body: The 'id' and 'lid' element are mutually exclusive.");
742+
error.Detail.Should().BeNull();
743743
error.Source.Pointer.Should().Be("/atomic:operations[0]/data[0]");
744744
}
745745

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

+8-8
Original file line numberDiff line numberDiff line change
@@ -411,8 +411,8 @@ public async Task Cannot_replace_for_missing_ID_in_ref()
411411

412412
ErrorObject error = responseDocument.Errors[0];
413413
error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity);
414-
error.Title.Should().Be("Failed to deserialize request body: Request body must include 'id' or 'lid' element.");
415-
error.Detail.Should().Be("Expected 'id' or 'lid' element in 'ref' element.");
414+
error.Title.Should().Be("Failed to deserialize request body: The 'id' or 'lid' element is required.");
415+
error.Detail.Should().BeNull();
416416
error.Source.Pointer.Should().Be("/atomic:operations[0]/ref");
417417
}
418418

@@ -562,8 +562,8 @@ public async Task Cannot_replace_for_ID_and_local_ID_in_ref()
562562

563563
ErrorObject error = responseDocument.Errors[0];
564564
error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity);
565-
error.Title.Should().Be("Failed to deserialize request body: Request body must include 'id' or 'lid' element.");
566-
error.Detail.Should().Be("Expected 'id' or 'lid' element in 'ref' element.");
565+
error.Title.Should().Be("Failed to deserialize request body: The 'id' and 'lid' element are mutually exclusive.");
566+
error.Detail.Should().BeNull();
567567
error.Source.Pointer.Should().Be("/atomic:operations[0]/ref");
568568
}
569569

@@ -783,8 +783,8 @@ public async Task Cannot_replace_for_missing_ID_in_data()
783783

784784
ErrorObject error = responseDocument.Errors[0];
785785
error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity);
786-
error.Title.Should().Be("Failed to deserialize request body: Request body must include 'id' or 'lid' element.");
787-
error.Detail.Should().Be("Expected 'id' or 'lid' element in 'data' element.");
786+
error.Title.Should().Be("Failed to deserialize request body: The 'id' or 'lid' element is required.");
787+
error.Detail.Should().BeNull();
788788
error.Source.Pointer.Should().Be("/atomic:operations[0]/data[0]");
789789
}
790790

@@ -830,8 +830,8 @@ public async Task Cannot_replace_for_ID_and_local_ID_in_data()
830830

831831
ErrorObject error = responseDocument.Errors[0];
832832
error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity);
833-
error.Title.Should().Be("Failed to deserialize request body: Request body must include 'id' or 'lid' element.");
834-
error.Detail.Should().Be("Expected 'id' or 'lid' element in 'data' element.");
833+
error.Title.Should().Be("Failed to deserialize request body: The 'id' and 'lid' element are mutually exclusive.");
834+
error.Detail.Should().BeNull();
835835
error.Source.Pointer.Should().Be("/atomic:operations[0]/data[0]");
836836
}
837837

0 commit comments

Comments
 (0)