diff --git a/JSONAPI.Tests/Data/ErrorSerializerTest.json b/JSONAPI.Tests/Data/ErrorSerializerTest.json
new file mode 100644
index 00000000..e64ce626
--- /dev/null
+++ b/JSONAPI.Tests/Data/ErrorSerializerTest.json
@@ -0,0 +1,12 @@
+{
+ "errors": [
+ {
+ "id": "TEST-ERROR-ID",
+ "status": "500",
+ "title": "System.Exception",
+ "detail": "This is the exception message!",
+ "inner": null,
+ "stackTrace": "Stack trace would go here"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/JSONAPI.Tests/Data/FormatterErrorSerializationTest.json b/JSONAPI.Tests/Data/FormatterErrorSerializationTest.json
new file mode 100644
index 00000000..f5439ca2
--- /dev/null
+++ b/JSONAPI.Tests/Data/FormatterErrorSerializationTest.json
@@ -0,0 +1 @@
+{"test":"foo"}
\ No newline at end of file
diff --git a/JSONAPI.Tests/JSONAPI.Tests.csproj b/JSONAPI.Tests/JSONAPI.Tests.csproj
index fa20d111..b6265c16 100644
--- a/JSONAPI.Tests/JSONAPI.Tests.csproj
+++ b/JSONAPI.Tests/JSONAPI.Tests.csproj
@@ -38,6 +38,12 @@
false
+
+ ..\packages\FluentAssertions.3.2.2\lib\net45\FluentAssertions.dll
+
+
+ ..\packages\FluentAssertions.3.2.2\lib\net45\FluentAssertions.Core.dll
+
False
@@ -60,6 +66,8 @@
False
..\packages\Microsoft.AspNet.WebApi.WebHost.5.2.2\lib\net45\System.Web.Http.WebHost.dll
+
+
@@ -68,7 +76,9 @@
+
+
@@ -82,6 +92,12 @@
+
+ Always
+
+
+ Always
+
Always
diff --git a/JSONAPI.Tests/Json/ErrorSerializerTests.cs b/JSONAPI.Tests/Json/ErrorSerializerTests.cs
new file mode 100644
index 00000000..9a1a60e5
--- /dev/null
+++ b/JSONAPI.Tests/Json/ErrorSerializerTests.cs
@@ -0,0 +1,64 @@
+using System;
+using System.IO;
+using System.Web.Http;
+using FluentAssertions;
+using JSONAPI.Json;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Newtonsoft.Json;
+
+namespace JSONAPI.Tests.Json
+{
+ [TestClass]
+ public class ErrorSerializerTests
+ {
+ private class TestErrorIdProvider : IErrorIdProvider
+ {
+ public string GenerateId(HttpError error)
+ {
+ return "TEST-ERROR-ID";
+ }
+ }
+
+ [TestMethod]
+ public void CanSerialize_returns_true_for_HttpError()
+ {
+ var serializer = new ErrorSerializer();
+ var result = serializer.CanSerialize(typeof (HttpError));
+ result.Should().BeTrue();
+ }
+
+ [TestMethod]
+ public void CanSerialize_returns_false_for_Exception()
+ {
+ var serializer = new ErrorSerializer();
+ var result = serializer.CanSerialize(typeof(Exception));
+ result.Should().BeFalse();
+ }
+
+ [TestMethod]
+ [DeploymentItem(@"Data\ErrorSerializerTest.json")]
+ public void SerializeError_serializes_httperror()
+ {
+ using (var stream = new MemoryStream())
+ {
+ var textWriter = new StreamWriter(stream);
+ var writer = new JsonTextWriter(textWriter);
+ var error = new HttpError(new Exception("This is the exception message!"), true)
+ {
+ StackTrace = "Stack trace would go here"
+ };
+ var jsonSerializer = new JsonSerializer();
+
+ var serializer = new ErrorSerializer(new TestErrorIdProvider());
+ serializer.SerializeError(error, stream, writer, jsonSerializer);
+
+ writer.Flush();
+
+ var expectedJson = File.ReadAllText("ErrorSerializerTest.json");
+ var minifiedExpectedJson = JsonHelpers.MinifyJson(expectedJson);
+ var output = System.Text.Encoding.ASCII.GetString(stream.ToArray());
+ output.Should().Be(minifiedExpectedJson);
+ }
+ }
+ }
+}
diff --git a/JSONAPI.Tests/Json/JsonApiMediaFormaterTests.cs b/JSONAPI.Tests/Json/JsonApiMediaFormaterTests.cs
index 1834171c..1a572d10 100644
--- a/JSONAPI.Tests/Json/JsonApiMediaFormaterTests.cs
+++ b/JSONAPI.Tests/Json/JsonApiMediaFormaterTests.cs
@@ -1,5 +1,8 @@
using System;
using System.Linq;
+using System.Text.RegularExpressions;
+using System.Web.Http;
+using FluentAssertions;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using JSONAPI.Tests.Models;
using Newtonsoft.Json;
@@ -17,6 +20,22 @@ public class JsonApiMediaFormaterTests
Author a;
Post p, p2, p3, p4;
+ private class MockErrorSerializer : IErrorSerializer
+ {
+ public bool CanSerialize(Type type)
+ {
+ return true;
+ }
+
+ public void SerializeError(object error, Stream writeStream, JsonWriter writer, JsonSerializer serializer)
+ {
+ writer.WriteStartObject();
+ writer.WritePropertyName("test");
+ serializer.Serialize(writer, "foo");
+ writer.WriteEndObject();
+ }
+ }
+
[TestInitialize]
public void SetupModels()
{
@@ -155,6 +174,52 @@ public void SerializeArrayIntegrationTest()
//Assert.AreEqual("[2,3,4]", sw.ToString());
}
+ [TestMethod]
+ [DeploymentItem(@"Data\FormatterErrorSerializationTest.json")]
+ public void Should_serialize_error()
+ {
+ // Arrange
+ var formatter = new JSONAPI.Json.JsonApiFormatter(new MockErrorSerializer());
+ formatter.PluralizationService = new JSONAPI.Core.PluralizationService();
+ var stream = new MemoryStream();
+
+ // Act
+ var payload = new HttpError(new Exception(), true);
+ formatter.WriteToStreamAsync(typeof(HttpError), payload, stream, (System.Net.Http.HttpContent)null, (System.Net.TransportContext)null);
+
+ // Assert
+ var expectedJson = File.ReadAllText("FormatterErrorSerializationTest.json");
+ var minifiedExpectedJson = JsonHelpers.MinifyJson(expectedJson);
+ var output = System.Text.Encoding.ASCII.GetString(stream.ToArray());
+ output.Should().Be(minifiedExpectedJson);
+ }
+
+ [TestMethod]
+ [DeploymentItem(@"Data\ErrorSerializerTest.json")]
+ public void SerializeErrorIntegrationTest()
+ {
+ // Arrange
+ JsonApiFormatter formatter = new JSONAPI.Json.JsonApiFormatter();
+ formatter.PluralizationService = new JSONAPI.Core.PluralizationService();
+ MemoryStream stream = new MemoryStream();
+
+ // Act
+ var payload = new HttpError(new Exception("This is the exception message!"), true)
+ {
+ StackTrace = "Stack trace would go here"
+ };
+ formatter.WriteToStreamAsync(typeof(HttpError), payload, stream, (System.Net.Http.HttpContent)null, (System.Net.TransportContext)null);
+
+ // Assert
+ var expectedJson = File.ReadAllText("ErrorSerializerTest.json");
+ var minifiedExpectedJson = JsonHelpers.MinifyJson(expectedJson);
+ var output = System.Text.Encoding.ASCII.GetString(stream.ToArray());
+ output = Regex.Replace(output,
+ @"[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}",
+ "TEST-ERROR-ID"); // We don't know what the GUID will be, so replace it
+ output.Should().Be(minifiedExpectedJson);
+ }
+
[TestMethod]
public void DeserializeCollectionIntegrationTest()
{
diff --git a/JSONAPI.Tests/Json/JsonHelpers.cs b/JSONAPI.Tests/Json/JsonHelpers.cs
new file mode 100644
index 00000000..440e2e1c
--- /dev/null
+++ b/JSONAPI.Tests/Json/JsonHelpers.cs
@@ -0,0 +1,13 @@
+using System.Text.RegularExpressions;
+
+namespace JSONAPI.Tests.Json
+{
+ static class JsonHelpers
+ {
+ // http://stackoverflow.com/questions/8913138/minify-indented-json-string-in-net
+ public static string MinifyJson(string input)
+ {
+ return Regex.Replace(input, "(\"(?:[^\"\\\\]|\\\\.)*\")|\\s+", "$1");
+ }
+ }
+}
diff --git a/JSONAPI.Tests/packages.config b/JSONAPI.Tests/packages.config
index 22761302..21995fcd 100644
--- a/JSONAPI.Tests/packages.config
+++ b/JSONAPI.Tests/packages.config
@@ -1,5 +1,6 @@
+
diff --git a/JSONAPI/JSONAPI.csproj b/JSONAPI/JSONAPI.csproj
index 06d28f1b..b7e05b55 100644
--- a/JSONAPI/JSONAPI.csproj
+++ b/JSONAPI/JSONAPI.csproj
@@ -83,6 +83,10 @@
+
+
+
+
diff --git a/JSONAPI/Json/ErrorSerializer.cs b/JSONAPI/Json/ErrorSerializer.cs
new file mode 100644
index 00000000..3451193a
--- /dev/null
+++ b/JSONAPI/Json/ErrorSerializer.cs
@@ -0,0 +1,77 @@
+using System;
+using System.IO;
+using System.Web.Http;
+using Newtonsoft.Json;
+
+namespace JSONAPI.Json
+{
+ internal class ErrorSerializer : IErrorSerializer
+ {
+ private class JsonApiError
+ {
+ [JsonProperty(PropertyName = "id")]
+ public string Id { get; set; }
+
+ [JsonProperty(PropertyName = "status")]
+ public string Status { get; set; }
+
+ [JsonProperty(PropertyName = "title")]
+ public string Title { get; set; }
+
+ [JsonProperty(PropertyName = "detail")]
+ public string Detail { get; set; }
+
+ [JsonProperty(PropertyName = "inner")]
+ public JsonApiError Inner { get; set; }
+
+ [JsonProperty(PropertyName = "stackTrace")]
+ public string StackTrace { get; set; }
+
+ public JsonApiError(HttpError error)
+ {
+ Title = error.ExceptionType ?? error.Message;
+ Status = "500";
+ Detail = error.ExceptionMessage ?? error.MessageDetail;
+ StackTrace = error.StackTrace;
+
+ if (error.InnerException != null)
+ Inner = new JsonApiError(error.InnerException);
+ }
+ }
+
+ private readonly IErrorIdProvider _errorIdProvider;
+
+ public ErrorSerializer()
+ : this(new GuidErrorIdProvider())
+ {
+
+ }
+
+ public ErrorSerializer(IErrorIdProvider errorIdProvider)
+ {
+ _errorIdProvider = errorIdProvider;
+ }
+
+ public bool CanSerialize(Type type)
+ {
+ return type == typeof (HttpError);
+ }
+
+ public void SerializeError(object error, Stream writeStream, JsonWriter writer, JsonSerializer serializer)
+ {
+ var httpError = error as HttpError;
+ if (httpError == null) throw new Exception("Unsupported error type.");
+
+ writer.WriteStartObject();
+ writer.WritePropertyName("errors");
+
+ var jsonApiError = new JsonApiError(httpError)
+ {
+ Id = _errorIdProvider.GenerateId(httpError)
+ };
+ serializer.Serialize(writer, new[] { jsonApiError });
+
+ writer.WriteEndObject();
+ }
+ }
+}
diff --git a/JSONAPI/Json/GuidErrorIdProvider.cs b/JSONAPI/Json/GuidErrorIdProvider.cs
new file mode 100644
index 00000000..5205d478
--- /dev/null
+++ b/JSONAPI/Json/GuidErrorIdProvider.cs
@@ -0,0 +1,13 @@
+using System;
+using System.Web.Http;
+
+namespace JSONAPI.Json
+{
+ internal class GuidErrorIdProvider : IErrorIdProvider
+ {
+ public string GenerateId(HttpError error)
+ {
+ return Guid.NewGuid().ToString();
+ }
+ }
+}
diff --git a/JSONAPI/Json/IErrorIdProvider.cs b/JSONAPI/Json/IErrorIdProvider.cs
new file mode 100644
index 00000000..206cf5c7
--- /dev/null
+++ b/JSONAPI/Json/IErrorIdProvider.cs
@@ -0,0 +1,9 @@
+using System.Web.Http;
+
+namespace JSONAPI.Json
+{
+ internal interface IErrorIdProvider
+ {
+ string GenerateId(HttpError error);
+ }
+}
diff --git a/JSONAPI/Json/IErrorSerializer.cs b/JSONAPI/Json/IErrorSerializer.cs
new file mode 100644
index 00000000..886c832c
--- /dev/null
+++ b/JSONAPI/Json/IErrorSerializer.cs
@@ -0,0 +1,12 @@
+using Newtonsoft.Json;
+using System;
+using System.IO;
+
+namespace JSONAPI.Json
+{
+ internal interface IErrorSerializer
+ {
+ bool CanSerialize(Type type);
+ void SerializeError(object error, Stream writeStream, JsonWriter writer, JsonSerializer serializer);
+ }
+}
diff --git a/JSONAPI/Json/JsonApiFormatter.cs b/JSONAPI/Json/JsonApiFormatter.cs
index 770e74f9..e8116f2f 100644
--- a/JSONAPI/Json/JsonApiFormatter.cs
+++ b/JSONAPI/Json/JsonApiFormatter.cs
@@ -20,11 +20,18 @@ namespace JSONAPI.Json
public class JsonApiFormatter : JsonMediaTypeFormatter
{
public JsonApiFormatter()
+ : this(new ErrorSerializer())
{
+ }
+
+ internal JsonApiFormatter(IErrorSerializer errorSerializer)
+ {
+ _errorSerializer = errorSerializer;
SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/vnd.api+json"));
}
public IPluralizationService PluralizationService { get; set; }
+ private readonly IErrorSerializer _errorSerializer;
private Lazy> _relationAggregators
= new Lazy>(
@@ -79,32 +86,40 @@ public override Task WriteToStreamAsync(System.Type type, object value, Stream w
this.RelationAggregators[writeStream] = aggregator;
}
}
-
- Type valtype = GetSingleType(value.GetType());
- if (IsMany(value.GetType()))
- aggregator.AddPrimary(valtype, (IEnumerable