Skip to content

Commit 927077d

Browse files
author
Bart Koelman
committed
Refactor serialization objects
- Simplified error objects, so they are similar to the other serialization objects. This means no default instances, constructors (exception: ErrorObject) or conditional serialization logic. And explicit names to overrule naming conventions. And annotations to skip serialization when null. - Added missing members from JSON:API v1.1 spec: ErrorDocument.Meta, ErrorLinks.Type, ErrorSource.Header, ResourceIdentifierObject.Meta - Normalized collection types - Updated documentation: Link to v1.1 of JSON:API spec instead of copy/pasted text
1 parent c03b85d commit 927077d

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+158
-283
lines changed

src/JsonApiDotNetCore/AtomicOperations/LocalIdValidator.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ public void Validate(IEnumerable<OperationContainer> operations)
4747
{
4848
foreach (ErrorObject error in exception.Errors)
4949
{
50+
error.Source ??= new ErrorSource();
5051
error.Source.Pointer = $"/atomic:operations[{operationIndex}]{error.Source.Pointer}";
5152
}
5253

src/JsonApiDotNetCore/AtomicOperations/OperationsProcessor.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ public virtual async Task<IList<OperationContainer>> ProcessAsync(IList<Operatio
7979
{
8080
foreach (ErrorObject error in exception.Errors)
8181
{
82+
error.Source ??= new ErrorSource();
8283
error.Source.Pointer = $"/atomic:operations[{results.Count}]{error.Source.Pointer}";
8384
}
8485

@@ -92,7 +93,7 @@ public virtual async Task<IList<OperationContainer>> ProcessAsync(IList<Operatio
9293
{
9394
Title = "An unhandled error occurred while processing an operation in this request.",
9495
Detail = exception.Message,
95-
Source =
96+
Source = new ErrorSource
9697
{
9798
Pointer = $"/atomic:operations[{results.Count}]"
9899
}

src/JsonApiDotNetCore/Configuration/IJsonApiOptions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ public interface IJsonApiOptions
3131
bool IncludeJsonApiVersion { get; }
3232

3333
/// <summary>
34-
/// Whether or not <see cref="Exception" /> stack traces should be serialized in <see cref="ErrorMeta" /> objects. False by default.
34+
/// Whether or not <see cref="Exception" /> stack traces should be serialized in <see cref="ErrorObject.Meta" />. False by default.
3535
/// </summary>
3636
bool IncludeExceptionStackTraceInErrors { get; }
3737

src/JsonApiDotNetCore/Controllers/CoreJsonApiController.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System.Collections.Generic;
2+
using System.Linq;
23
using JsonApiDotNetCore.Serialization.Objects;
34
using Microsoft.AspNetCore.Mvc;
45

@@ -20,7 +21,10 @@ protected IActionResult Error(IEnumerable<ErrorObject> errors)
2021
{
2122
ArgumentGuard.NotNull(errors, nameof(errors));
2223

23-
var document = new ErrorDocument(errors);
24+
var document = new ErrorDocument
25+
{
26+
Errors = errors.ToList()
27+
};
2428

2529
return new ObjectResult(document)
2630
{

src/JsonApiDotNetCore/Errors/InvalidModelStateException.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using JetBrains.Annotations;
99
using JsonApiDotNetCore.Controllers;
1010
using JsonApiDotNetCore.Resources.Annotations;
11+
using JsonApiDotNetCore.Serialization;
1112
using JsonApiDotNetCore.Serialization.Objects;
1213
using Microsoft.AspNetCore.Mvc.ModelBinding;
1314

@@ -116,6 +117,7 @@ private static ErrorObject FromModelError(ModelError modelError, string attribut
116117

117118
if (includeExceptionStackTraceInErrors && modelError.Exception != null)
118119
{
120+
error.Meta ??= new Dictionary<string, object>();
119121
error.Meta.IncludeExceptionStackTrace(modelError.Exception.Demystify());
120122
}
121123

src/JsonApiDotNetCore/Errors/InvalidQueryStringParameterException.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ public InvalidQueryStringParameterException(string queryParameterName, string ge
1818
{
1919
Title = genericMessage,
2020
Detail = specificMessage,
21-
Source =
21+
Source = new ErrorSource
2222
{
2323
Parameter = queryParameterName
2424
}

src/JsonApiDotNetCore/Errors/ResourceIdInCreateResourceNotAllowedException.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ public ResourceIdInCreateResourceNotAllowedException(int? atomicOperationIndex =
1616
Title = atomicOperationIndex == null
1717
? "Specifying the resource ID in POST requests is not allowed."
1818
: "Specifying the resource ID in operations that create a resource is not allowed.",
19-
Source =
19+
Source = new ErrorSource
2020
{
2121
Pointer = atomicOperationIndex != null ? $"/atomic:operations[{atomicOperationIndex}]/data/id" : "/data/id"
2222
}

src/JsonApiDotNetCore/Errors/UnsuccessfulActionResultException.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ private static ErrorObject ToError(ProblemDetails problemDetails)
4343

4444
if (!string.IsNullOrWhiteSpace(problemDetails.Type))
4545
{
46+
error.Links ??= new ErrorLinks();
4647
error.Links.About = problemDetails.Type;
4748
}
4849

src/JsonApiDotNetCore/Middleware/ExceptionHandler.cs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
using System;
22
using System.Collections.Generic;
33
using System.Diagnostics;
4+
using System.Linq;
45
using System.Net;
56
using JetBrains.Annotations;
67
using JsonApiDotNetCore.Configuration;
78
using JsonApiDotNetCore.Errors;
9+
using JsonApiDotNetCore.Serialization;
810
using JsonApiDotNetCore.Serialization.Objects;
911
using Microsoft.Extensions.Logging;
1012

@@ -88,14 +90,21 @@ protected virtual ErrorDocument CreateErrorDocument(Exception exception)
8890
ApplyOptions(error, exception);
8991
}
9092

91-
return new ErrorDocument(errors);
93+
return new ErrorDocument
94+
{
95+
Errors = errors.ToList()
96+
};
9297
}
9398

9499
private void ApplyOptions(ErrorObject error, Exception exception)
95100
{
96101
Exception resultException = exception is InvalidModelStateException ? null : exception;
97102

98-
error.Meta.IncludeExceptionStackTrace(_options.IncludeExceptionStackTraceInErrors ? resultException : null);
103+
if (resultException != null && _options.IncludeExceptionStackTraceInErrors)
104+
{
105+
error.Meta ??= new Dictionary<string, object>();
106+
error.Meta.IncludeExceptionStackTrace(resultException);
107+
}
99108
}
100109
}
101110
}

src/JsonApiDotNetCore/Middleware/JsonApiMiddleware.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,11 @@ private static async Task FlushResponseAsync(HttpResponse httpResponse, JsonSeri
209209
httpResponse.ContentType = HeaderConstants.MediaType;
210210
httpResponse.StatusCode = (int)error.StatusCode;
211211

212+
var errorDocument = new ErrorDocument
213+
{
214+
Errors = error.AsList()
215+
};
216+
212217
var serializer = JsonSerializer.CreateDefault(serializerSettings);
213218
serializer.ApplyErrorSettings();
214219

@@ -218,7 +223,7 @@ private static async Task FlushResponseAsync(HttpResponse httpResponse, JsonSeri
218223
await using (var streamWriter = new StreamWriter(stream, leaveOpen: true))
219224
{
220225
using var jsonWriter = new JsonTextWriter(streamWriter);
221-
serializer.Serialize(jsonWriter, new ErrorDocument(error));
226+
serializer.Serialize(jsonWriter, errorDocument);
222227
}
223228

224229
stream.Seek(0, SeekOrigin.Begin);

src/JsonApiDotNetCore/Serialization/JsonApiReader.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ private InvalidRequestBodyException ToInvalidRequestBodyException(JsonApiSeriali
108108
{
109109
foreach (ErrorObject error in requestException.Errors)
110110
{
111+
error.Source ??= new ErrorSource();
111112
error.Source.Pointer = $"/atomic:operations[{exception.AtomicOperationIndex}]";
112113
}
113114
}

src/JsonApiDotNetCore/Serialization/JsonApiWriter.cs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System;
22
using System.Collections.Generic;
33
using System.IO;
4+
using System.Linq;
45
using System.Net;
56
using System.Net.Http;
67
using System.Text;
@@ -131,12 +132,18 @@ private static object WrapErrors(object contextObject)
131132
{
132133
if (contextObject is IEnumerable<ErrorObject> errors)
133134
{
134-
return new ErrorDocument(errors);
135+
return new ErrorDocument
136+
{
137+
Errors = errors.ToList()
138+
};
135139
}
136140

137141
if (contextObject is ErrorObject error)
138142
{
139-
return new ErrorDocument(error);
143+
return new ErrorDocument
144+
{
145+
Errors = error.AsList()
146+
};
140147
}
141148

142149
return contextObject;
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
using System;
2+
using System.Collections.Generic;
3+
4+
namespace JsonApiDotNetCore.Serialization
5+
{
6+
internal static class MetaExtensions
7+
{
8+
public static void IncludeExceptionStackTrace(this IDictionary<string, object> meta, Exception exception)
9+
{
10+
ArgumentGuard.NotNull(exception, nameof(exception));
11+
12+
meta["StackTrace"] = exception.ToString().Split("\n", int.MaxValue, StringSplitOptions.RemoveEmptyEntries);
13+
}
14+
}
15+
}

src/JsonApiDotNetCore/Serialization/Objects/AtomicOperationCode.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
namespace JsonApiDotNetCore.Serialization.Objects
55
{
66
/// <summary>
7-
/// See https://jsonapi.org/ext/atomic/#operation-objects.
7+
/// See "op" in https://jsonapi.org/ext/atomic/#operation-objects.
88
/// </summary>
99
[JsonConverter(typeof(StringEnumConverter))]
1010
public enum AtomicOperationCode

src/JsonApiDotNetCore/Serialization/Objects/AtomicOperationObject.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ namespace JsonApiDotNetCore.Serialization.Objects
1010
public sealed class AtomicOperationObject : ExposableData<ResourceObject>
1111
{
1212
[JsonProperty("meta", NullValueHandling = NullValueHandling.Ignore)]
13-
public Dictionary<string, object> Meta { get; set; }
13+
public IDictionary<string, object> Meta { get; set; }
1414

1515
[JsonProperty("op")]
1616
[JsonConverter(typeof(StringEnumConverter))]

src/JsonApiDotNetCore/Serialization/Objects/AtomicOperationsDocument.cs

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,33 +8,18 @@ namespace JsonApiDotNetCore.Serialization.Objects
88
/// </summary>
99
public sealed class AtomicOperationsDocument
1010
{
11-
/// <summary>
12-
/// See "meta" in https://jsonapi.org/format/#document-top-level.
13-
/// </summary>
1411
[JsonProperty("meta", NullValueHandling = NullValueHandling.Ignore)]
1512
public IDictionary<string, object> Meta { get; set; }
1613

17-
/// <summary>
18-
/// See "jsonapi" in https://jsonapi.org/format/#document-top-level.
19-
/// </summary>
2014
[JsonProperty("jsonapi", NullValueHandling = NullValueHandling.Ignore)]
2115
public JsonApiObject JsonApi { get; set; }
2216

23-
/// <summary>
24-
/// See "links" in https://jsonapi.org/format/#document-top-level.
25-
/// </summary>
2617
[JsonProperty("links", NullValueHandling = NullValueHandling.Ignore)]
2718
public TopLevelLinks Links { get; set; }
2819

29-
/// <summary>
30-
/// See https://jsonapi.org/ext/atomic/#operation-objects.
31-
/// </summary>
3220
[JsonProperty("atomic:operations", NullValueHandling = NullValueHandling.Ignore)]
3321
public IList<AtomicOperationObject> Operations { get; set; }
3422

35-
/// <summary>
36-
/// See https://jsonapi.org/ext/atomic/#result-objects.
37-
/// </summary>
3823
[JsonProperty("atomic:results", NullValueHandling = NullValueHandling.Ignore)]
3924
public IList<AtomicResultObject> Results { get; set; }
4025
}
Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,13 @@
1-
using System.Text;
21
using Newtonsoft.Json;
32

43
namespace JsonApiDotNetCore.Serialization.Objects
54
{
65
/// <summary>
7-
/// See 'ref' in https://jsonapi.org/ext/atomic/#operation-objects.
6+
/// See "ref" in https://jsonapi.org/ext/atomic/#operation-objects.
87
/// </summary>
98
public sealed class AtomicReference : ResourceIdentifierObject
109
{
1110
[JsonProperty("relationship", NullValueHandling = NullValueHandling.Ignore)]
1211
public string Relationship { get; set; }
13-
14-
protected override void WriteMembers(StringBuilder builder)
15-
{
16-
base.WriteMembers(builder);
17-
WriteMember(builder, "relationship", Relationship);
18-
}
1912
}
2013
}

src/JsonApiDotNetCore/Serialization/Objects/AtomicResultObject.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,6 @@ namespace JsonApiDotNetCore.Serialization.Objects
99
public sealed class AtomicResultObject : ExposableData<ResourceObject>
1010
{
1111
[JsonProperty("meta", NullValueHandling = NullValueHandling.Ignore)]
12-
public Dictionary<string, object> Meta { get; set; }
12+
public IDictionary<string, object> Meta { get; set; }
1313
}
1414
}

src/JsonApiDotNetCore/Serialization/Objects/Document.cs

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,31 +4,19 @@
44
namespace JsonApiDotNetCore.Serialization.Objects
55
{
66
/// <summary>
7-
/// https://jsonapi.org/format/#document-structure
7+
/// See https://jsonapi.org/format/1.1/#document-top-level.
88
/// </summary>
99
public sealed class Document : ExposableData<ResourceObject>
1010
{
11-
/// <summary>
12-
/// see "meta" in https://jsonapi.org/format/#document-top-level
13-
/// </summary>
1411
[JsonProperty("meta", NullValueHandling = NullValueHandling.Ignore)]
1512
public IDictionary<string, object> Meta { get; set; }
1613

17-
/// <summary>
18-
/// see "jsonapi" in https://jsonapi.org/format/#document-top-level
19-
/// </summary>
2014
[JsonProperty("jsonapi", NullValueHandling = NullValueHandling.Ignore)]
2115
public JsonApiObject JsonApi { get; set; }
2216

23-
/// <summary>
24-
/// see "links" in https://jsonapi.org/format/#document-top-level
25-
/// </summary>
2617
[JsonProperty("links", NullValueHandling = NullValueHandling.Ignore)]
2718
public TopLevelLinks Links { get; set; }
2819

29-
/// <summary>
30-
/// see "included" in https://jsonapi.org/format/#document-top-level
31-
/// </summary>
3220
[JsonProperty("included", NullValueHandling = NullValueHandling.Ignore, Order = 1)]
3321
public IList<ResourceObject> Included { get; set; }
3422
}

src/JsonApiDotNetCore/Serialization/Objects/ErrorDocument.cs

Lines changed: 9 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,22 @@
1-
using System;
21
using System.Collections.Generic;
32
using System.Linq;
43
using System.Net;
5-
using JetBrains.Annotations;
4+
using Newtonsoft.Json;
65

76
namespace JsonApiDotNetCore.Serialization.Objects
87
{
9-
[PublicAPI]
8+
/// <summary>
9+
/// See https://jsonapi.org/format/1.1/#document-top-level.
10+
/// </summary>
1011
public sealed class ErrorDocument
1112
{
12-
public IReadOnlyList<ErrorObject> Errors { get; }
13+
[JsonProperty("errors")]
14+
public IList<ErrorObject> Errors { get; set; }
1315

14-
public ErrorDocument()
15-
: this(Array.Empty<ErrorObject>())
16-
{
17-
}
18-
19-
public ErrorDocument(ErrorObject error)
20-
: this(error.AsEnumerable())
21-
{
22-
}
23-
24-
public ErrorDocument(IEnumerable<ErrorObject> errors)
25-
{
26-
ArgumentGuard.NotNull(errors, nameof(errors));
27-
28-
Errors = errors.ToList();
29-
}
16+
[JsonProperty("meta", NullValueHandling = NullValueHandling.Ignore)]
17+
public IDictionary<string, object> Meta { get; set; }
3018

31-
public HttpStatusCode GetErrorStatusCode()
19+
internal HttpStatusCode GetErrorStatusCode()
3220
{
3321
int[] statusCodes = Errors.Select(error => (int)error.StatusCode).Distinct().ToArray();
3422

src/JsonApiDotNetCore/Serialization/Objects/ErrorLinks.cs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,15 @@
22

33
namespace JsonApiDotNetCore.Serialization.Objects
44
{
5+
/// <summary>
6+
/// See "links" in https://jsonapi.org/format/1.1/#error-objects.
7+
/// </summary>
58
public sealed class ErrorLinks
69
{
7-
/// <summary>
8-
/// A URL that leads to further details about this particular occurrence of the problem.
9-
/// </summary>
10-
[JsonProperty]
10+
[JsonProperty("about", NullValueHandling = NullValueHandling.Ignore)]
1111
public string About { get; set; }
12+
13+
[JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)]
14+
public string Type { get; set; }
1215
}
1316
}

0 commit comments

Comments
 (0)