diff --git a/src/JsonApiDotNetCore/Configuration/IJsonApiOptions.cs b/src/JsonApiDotNetCore/Configuration/IJsonApiOptions.cs
index 707d1d0944..f735e9c2ef 100644
--- a/src/JsonApiDotNetCore/Configuration/IJsonApiOptions.cs
+++ b/src/JsonApiDotNetCore/Configuration/IJsonApiOptions.cs
@@ -34,6 +34,11 @@ internal NamingStrategy SerializerNamingStrategy
///
AttrCapabilities DefaultAttrCapabilities { get; }
+ ///
+ /// Indicates whether responses should contain a jsonapi object that contains the highest JSON:API version supported. False by default.
+ ///
+ bool IncludeJsonApiVersion { get; }
+
///
/// Whether or not stack traces should be serialized in objects. False by default.
///
diff --git a/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs b/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs
index c1079fad58..8214b576ed 100644
--- a/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs
+++ b/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs
@@ -22,6 +22,9 @@ public sealed class JsonApiOptions : IJsonApiOptions
///
public AttrCapabilities DefaultAttrCapabilities { get; set; } = AttrCapabilities.All;
+ ///
+ public bool IncludeJsonApiVersion { get; set; }
+
///
public bool IncludeExceptionStackTraceInErrors { get; set; }
diff --git a/src/JsonApiDotNetCore/Serialization/AtomicOperationsResponseSerializer.cs b/src/JsonApiDotNetCore/Serialization/AtomicOperationsResponseSerializer.cs
index aeb773dd88..00d28fe23f 100644
--- a/src/JsonApiDotNetCore/Serialization/AtomicOperationsResponseSerializer.cs
+++ b/src/JsonApiDotNetCore/Serialization/AtomicOperationsResponseSerializer.cs
@@ -67,6 +67,18 @@ private string SerializeOperationsDocument(IEnumerable opera
Meta = _metaBuilder.Build()
};
+ if (_options.IncludeJsonApiVersion)
+ {
+ document.JsonApi = new JsonApiObject
+ {
+ Version = "1.1",
+ Ext = new List
+ {
+ "https://jsonapi.org/ext/atomic"
+ }
+ };
+ }
+
return SerializeObject(document, _options.SerializerSettings);
}
diff --git a/src/JsonApiDotNetCore/Serialization/Objects/AtomicOperationsDocument.cs b/src/JsonApiDotNetCore/Serialization/Objects/AtomicOperationsDocument.cs
index 8d08f5fb31..b7352ed4c6 100644
--- a/src/JsonApiDotNetCore/Serialization/Objects/AtomicOperationsDocument.cs
+++ b/src/JsonApiDotNetCore/Serialization/Objects/AtomicOperationsDocument.cs
@@ -18,7 +18,7 @@ public sealed class AtomicOperationsDocument
/// See "jsonapi" in https://jsonapi.org/format/#document-top-level.
///
[JsonProperty("jsonapi", NullValueHandling = NullValueHandling.Ignore)]
- public IDictionary JsonApi { get; set; }
+ public JsonApiObject JsonApi { get; set; }
///
/// See "links" in https://jsonapi.org/format/#document-top-level.
diff --git a/src/JsonApiDotNetCore/Serialization/Objects/Document.cs b/src/JsonApiDotNetCore/Serialization/Objects/Document.cs
index 386309aedd..56c79f2b86 100644
--- a/src/JsonApiDotNetCore/Serialization/Objects/Document.cs
+++ b/src/JsonApiDotNetCore/Serialization/Objects/Document.cs
@@ -18,7 +18,7 @@ public sealed class Document : ExposableData
/// see "jsonapi" in https://jsonapi.org/format/#document-top-level
///
[JsonProperty("jsonapi", NullValueHandling = NullValueHandling.Ignore)]
- public IDictionary JsonApi { get; set; }
+ public JsonApiObject JsonApi { get; set; }
///
/// see "links" in https://jsonapi.org/format/#document-top-level
diff --git a/src/JsonApiDotNetCore/Serialization/Objects/JsonApiObject.cs b/src/JsonApiDotNetCore/Serialization/Objects/JsonApiObject.cs
new file mode 100644
index 0000000000..66682fb6a2
--- /dev/null
+++ b/src/JsonApiDotNetCore/Serialization/Objects/JsonApiObject.cs
@@ -0,0 +1,26 @@
+using System.Collections.Generic;
+using Newtonsoft.Json;
+
+namespace JsonApiDotNetCore.Serialization.Objects
+{
+ ///
+ /// https://jsonapi.org/format/1.1/#document-jsonapi-object.
+ ///
+ public sealed class JsonApiObject
+ {
+ [JsonProperty("version", NullValueHandling = NullValueHandling.Ignore)]
+ public string Version { get; set; }
+
+ [JsonProperty("ext", NullValueHandling = NullValueHandling.Ignore)]
+ public ICollection Ext { get; set; }
+
+ [JsonProperty("profile", NullValueHandling = NullValueHandling.Ignore)]
+ public ICollection Profile { get; set; }
+
+ ///
+ /// see "meta" in https://jsonapi.org/format/1.1/#document-meta
+ ///
+ [JsonProperty("meta", NullValueHandling = NullValueHandling.Ignore)]
+ public IDictionary Meta { get; set; }
+ }
+}
diff --git a/src/JsonApiDotNetCore/Serialization/ResponseSerializer.cs b/src/JsonApiDotNetCore/Serialization/ResponseSerializer.cs
index fb0b60f5fd..8e2dbd41f1 100644
--- a/src/JsonApiDotNetCore/Serialization/ResponseSerializer.cs
+++ b/src/JsonApiDotNetCore/Serialization/ResponseSerializer.cs
@@ -149,6 +149,14 @@ internal string SerializeMany(IReadOnlyCollection resources)
///
private void AddTopLevelObjects(Document document)
{
+ if (_options.IncludeJsonApiVersion)
+ {
+ document.JsonApi = new JsonApiObject
+ {
+ Version = "1.1"
+ };
+ }
+
document.Links = _linkBuilder.GetTopLevelLinks();
document.Meta = _metaBuilder.Build();
document.Included = _includedBuilder.Build();
diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Mixed/AtomicSerializationTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Mixed/AtomicSerializationTests.cs
new file mode 100644
index 0000000000..52dd8b2efa
--- /dev/null
+++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Mixed/AtomicSerializationTests.cs
@@ -0,0 +1,101 @@
+using System.Net;
+using System.Net.Http;
+using System.Threading.Tasks;
+using FluentAssertions;
+using JsonApiDotNetCore.Configuration;
+using JsonApiDotNetCore.Resources;
+using JsonApiDotNetCoreExample.Controllers;
+using JsonApiDotNetCoreExampleTests.Startups;
+using Microsoft.Extensions.DependencyInjection;
+using TestBuildingBlocks;
+using Xunit;
+
+namespace JsonApiDotNetCoreExampleTests.IntegrationTests.AtomicOperations.Mixed
+{
+ public sealed class AtomicSerializationTests : IClassFixture, OperationsDbContext>>
+ {
+ private readonly ExampleIntegrationTestContext, OperationsDbContext> _testContext;
+ private readonly OperationsFakers _fakers = new OperationsFakers();
+
+ public AtomicSerializationTests(ExampleIntegrationTestContext, OperationsDbContext> testContext)
+ {
+ _testContext = testContext;
+
+ testContext.UseController();
+
+ testContext.ConfigureServicesAfterStartup(services =>
+ {
+ services.AddScoped(typeof(IResourceChangeTracker<>), typeof(NeverSameResourceChangeTracker<>));
+ });
+
+ var options = (JsonApiOptions)testContext.Factory.Services.GetRequiredService();
+ options.IncludeJsonApiVersion = true;
+ options.AllowClientGeneratedIds = true;
+ }
+
+ [Fact]
+ public async Task Includes_version_with_ext_on_operations_endpoint()
+ {
+ // Arrange
+ const int newArtistId = 12345;
+ string newArtistName = _fakers.Performer.Generate().ArtistName;
+
+ await _testContext.RunOnDatabaseAsync(async dbContext =>
+ {
+ await dbContext.ClearTableAsync();
+ });
+
+ var requestBody = new
+ {
+ atomic__operations = new[]
+ {
+ new
+ {
+ op = "add",
+ data = new
+ {
+ type = "performers",
+ id = newArtistId,
+ attributes = new
+ {
+ artistName = newArtistName
+ }
+ }
+ }
+ }
+ };
+
+ const string route = "/operations";
+
+ // Act
+ (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody);
+
+ // Assert
+ httpResponse.Should().HaveStatusCode(HttpStatusCode.OK);
+
+ responseDocument.Should().BeJson(@"{
+ ""jsonapi"": {
+ ""version"": ""1.1"",
+ ""ext"": [
+ ""https://jsonapi.org/ext/atomic""
+ ]
+ },
+ ""atomic:results"": [
+ {
+ ""data"": {
+ ""type"": ""performers"",
+ ""id"": """ + newArtistId + @""",
+ ""attributes"": {
+ ""artistName"": """ + newArtistName + @""",
+ ""bornAt"": ""0001-01-01T01:00:00+01:00""
+ },
+ ""links"": {
+ ""self"": ""http://localhost/performers/" + newArtistId + @"""
+ }
+ }
+ }
+ ]
+}");
+ }
+ }
+}
diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Serialization/SerializationTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Serialization/SerializationTests.cs
index c9e5c821d7..bd603c745d 100644
--- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Serialization/SerializationTests.cs
+++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Serialization/SerializationTests.cs
@@ -37,6 +37,7 @@ public SerializationTests(ExampleIntegrationTestContext();
options.IncludeExceptionStackTraceInErrors = false;
options.AllowClientGeneratedIds = true;
+ options.IncludeJsonApiVersion = false;
}
[Fact]
@@ -555,6 +556,40 @@ await _testContext.RunOnDatabaseAsync(async dbContext =>
""self"": ""http://localhost/meetingAttendees/" + existingAttendee.StringId + @"""
}
}
+}");
+ }
+
+ [Fact]
+ public async Task Includes_version_on_resource_endpoint()
+ {
+ // Arrange
+ var options = (JsonApiOptions)_testContext.Factory.Services.GetRequiredService();
+ options.IncludeJsonApiVersion = true;
+
+ MeetingAttendee attendee = _fakers.MeetingAttendee.Generate();
+
+ await _testContext.RunOnDatabaseAsync(async dbContext =>
+ {
+ dbContext.Attendees.Add(attendee);
+ await dbContext.SaveChangesAsync();
+ });
+
+ string route = $"/meetingAttendees/{attendee.StringId}/meeting";
+
+ // Act
+ (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecuteGetAsync(route);
+
+ // Assert
+ httpResponse.Should().HaveStatusCode(HttpStatusCode.OK);
+
+ responseDocument.Should().BeJson(@"{
+ ""jsonapi"": {
+ ""version"": ""1.1""
+ },
+ ""links"": {
+ ""self"": ""http://localhost/meetingAttendees/" + attendee.StringId + @"/meeting""
+ },
+ ""data"": null
}");
}
}